From 5cfcee12172d571c787b6d34af8f8e36183fc186 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Wed, 24 Jun 2026 11:17:30 +0000 Subject: [PATCH 001/137] feat: add local registry flow and desktop examples --- examples/README.md | 21 + examples/electron-wasix/.gitignore | 3 + examples/electron-wasix/README.md | 14 + examples/electron-wasix/index.html | 68 + examples/electron-wasix/package.json | 21 + examples/electron-wasix/src-wasix/Cargo.lock | 4026 +++++++++ examples/electron-wasix/src-wasix/Cargo.toml | 12 + examples/electron-wasix/src-wasix/src/main.rs | 34 + examples/electron-wasix/src/main-process.ts | 56 + examples/electron-wasix/src/preload.ts | 19 + examples/electron-wasix/src/renderer.ts | 1 + examples/electron-wasix/src/sidecar.ts | 55 + examples/electron-wasix/src/styles.css | 1 + examples/electron-wasix/src/todos.ts | 153 + examples/electron-wasix/src/types.ts | 28 + examples/electron-wasix/tsconfig.main.json | 14 + .../electron-wasix/tsconfig.renderer.json | 14 + examples/electron-wasix/vite.config.ts | 14 + examples/electron/.gitignore | 2 + examples/electron/README.md | 10 + examples/electron/index.html | 68 + examples/electron/package.json | 21 + examples/electron/src/main-process.ts | 56 + examples/electron/src/preload.ts | 19 + examples/electron/src/renderer.ts | 135 + examples/electron/src/styles.css | 1 + examples/electron/src/todos.ts | 154 + examples/electron/src/types.ts | 28 + examples/electron/tsconfig.main.json | 14 + examples/electron/tsconfig.renderer.json | 14 + examples/electron/vite.config.ts | 14 + examples/tauri-wasix/.gitignore | 4 + examples/tauri-wasix/README.md | 10 + examples/tauri-wasix/index.html | 68 + examples/tauri-wasix/package.json | 20 + examples/tauri-wasix/src-tauri/Cargo.lock | 7351 +++++++++++++++++ examples/tauri-wasix/src-tauri/Cargo.toml | 24 + examples/tauri-wasix/src-tauri/build.rs | 3 + .../src-tauri/capabilities/default.json | 7 + examples/tauri-wasix/src-tauri/src/lib.rs | 255 + examples/tauri-wasix/src-tauri/src/main.rs | 6 + .../tauri-wasix/src-tauri/tauri.conf.json | 30 + examples/tauri-wasix/src/main.ts | 1 + examples/tauri-wasix/src/styles.css | 1 + examples/tauri-wasix/tsconfig.json | 16 + examples/tauri-wasix/vite.config.ts | 9 + examples/tauri/.gitignore | 4 + examples/tauri/README.md | 11 + examples/tauri/index.html | 68 + examples/tauri/package.json | 20 + examples/tauri/src-tauri/Cargo.lock | 4589 ++++++++++ examples/tauri/src-tauri/Cargo.toml | 29 + examples/tauri/src-tauri/build.rs | 4 + .../tauri/src-tauri/capabilities/default.json | 7 + examples/tauri/src-tauri/src/lib.rs | 234 + examples/tauri/src-tauri/src/main.rs | 6 + examples/tauri/src-tauri/tauri.conf.json | 30 + examples/tauri/src/main.ts | 160 + examples/tauri/src/styles.css | 231 + examples/tauri/tsconfig.json | 16 + examples/tauri/vite.config.ts | 9 + examples/tools/check-examples.sh | 10 +- pnpm-lock.yaml | 584 ++ pnpm-workspace.yaml | 5 + src/runtimes/liboliphaunt/icu/build.rs | 87 +- tools/release/local_registry_publish.py | 754 ++ ...kage_liboliphaunt_wasix_cargo_artifacts.py | 51 +- 67 files changed, 19798 insertions(+), 6 deletions(-) create mode 100644 examples/README.md create mode 100644 examples/electron-wasix/.gitignore create mode 100644 examples/electron-wasix/README.md create mode 100644 examples/electron-wasix/index.html create mode 100644 examples/electron-wasix/package.json create mode 100644 examples/electron-wasix/src-wasix/Cargo.lock create mode 100644 examples/electron-wasix/src-wasix/Cargo.toml create mode 100644 examples/electron-wasix/src-wasix/src/main.rs create mode 100644 examples/electron-wasix/src/main-process.ts create mode 100644 examples/electron-wasix/src/preload.ts create mode 100644 examples/electron-wasix/src/renderer.ts create mode 100644 examples/electron-wasix/src/sidecar.ts create mode 100644 examples/electron-wasix/src/styles.css create mode 100644 examples/electron-wasix/src/todos.ts create mode 100644 examples/electron-wasix/src/types.ts create mode 100644 examples/electron-wasix/tsconfig.main.json create mode 100644 examples/electron-wasix/tsconfig.renderer.json create mode 100644 examples/electron-wasix/vite.config.ts create mode 100644 examples/electron/.gitignore create mode 100644 examples/electron/README.md create mode 100644 examples/electron/index.html create mode 100644 examples/electron/package.json create mode 100644 examples/electron/src/main-process.ts create mode 100644 examples/electron/src/preload.ts create mode 100644 examples/electron/src/renderer.ts create mode 100644 examples/electron/src/styles.css create mode 100644 examples/electron/src/todos.ts create mode 100644 examples/electron/src/types.ts create mode 100644 examples/electron/tsconfig.main.json create mode 100644 examples/electron/tsconfig.renderer.json create mode 100644 examples/electron/vite.config.ts create mode 100644 examples/tauri-wasix/.gitignore create mode 100644 examples/tauri-wasix/README.md create mode 100644 examples/tauri-wasix/index.html create mode 100644 examples/tauri-wasix/package.json create mode 100644 examples/tauri-wasix/src-tauri/Cargo.lock create mode 100644 examples/tauri-wasix/src-tauri/Cargo.toml create mode 100644 examples/tauri-wasix/src-tauri/build.rs create mode 100644 examples/tauri-wasix/src-tauri/capabilities/default.json create mode 100644 examples/tauri-wasix/src-tauri/src/lib.rs create mode 100644 examples/tauri-wasix/src-tauri/src/main.rs create mode 100644 examples/tauri-wasix/src-tauri/tauri.conf.json create mode 100644 examples/tauri-wasix/src/main.ts create mode 100644 examples/tauri-wasix/src/styles.css create mode 100644 examples/tauri-wasix/tsconfig.json create mode 100644 examples/tauri-wasix/vite.config.ts create mode 100644 examples/tauri/.gitignore create mode 100644 examples/tauri/README.md create mode 100644 examples/tauri/index.html create mode 100644 examples/tauri/package.json create mode 100644 examples/tauri/src-tauri/Cargo.lock create mode 100644 examples/tauri/src-tauri/Cargo.toml create mode 100644 examples/tauri/src-tauri/build.rs create mode 100644 examples/tauri/src-tauri/capabilities/default.json create mode 100644 examples/tauri/src-tauri/src/lib.rs create mode 100644 examples/tauri/src-tauri/src/main.rs create mode 100644 examples/tauri/src-tauri/tauri.conf.json create mode 100644 examples/tauri/src/main.ts create mode 100644 examples/tauri/src/styles.css create mode 100644 examples/tauri/tsconfig.json create mode 100644 examples/tauri/vite.config.ts create mode 100755 tools/release/local_registry_publish.py diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..fbe96bc8 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,21 @@ +# Oliphaunt Examples + +These examples keep the same todo schema across desktop shells: + +- `tauri`: Tauri v2 with the native Rust SDK. +- `tauri-wasix`: Tauri v2 with `oliphaunt-wasix` and SQLx. +- `electron`: Electron with the TypeScript SDK and native broker mode. +- `electron-wasix`: Electron with a Rust WASIX sidecar exposing a PostgreSQL URL. + +Each app opts into `hstore`, `pg_trgm`, and `unaccent`, then uses `hstore` +tags plus trigram/accent-insensitive search for the todo list. + +Local registry artifacts from CI run `28049923289` can be staged with: + +```sh +python3 tools/release/local_registry_publish.py download --run-id 28049923289 --preset local-publish +python3 tools/release/local_registry_publish.py publish +``` + +On Linux, SwiftPM artifacts are staged for inspection and skipped for registry +publish when `swift` is not installed. diff --git a/examples/electron-wasix/.gitignore b/examples/electron-wasix/.gitignore new file mode 100644 index 00000000..4144fc3b --- /dev/null +++ b/examples/electron-wasix/.gitignore @@ -0,0 +1,3 @@ +dist +node_modules +src-wasix/target diff --git a/examples/electron-wasix/README.md b/examples/electron-wasix/README.md new file mode 100644 index 00000000..46a65d53 --- /dev/null +++ b/examples/electron-wasix/README.md @@ -0,0 +1,14 @@ +# Electron WASIX Todo + +Electron keeps WASIX in a Rust sidecar. The sidecar starts +`OliphauntServer`, prints a local PostgreSQL URL, and stays alive until +Electron exits. The Electron main process uses `pg` with a single connection +and exposes the same preload API as the native Electron example. + +```sh +pnpm --dir examples/electron-wasix install +pnpm --dir examples/electron-wasix start +``` + +For packaged apps, build the `src-wasix` binary and set +`OLIPHAUNT_WASIX_TODO_SIDECAR` to its path before launching Electron. diff --git a/examples/electron-wasix/index.html b/examples/electron-wasix/index.html new file mode 100644 index 00000000..45e18bb2 --- /dev/null +++ b/examples/electron-wasix/index.html @@ -0,0 +1,68 @@ + + + + + + + Oliphaunt Electron WASIX Todo + + + +
+
+
+

Electron / WASIX sidecar / pg

+

Oliphaunt Todo

+
+ Ready +
+ +
+ + +
+ + + + +
+
+ +
+ +
+ + + +
+
+ +
+ 0 open + 0 done + 0 high priority +
+ +
+
+ + diff --git a/examples/electron-wasix/package.json b/examples/electron-wasix/package.json new file mode 100644 index 00000000..99e3905c --- /dev/null +++ b/examples/electron-wasix/package.json @@ -0,0 +1,21 @@ +{ + "name": "oliphaunt-example-electron-wasix", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "build": "tsc -p tsconfig.main.json && vite build", + "start": "pnpm run build && electron dist/main/main-process.js", + "dev:renderer": "vite" + }, + "dependencies": { + "pg": "^8.16.3" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "@types/pg": "^8.15.6", + "electron": "^39.2.5", + "typescript": "catalog:", + "vite": "^6.0.3" + } +} diff --git a/examples/electron-wasix/src-wasix/Cargo.lock b/examples/electron-wasix/src-wasix/Cargo.lock new file mode 100644 index 00000000..85e9f3c4 --- /dev/null +++ b/examples/electron-wasix/src-wasix/Cargo.lock @@ -0,0 +1,4026 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli 0.32.3", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "any_ascii" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70033777eb8b5124a81a1889416543dddef2de240019b674c81285a2635a7e1e" + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object 0.37.3", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.13.0", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn 2.0.118", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bus" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b7118d0221d84fada881b657c2ddb7cd55108db79c8764c9ee212c0c259b783" +dependencies = [ + "crossbeam-channel", + "num_cpus", + "parking_lot_core", +] + +[[package]] +name = "bytecheck" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0caa33a2c0edca0419d15ac723dff03f1956f7978329b1e3b5fdaaaed9d3ca8b" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "rancor", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +dependencies = [ + "serde", +] + +[[package]] +name = "bytesize" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e78e506b9d7633710dab98996f22f95f3d0f488e8f1aa162830556ed9fc14d" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cc" +version = "1.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex 2.0.1", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cooked-waker" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147be55d677052dabc6b22252d5dd0fd4c29c8c27aa4f2fbef0f94aa003b406f" + +[[package]] +name = "corosensei" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6886a0c0f263965933c438626e7179139a62b978a33aa18281cbf0cd5a975f34" +dependencies = [ + "autocfg", + "cfg-if", + "libc", + "scopeguard", + "windows-sys 0.59.0", +] + +[[package]] +name = "cpp_demangle" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.118", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + +[[package]] +name = "defmt" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" +dependencies = [ + "defmt 1.1.0", +] + +[[package]] +name = "defmt" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6e524506490a1953d237cb87b1cfc1e46f88c18f10a22dfe0f507dc6bfc7f7f" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0a27770e9c8f719a79d8b638281f4d828f77d8fd61e0bd94451b9b85e576a0b" +dependencies = [ + "defmt-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.118", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.118", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", +] + +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "enum-iterator" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "enumset" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "839c4174b41e75c8f7306110b2c51996a293b8d1d850edd529011841d9fede7d" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd536557b58c682b217b8fb199afdff47cd3eff260623f19e77074eb073d63a" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "escape8259" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5692dd7b5a1978a5aeb0ce83b7655c58ca8efdcb79d21036ea249da95afec2c6" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash 0.2.0", +] + +[[package]] +name = "heapless" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ignore" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b915661dd01db3f05050265b2477bcc6527b3792388e2749b41623cc592be67d" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "insta" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82" +dependencies = [ + "console", + "once_cell", + "regex", + "serde", + "similar", + "strip-ansi-escapes", + "tempfile", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iprange" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37209be0ad225457e63814401415e748e2453a5297f9b637338f5fb8afa4ec00" +dependencies = [ + "ipnet", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "leb128" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "lexical-sort" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c09e4591611e231daf4d4c685a66cb0410cc1e502027a20ae55f2bb9e997207a" +dependencies = [ + "any_ascii", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "libc", +] + +[[package]] +name = "libtest-mimic" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e6ba06f0ade6e504aff834d7c34298e5155c6baca353cc6a4aaff2f9fd7f33" +dependencies = [ + "anstream", + "anstyle", + "clap", + "escape8259", +] + +[[package]] +name = "libunwind" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6639b70a7ce854b79c70d7e83f16b5dc0137cc914f3d7d03803b513ecc67ac" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linked_hash_set" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "984fb35d06508d1e69fc91050cceba9c0b748f983e6739fa2c7a9237154c52c8" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lz4_flex" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e" +dependencies = [ + "twox-hash", +] + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + +[[package]] +name = "macho-unwind-info" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4bdc8b0ce69932332cf76d24af69c3a155242af95c226b2ab6c2e371ed1149" +dependencies = [ + "thiserror", + "zerocopy", + "zerocopy-derive", +] + +[[package]] +name = "managed" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "memmap2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d28bba84adfe6646737845bc5ebbfa2c08424eb1c37e94a1fd2a82adb56a872" +dependencies = [ + "libc", +] + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "more-asserts" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fafa6961cabd9c63bcd77a45d7e3b7f3b552b70417831fb0f56db717e72407e" + +[[package]] +name = "msvc-demangler" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeff6bd154a309b2ada5639b2661ca6ae4599b34e8487dc276d2cd637da2d76" +dependencies = [ + "bitflags 2.13.0", + "itoa", +] + +[[package]] +name = "munge" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c" +dependencies = [ + "munge_macro", +] + +[[package]] +name = "munge_macro" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "nom" +version = "5.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b" +dependencies = [ + "memchr", + "version_check", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "object" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "crc32fast", + "flate2", + "hashbrown 0.17.1", + "indexmap", + "memchr", + "ruzstd", +] + +[[package]] +name = "oliphaunt-electron-wasix-sidecar" +version = "0.1.0" +dependencies = [ + "anyhow", + "oliphaunt-wasix", + "serde_json", +] + +[[package]] +name = "oliphaunt-wasix" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "directories", + "dunce", + "filetime", + "flate2", + "hex", + "oliphaunt-wasix-aot-aarch64-apple-darwin", + "oliphaunt-wasix-aot-aarch64-unknown-linux-gnu", + "oliphaunt-wasix-aot-x86_64-pc-windows-msvc", + "oliphaunt-wasix-aot-x86_64-unknown-linux-gnu", + "oliphaunt-wasix-assets", + "regex", + "serde", + "serde_json", + "sha2 0.10.9", + "tar", + "tempfile", + "tokio", + "tracing", + "wasmer", + "wasmer-config", + "wasmer-types", + "wasmer-wasix", + "webc", + "zstd", +] + +[[package]] +name = "oliphaunt-wasix-aot-aarch64-apple-darwin" +version = "0.1.0" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "oliphaunt-wasix-aot-aarch64-unknown-linux-gnu" +version = "0.1.0" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "oliphaunt-wasix-aot-x86_64-pc-windows-msvc" +version = "0.1.0" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "oliphaunt-wasix-aot-x86_64-unknown-linux-gnu" +version = "0.1.0" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "oliphaunt-wasix-assets" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "path-clean" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", + "serde", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.118", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "ptr_meta" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "pulldown-cmark" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8" +dependencies = [ + "bitflags 1.3.2", + "memchr", + "unicase", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rancor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a063ea72381527c2a0561da9c80000ef822bdd7c3241b1cc1b12100e3df081ee" +dependencies = [ + "ptr_meta", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rangemap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "region" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b6ebd13bc009aef9cd476c1310d49ac354d36e240cf1bd753290f3dc7199a7" +dependencies = [ + "bitflags 1.3.2", + "libc", + "mach2 0.4.3", + "windows-sys 0.52.0", +] + +[[package]] +name = "rend" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "replace_with" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51743d3e274e2b18df81c4dc6caf8a5b8e15dbe799e0dca05c7617380094e884" + +[[package]] +name = "rkyv" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73389e0c99e664f919275ab5b5b0471391fe9a8de61e1dff9b1eaf56a90f16e3" +dependencies = [ + "bytecheck", + "bytes", + "hashbrown 0.17.1", + "indexmap", + "munge", + "ptr_meta", + "rancor", + "rend", + "rkyv_derive", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d2ed0b54125315fb36bd021e82d314d1c126548f871634b483f46b31d13cac6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty_pool" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed36cdb20de66d89a17ea04b8883fc7a386f2cf877aaedca5005583ce4876ff" +dependencies = [ + "crossbeam-channel", + "futures", + "futures-channel", + "futures-executor", + "num_cpus", +] + +[[package]] +name = "ruzstd" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7c1c839d570d835527c9a5e4db7cb2198683a988cb9d7293fc8674e6bd58fc8" +dependencies = [ + "twox-hash", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "saffron" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03fb9a628596fc7590eb7edbf7b0613287be78df107f5f97b118aad59fb2eea9" +dependencies = [ + "chrono", + "nom 5.1.3", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "indexmap", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", + "url", +] + +[[package]] +name = "schemars_derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.118", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "shared-buffer" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6c99835bad52957e7aa241d3975ed17c1e5f8c92026377d117a606f36b84b16" +dependencies = [ + "bytes", + "memmap2 0.6.2", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "smoltcp" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f73d40463bba65efc9adc6370b56df76d563cc46e2482bba58351b4afb7535e" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "cfg-if", + "defmt 0.3.100", + "heapless", + "managed", +] + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "symbolic-common" +version = "13.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1acef24ab2c9b307824e99ee81544a7fd5eac70b29898013580c2ab68e22104b" +dependencies = [ + "debugid", + "memmap2 0.9.11", + "stable_deref_trait", + "uuid", +] + +[[package]] +name = "symbolic-demangle" +version = "13.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eafb9860981a3611afed2ffadf834dabc8e7921ae9e6fe941ffee8d8d206888f" +dependencies = [ + "cpp_demangle", + "msvc-demangler", + "rustc-demangle", + "symbolic-common", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "terminal_size" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" +dependencies = [ + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "time" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "virtual-fs" +version = "0.702.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e66c1686d8c304c6136cb1a553cbc16c92261af8f34be365af8400b0ce82f94" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "dashmap", + "derive_more", + "dunce", + "futures", + "getrandom 0.4.3", + "indexmap", + "pin-project-lite", + "replace_with", + "shared-buffer", + "slab", + "thiserror", + "tokio", + "tracing", + "virtual-mio", + "wasmer-package", + "webc", +] + +[[package]] +name = "virtual-mio" +version = "0.702.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f86b519f58e30beca3845b5da865ebb7ea29c59b8d6b625ef8982ef1af93337" +dependencies = [ + "async-trait", + "bytes", + "futures", + "mio", + "parking", + "serde", + "socket2", + "thiserror", + "tracing", +] + +[[package]] +name = "virtual-net" +version = "0.702.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac308570c4756033af92f1b8680f0f84b82df526d25575c2136cde7bbbd838d6" +dependencies = [ + "anyhow", + "async-trait", + "base64", + "bincode", + "bytecheck", + "bytes", + "derive_more", + "futures-util", + "ipnet", + "iprange", + "libc", + "mio", + "pin-project-lite", + "rkyv", + "serde", + "smoltcp", + "socket2", + "thiserror", + "tokio", + "tracing", + "virtual-mio", +] + +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + +[[package]] +name = "wai-bindgen-gen-core" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa3dc41b510811122b3088197234c27e08fcad63ef936306dd8e11e2803876c" +dependencies = [ + "anyhow", + "wai-parser", +] + +[[package]] +name = "wai-bindgen-gen-rust" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19bc05e8380515c4337c40ef03b2ff233e391315b178a320de8640703d522efe" +dependencies = [ + "heck 0.3.3", + "wai-bindgen-gen-core", +] + +[[package]] +name = "wai-bindgen-gen-rust-wasm" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f35ce5e74086fac87f3a7bd50f643f00fe3559adb75c88521ecaa01c8a6199" +dependencies = [ + "heck 0.3.3", + "wai-bindgen-gen-core", + "wai-bindgen-gen-rust", +] + +[[package]] +name = "wai-bindgen-rust" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e5601c6f448c063e83a5e931b8fefcdf7e01ada424ad42372c948d2e3d67741" +dependencies = [ + "bitflags 1.3.2", + "wai-bindgen-rust-impl", +] + +[[package]] +name = "wai-bindgen-rust-impl" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdeeb5c1170246de8425a3e123e7ef260dc05ba2b522a1d369fe2315376efea4" +dependencies = [ + "proc-macro2", + "syn 1.0.109", + "wai-bindgen-gen-core", + "wai-bindgen-gen-rust-wasm", +] + +[[package]] +name = "wai-parser" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd0acb6d70885ea0c343749019ba74f015f64a9d30542e66db69b49b7e28186" +dependencies = [ + "anyhow", + "id-arena", + "pulldown-cmark", + "unicode-normalization", + "unicode-xid", +] + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.118", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.250.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2271adb766023046af314460f1fae02cc34ea16d736d93404d3b65be44270923" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasmer" +version = "7.2.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "596add954aa5e3937e889839c63250fc72340ccdb0cb9adcb89f026535300f73" +dependencies = [ + "bindgen", + "bytes", + "cfg-if", + "cmake", + "corosensei", + "dashmap", + "derive_more", + "futures", + "indexmap", + "itertools 0.14.0", + "js-sys", + "more-asserts", + "paste", + "rkyv", + "serde", + "serde-wasm-bindgen", + "shared-buffer", + "symbolic-demangle", + "tar", + "target-lexicon", + "thiserror", + "tracing", + "wasm-bindgen", + "wasmer-compiler", + "wasmer-derive", + "wasmer-types", + "wasmer-vm", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmer-compiler" +version = "7.2.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c15b69f6d74316e1a8366911bd04d9bab1115a8712c1fb4323d37624382d84c" +dependencies = [ + "backtrace", + "bytes", + "cfg-if", + "crossbeam-channel", + "enum-iterator", + "enumset", + "itertools 0.14.0", + "leb128", + "libc", + "macho-unwind-info", + "memmap2 0.9.11", + "more-asserts", + "object 0.39.1", + "rangemap", + "rayon", + "region", + "rkyv", + "self_cell", + "shared-buffer", + "smallvec", + "target-lexicon", + "tempfile", + "thiserror", + "wasmer-types", + "wasmer-vm", + "wasmparser", + "which", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmer-config" +version = "0.702.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcff14aae6b37c51f0bdc6e73736df7b978dd0515659e5fc6db3afb74ffe323f" +dependencies = [ + "anyhow", + "bytesize", + "ciborium", + "derive_builder", + "hex", + "indexmap", + "saffron", + "schemars", + "semver", + "serde", + "serde_json", + "serde_yaml", + "thiserror", + "toml", + "url", +] + +[[package]] +name = "wasmer-derive" +version = "7.2.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349030f566b3fe9ef09bf4abf4b917968a937f403a5e208740aa4c88e87928e5" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "wasmer-journal" +version = "0.702.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5863066574694ff8df6cf316416e89b7d4f0c7bca866facdfd4d8369b335fa55" +dependencies = [ + "anyhow", + "async-trait", + "base64", + "bincode", + "bytecheck", + "bytes", + "derive_more", + "lz4_flex", + "num_enum", + "rkyv", + "serde", + "serde_json", + "thiserror", + "tracing", + "virtual-fs", + "virtual-net", + "wasmer", + "wasmer-config", + "wasmer-wasix-types", +] + +[[package]] +name = "wasmer-package" +version = "0.702.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b786ad94623fa6612d4ed85e2603590797544ecd4ac5f8d414bebe677920cd5" +dependencies = [ + "anyhow", + "bytes", + "cfg-if", + "ciborium", + "flate2", + "ignore", + "insta", + "libc", + "semver", + "serde", + "serde_json", + "sha2 0.11.0", + "shared-buffer", + "tar", + "tempfile", + "thiserror", + "toml", + "url", + "wasmer-config", + "wasmer-types", + "webc", +] + +[[package]] +name = "wasmer-types" +version = "7.2.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aaf2baad42ce3f3ebc4508fbe8bb362fe31c08bae9048646842affd4868812d" +dependencies = [ + "bytecheck", + "crc32fast", + "enum-iterator", + "enumset", + "getrandom 0.4.3", + "hex", + "indexmap", + "itertools 0.14.0", + "more-asserts", + "rkyv", + "serde", + "sha2 0.11.0", + "target-lexicon", + "thiserror", + "wasmparser", +] + +[[package]] +name = "wasmer-vm" +version = "7.2.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54214dc7f3bc7c0f19eb31ac7d10796f30314a6fb3666004f4b11798646dd6e4" +dependencies = [ + "backtrace", + "bytesize", + "cc", + "cfg-if", + "corosensei", + "crossbeam-queue", + "dashmap", + "enum-iterator", + "fnv", + "gimli 0.33.0", + "indexmap", + "itertools 0.14.0", + "libc", + "libunwind", + "mach2 0.6.0", + "memoffset", + "more-asserts", + "parking_lot", + "region", + "rustversion", + "scopeguard", + "thiserror", + "wasmer-types", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmer-wasix" +version = "0.702.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6cfbfb4636accd684b014841965d19674b75b8ae8446e9327ef04f7a7e9ae9" +dependencies = [ + "anyhow", + "async-trait", + "base64", + "bincode", + "blake3", + "bus", + "bytecheck", + "bytes", + "cfg-if", + "cooked-waker", + "crossbeam-channel", + "dashmap", + "derive_more", + "flate2", + "fnv", + "fs_extra", + "futures", + "getrandom 0.3.4", + "getrandom 0.4.3", + "heapless", + "hex", + "http", + "itertools 0.14.0", + "libc", + "libtest-mimic", + "linked_hash_set", + "lz4_flex", + "num_enum", + "once_cell", + "petgraph", + "pin-project", + "pin-utils", + "rand 0.10.1", + "rkyv", + "rusty_pool", + "semver", + "serde", + "serde_derive", + "serde_json", + "serde_yaml", + "sha2 0.11.0", + "shared-buffer", + "tempfile", + "terminal_size", + "termios", + "thiserror", + "tokio", + "tokio-stream", + "toml", + "tracing", + "url", + "urlencoding", + "virtual-fs", + "virtual-mio", + "virtual-net", + "waker-fn", + "walkdir", + "wasm-encoder", + "wasmer", + "wasmer-config", + "wasmer-journal", + "wasmer-package", + "wasmer-types", + "wasmer-wasix-types", + "wasmparser", + "webc", + "weezl", + "windows-sys 0.61.2", + "xxhash-rust", + "zstd", +] + +[[package]] +name = "wasmer-wasix-types" +version = "0.702.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e823d48c54f97a6663844c2fd52dad4894da08fc930bcb930b93799b5d9606" +dependencies = [ + "anyhow", + "bitflags 2.13.0", + "byteorder", + "cfg-if", + "num_enum", + "serde", + "time", + "tracing", + "wai-bindgen-gen-core", + "wai-bindgen-gen-rust", + "wai-bindgen-gen-rust-wasm", + "wai-bindgen-rust", + "wai-parser", + "wasmer", + "wasmer-derive", + "wasmer-types", +] + +[[package]] +name = "wasmparser" +version = "0.250.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071d99cdfb8111603ed05500506c3298a940b58d609dd0259d3981785dd33556" +dependencies = [ + "bitflags 2.13.0", + "indexmap", +] + +[[package]] +name = "webc" +version = "12.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cb48ee4bc7a902c0f1d9eb0c0656f0e78149f1190b7f78e1f28256e88279a84" +dependencies = [ + "anyhow", + "base64", + "bytes", + "cfg-if", + "ciborium", + "document-features", + "ignore", + "indexmap", + "leb128", + "lexical-sort", + "libc", + "once_cell", + "path-clean", + "rand 0.9.4", + "serde", + "serde_json", + "sha2 0.10.9", + "shared-buffer", + "thiserror", + "url", +] + +[[package]] +name = "weezl" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ca08e5ef825b65b056d9efbd95c8750683f0a6d0466d02e96dc2e4e360f3d2" + +[[package]] +name = "which" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d7cd18d4acb58fb3cdfe9ea54e6cd96a4e7d4cc45c56338b236e82dad47248" +dependencies = [ + "libc", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/examples/electron-wasix/src-wasix/Cargo.toml b/examples/electron-wasix/src-wasix/Cargo.toml new file mode 100644 index 00000000..73d291bd --- /dev/null +++ b/examples/electron-wasix/src-wasix/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "oliphaunt-electron-wasix-sidecar" +version = "0.1.0" +edition = "2021" +publish = false + +[workspace] + +[dependencies] +anyhow = "1" +oliphaunt-wasix = { path = "../../../src/bindings/wasix-rust/crates/oliphaunt-wasix", features = ["extensions"] } +serde_json = "1" diff --git a/examples/electron-wasix/src-wasix/src/main.rs b/examples/electron-wasix/src-wasix/src/main.rs new file mode 100644 index 00000000..632cb4e6 --- /dev/null +++ b/examples/electron-wasix/src-wasix/src/main.rs @@ -0,0 +1,34 @@ +use std::env; +use std::io::{self, Write}; +use std::path::PathBuf; +use std::thread; + +use anyhow::{Context, Result, bail}; +use oliphaunt_wasix::{extensions, OliphauntServer}; +use serde_json::json; + +fn main() -> Result<()> { + let root = parse_root()?; + let server = OliphauntServer::builder() + .path(root) + .extensions([extensions::HSTORE, extensions::PG_TRGM, extensions::UNACCENT]) + .start() + .context("start oliphaunt-wasix server")?; + println!("{}", json!({ "databaseUrl": server.connection_uri() })); + io::stdout().flush()?; + let _server = server; + loop { + thread::park(); + } +} + +fn parse_root() -> Result { + let mut args = env::args().skip(1); + while let Some(arg) = args.next() { + if arg == "--root" { + let value = args.next().context("--root requires a path")?; + return Ok(PathBuf::from(value)); + } + } + bail!("usage: oliphaunt-electron-wasix-sidecar --root ") +} diff --git a/examples/electron-wasix/src/main-process.ts b/examples/electron-wasix/src/main-process.ts new file mode 100644 index 00000000..05cd13d9 --- /dev/null +++ b/examples/electron-wasix/src/main-process.ts @@ -0,0 +1,56 @@ +import { app, BrowserWindow, ipcMain } from "electron"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { closeStore, createTodo, deleteTodo, listTodos, toggleTodo } from "./todos.js"; +import type { CreateTodoInput, StatusFilter } from "./types.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +function createWindow() { + const window = new BrowserWindow({ + width: 1100, + height: 760, + title: "Oliphaunt Electron WASIX Todo", + webPreferences: { + preload: join(__dirname, "preload.js"), + contextIsolation: true, + nodeIntegration: false, + }, + }); + + const devServer = process.env.VITE_DEV_SERVER_URL; + if (devServer) { + void window.loadURL(devServer); + } else { + void window.loadFile(join(__dirname, "../renderer/index.html")); + } +} + +ipcMain.handle( + "todos:list", + (_event, filter: { search: string; status: StatusFilter }) => listTodos(app.getPath("userData"), filter), +); +ipcMain.handle("todos:create", (_event, input: CreateTodoInput) => + createTodo(app.getPath("userData"), input), +); +ipcMain.handle("todos:toggle", (_event, id: number) => toggleTodo(app.getPath("userData"), id)); +ipcMain.handle("todos:delete", (_event, id: number) => deleteTodo(app.getPath("userData"), id)); + +await app.whenReady(); +createWindow(); + +app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) createWindow(); +}); + +app.on("window-all-closed", () => { + if (process.platform !== "darwin") app.quit(); +}); + +app.on("before-quit", (event) => { + event.preventDefault(); + closeStore() + .catch((error) => console.error(error)) + .finally(() => app.exit(0)); +}); diff --git a/examples/electron-wasix/src/preload.ts b/examples/electron-wasix/src/preload.ts new file mode 100644 index 00000000..0cebe053 --- /dev/null +++ b/examples/electron-wasix/src/preload.ts @@ -0,0 +1,19 @@ +import { contextBridge, ipcRenderer } from "electron"; +import type { CreateTodoInput, StatusFilter, TodoApi } from "./types.js"; + +const api: TodoApi = { + listTodos(filter: { search: string; status: StatusFilter }) { + return ipcRenderer.invoke("todos:list", filter); + }, + createTodo(input: CreateTodoInput) { + return ipcRenderer.invoke("todos:create", input); + }, + toggleTodo(id: number) { + return ipcRenderer.invoke("todos:toggle", id); + }, + deleteTodo(id: number) { + return ipcRenderer.invoke("todos:delete", id); + }, +}; + +contextBridge.exposeInMainWorld("todos", api); diff --git a/examples/electron-wasix/src/renderer.ts b/examples/electron-wasix/src/renderer.ts new file mode 100644 index 00000000..2dd749fc --- /dev/null +++ b/examples/electron-wasix/src/renderer.ts @@ -0,0 +1 @@ +import "../../electron/src/renderer.ts"; diff --git a/examples/electron-wasix/src/sidecar.ts b/examples/electron-wasix/src/sidecar.ts new file mode 100644 index 00000000..0e58499e --- /dev/null +++ b/examples/electron-wasix/src/sidecar.ts @@ -0,0 +1,55 @@ +import { spawn, type ChildProcess } from "node:child_process"; +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import { createInterface } from "node:readline"; + +export type WasixSidecar = { + databaseUrl: string; + process: ChildProcess; +}; + +export async function startWasixSidecar(root: string): Promise { + const configured = process.env.OLIPHAUNT_WASIX_TODO_SIDECAR; + const command = configured || "cargo"; + const args = configured + ? ["--root", root] + : [ + "run", + "--quiet", + "--manifest-path", + join(process.cwd(), "src-wasix/Cargo.toml"), + "--", + "--root", + root, + ]; + if (configured && !existsSync(configured)) { + throw new Error(`OLIPHAUNT_WASIX_TODO_SIDECAR does not exist: ${configured}`); + } + + const child = spawn(command, args, { + cwd: process.cwd(), + stdio: ["ignore", "pipe", "pipe"], + }); + child.stderr.on("data", (chunk) => { + process.stderr.write(chunk); + }); + + const lines = createInterface({ input: child.stdout }); + const firstLine = await new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error("timed out waiting for WASIX sidecar")), 60_000); + child.once("exit", (code) => { + clearTimeout(timer); + reject(new Error(`WASIX sidecar exited before ready: ${code ?? "signal"}`)); + }); + lines.once("line", (line) => { + clearTimeout(timer); + resolve(line); + }); + }); + const payload = JSON.parse(firstLine) as { databaseUrl?: string }; + if (!payload.databaseUrl) throw new Error("WASIX sidecar did not print databaseUrl"); + return { + databaseUrl: payload.databaseUrl, + process: child, + }; +} diff --git a/examples/electron-wasix/src/styles.css b/examples/electron-wasix/src/styles.css new file mode 100644 index 00000000..1c8454f3 --- /dev/null +++ b/examples/electron-wasix/src/styles.css @@ -0,0 +1 @@ +@import "../../tauri/src/styles.css"; diff --git a/examples/electron-wasix/src/todos.ts b/examples/electron-wasix/src/todos.ts new file mode 100644 index 00000000..181c4170 --- /dev/null +++ b/examples/electron-wasix/src/todos.ts @@ -0,0 +1,153 @@ +import { join } from "node:path"; + +import pg from "pg"; +import type { CreateTodoInput, StatusFilter, Todo } from "./types.js"; +import { startWasixSidecar, type WasixSidecar } from "./sidecar.js"; + +const { Pool } = pg; + +const schemaStatements = [ + "CREATE EXTENSION IF NOT EXISTS hstore", + "CREATE EXTENSION IF NOT EXISTS pg_trgm", + "CREATE EXTENSION IF NOT EXISTS unaccent", + `CREATE TABLE IF NOT EXISTS todos ( + id bigserial PRIMARY KEY, + title text NOT NULL, + notes text NOT NULL DEFAULT '', + tags hstore NOT NULL DEFAULT ''::hstore, + done boolean NOT NULL DEFAULT false, + priority integer NOT NULL DEFAULT 2 CHECK (priority BETWEEN 1 AND 3), + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() + )`, + "CREATE INDEX IF NOT EXISTS todos_title_trgm ON todos USING gin (title gin_trgm_ops)", +]; + +const selectTodos = ` +SELECT + id, + title, + notes, + COALESCE(tags -> 'area', '') AS area, + COALESCE(tags -> 'context', '') AS context, + done, + priority, + to_char(created_at, 'YYYY-MM-DD HH24:MI') AS created_at, + to_char(updated_at, 'YYYY-MM-DD HH24:MI') AS updated_at +FROM todos +WHERE + ( + $1::text = '' + OR unaccent(title || ' ' || notes) ILIKE '%' || unaccent($1::text) || '%' + OR COALESCE(tags -> 'area', '') ILIKE '%' || $1::text || '%' + OR COALESCE(tags -> 'context', '') ILIKE '%' || $1::text || '%' + OR tags ? $1::text + ) + AND ( + $2::text = 'all' + OR ($2::text = 'open' AND NOT done) + OR ($2::text = 'done' AND done) + ) +ORDER BY done ASC, priority ASC, updated_at DESC, id DESC +`; + +const returningTodo = ` +RETURNING + id, + title, + notes, + COALESCE(tags -> 'area', '') AS area, + COALESCE(tags -> 'context', '') AS context, + done, + priority, + to_char(created_at, 'YYYY-MM-DD HH24:MI') AS created_at, + to_char(updated_at, 'YYYY-MM-DD HH24:MI') AS updated_at +`; + +type Store = { + pool: pg.Pool; + sidecar: WasixSidecar; +}; + +let storePromise: Promise | undefined; + +async function getStore(userData: string) { + storePromise ??= openStore(userData); + return storePromise; +} + +async function openStore(userData: string): Promise { + const sidecar = await startWasixSidecar(join(userData, "oliphaunt-wasix-todos")); + const pool = new Pool({ + connectionString: sidecar.databaseUrl, + max: 1, + }); + for (const statement of schemaStatements) { + await pool.query(statement); + } + return { pool, sidecar }; +} + +export async function listTodos( + userData: string, + filter: { search: string; status: StatusFilter }, +) { + const { pool } = await getStore(userData); + const result = await pool.query(selectTodos, [filter.search, filter.status]); + return result.rows.map(todoFromRow); +} + +export async function createTodo(userData: string, input: CreateTodoInput) { + const { pool } = await getStore(userData); + const result = await pool.query( + `INSERT INTO todos (title, notes, tags, priority) + VALUES ($1, $2, hstore(ARRAY['area', $3, 'context', $4]), $5) + ${returningTodo}`, + [input.title, input.notes, input.area, input.context, clampPriority(input.priority)], + ); + return oneTodo(result.rows); +} + +export async function toggleTodo(userData: string, id: number) { + const { pool } = await getStore(userData); + const result = await pool.query( + `UPDATE todos SET done = NOT done, updated_at = now() WHERE id = $1 ${returningTodo}`, + [id], + ); + return oneTodo(result.rows); +} + +export async function deleteTodo(userData: string, id: number) { + const { pool } = await getStore(userData); + await pool.query("DELETE FROM todos WHERE id = $1", [id]); +} + +export async function closeStore() { + if (!storePromise) return; + const store = await storePromise; + await store.pool.end(); + store.sidecar.process.kill(); +} + +function oneTodo(rows: unknown[]) { + if (rows.length === 0) throw new Error("todo was not returned"); + return todoFromRow(rows[0] as pg.QueryResultRow); +} + +function todoFromRow(row: pg.QueryResultRow): Todo { + return { + id: Number(row.id), + title: String(row.title), + notes: String(row.notes), + area: String(row.area), + context: String(row.context), + priority: Number(row.priority), + done: Boolean(row.done), + createdAt: String(row.created_at), + updatedAt: String(row.updated_at), + }; +} + +function clampPriority(value: number) { + return Math.min(Math.max(Math.trunc(value) || 2, 1), 3); +} diff --git a/examples/electron-wasix/src/types.ts b/examples/electron-wasix/src/types.ts new file mode 100644 index 00000000..94e07d30 --- /dev/null +++ b/examples/electron-wasix/src/types.ts @@ -0,0 +1,28 @@ +export type Todo = { + id: number; + title: string; + notes: string; + area: string; + context: string; + priority: number; + done: boolean; + createdAt: string; + updatedAt: string; +}; + +export type CreateTodoInput = { + title: string; + notes: string; + area: string; + context: string; + priority: number; +}; + +export type StatusFilter = "open" | "all" | "done"; + +export type TodoApi = { + listTodos(filter: { search: string; status: StatusFilter }): Promise; + createTodo(input: CreateTodoInput): Promise; + toggleTodo(id: number): Promise; + deleteTodo(id: number): Promise; +}; diff --git a/examples/electron-wasix/tsconfig.main.json b/examples/electron-wasix/tsconfig.main.json new file mode 100644 index 00000000..42c05c32 --- /dev/null +++ b/examples/electron-wasix/tsconfig.main.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022", "DOM"], + "outDir": "dist/main", + "rootDir": "src", + "strict": true, + "skipLibCheck": true, + "sourceMap": true + }, + "include": ["src/main-process.ts", "src/preload.ts", "src/sidecar.ts", "src/todos.ts", "src/types.ts"] +} diff --git a/examples/electron-wasix/tsconfig.renderer.json b/examples/electron-wasix/tsconfig.renderer.json new file mode 100644 index 00000000..86f41c38 --- /dev/null +++ b/examples/electron-wasix/tsconfig.renderer.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "strict": true, + "skipLibCheck": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true + }, + "include": ["src/renderer.ts", "src/types.ts"] +} diff --git a/examples/electron-wasix/vite.config.ts b/examples/electron-wasix/vite.config.ts new file mode 100644 index 00000000..41b47a44 --- /dev/null +++ b/examples/electron-wasix/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + root: ".", + clearScreen: false, + server: { + port: 5175, + strictPort: true, + }, + build: { + outDir: "dist/renderer", + emptyOutDir: false, + }, +}); diff --git a/examples/electron/.gitignore b/examples/electron/.gitignore new file mode 100644 index 00000000..de4d1f00 --- /dev/null +++ b/examples/electron/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/examples/electron/README.md b/examples/electron/README.md new file mode 100644 index 00000000..def6e7ee --- /dev/null +++ b/examples/electron/README.md @@ -0,0 +1,10 @@ +# Electron Native Todo + +Electron owns the Oliphaunt TypeScript SDK in the main process and exposes a +small IPC surface to the renderer through preload. The app uses `nativeBroker` +mode with a persistent root under Electron's user data directory. + +```sh +pnpm --dir examples/electron install +pnpm --dir examples/electron start +``` diff --git a/examples/electron/index.html b/examples/electron/index.html new file mode 100644 index 00000000..dc1ad064 --- /dev/null +++ b/examples/electron/index.html @@ -0,0 +1,68 @@ + + + + + + + Oliphaunt Electron Todo + + + +
+
+
+

Electron / TypeScript SDK / native broker

+

Oliphaunt Todo

+
+ Ready +
+ +
+ + +
+ + + + +
+
+ +
+ +
+ + + +
+
+ +
+ 0 open + 0 done + 0 high priority +
+ +
+
+ + diff --git a/examples/electron/package.json b/examples/electron/package.json new file mode 100644 index 00000000..8aee4d13 --- /dev/null +++ b/examples/electron/package.json @@ -0,0 +1,21 @@ +{ + "name": "oliphaunt-example-electron", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "prebuild": "pnpm --dir ../../src/sdks/js run build", + "build": "tsc -p tsconfig.main.json && vite build", + "start": "pnpm run build && electron dist/main/main-process.js", + "dev:renderer": "vite" + }, + "dependencies": { + "@oliphaunt/ts": "workspace:*" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "electron": "^39.2.5", + "typescript": "catalog:", + "vite": "^6.0.3" + } +} diff --git a/examples/electron/src/main-process.ts b/examples/electron/src/main-process.ts new file mode 100644 index 00000000..5c1e9dc6 --- /dev/null +++ b/examples/electron/src/main-process.ts @@ -0,0 +1,56 @@ +import { app, BrowserWindow, ipcMain } from "electron"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { closeDatabase, createTodo, deleteTodo, listTodos, toggleTodo } from "./todos.js"; +import type { CreateTodoInput, StatusFilter } from "./types.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +function createWindow() { + const window = new BrowserWindow({ + width: 1100, + height: 760, + title: "Oliphaunt Electron Todo", + webPreferences: { + preload: join(__dirname, "preload.js"), + contextIsolation: true, + nodeIntegration: false, + }, + }); + + const devServer = process.env.VITE_DEV_SERVER_URL; + if (devServer) { + void window.loadURL(devServer); + } else { + void window.loadFile(join(__dirname, "../renderer/index.html")); + } +} + +ipcMain.handle( + "todos:list", + (_event, filter: { search: string; status: StatusFilter }) => listTodos(app.getPath("userData"), filter), +); +ipcMain.handle("todos:create", (_event, input: CreateTodoInput) => + createTodo(app.getPath("userData"), input), +); +ipcMain.handle("todos:toggle", (_event, id: number) => toggleTodo(app.getPath("userData"), id)); +ipcMain.handle("todos:delete", (_event, id: number) => deleteTodo(app.getPath("userData"), id)); + +await app.whenReady(); +createWindow(); + +app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) createWindow(); +}); + +app.on("window-all-closed", () => { + if (process.platform !== "darwin") app.quit(); +}); + +app.on("before-quit", (event) => { + event.preventDefault(); + closeDatabase() + .catch((error) => console.error(error)) + .finally(() => app.exit(0)); +}); diff --git a/examples/electron/src/preload.ts b/examples/electron/src/preload.ts new file mode 100644 index 00000000..0cebe053 --- /dev/null +++ b/examples/electron/src/preload.ts @@ -0,0 +1,19 @@ +import { contextBridge, ipcRenderer } from "electron"; +import type { CreateTodoInput, StatusFilter, TodoApi } from "./types.js"; + +const api: TodoApi = { + listTodos(filter: { search: string; status: StatusFilter }) { + return ipcRenderer.invoke("todos:list", filter); + }, + createTodo(input: CreateTodoInput) { + return ipcRenderer.invoke("todos:create", input); + }, + toggleTodo(id: number) { + return ipcRenderer.invoke("todos:toggle", id); + }, + deleteTodo(id: number) { + return ipcRenderer.invoke("todos:delete", id); + }, +}; + +contextBridge.exposeInMainWorld("todos", api); diff --git a/examples/electron/src/renderer.ts b/examples/electron/src/renderer.ts new file mode 100644 index 00000000..a38885b2 --- /dev/null +++ b/examples/electron/src/renderer.ts @@ -0,0 +1,135 @@ +import type { CreateTodoInput, StatusFilter, Todo, TodoApi } from "./types"; + +declare global { + interface Window { + todos: TodoApi; + } +} + +const form = document.querySelector("#todo-form"); +const list = document.querySelector("#todo-list"); +const status = document.querySelector("#status"); +const search = document.querySelector("#search"); +const openCount = document.querySelector("#open-count"); +const doneCount = document.querySelector("#done-count"); +const highCount = document.querySelector("#high-count"); +let activeStatus: StatusFilter = "open"; +let todos: Todo[] = []; + +async function listTodos() { + todos = await window.todos.listTodos({ + search: search?.value.trim() ?? "", + status: activeStatus, + }); + render(); +} + +function setStatus(message: string) { + if (status) status.value = message; +} + +function priorityLabel(priority: number) { + if (priority === 1) return "High"; + if (priority === 3) return "Low"; + return "Normal"; +} + +function render() { + const open = todos.filter((todo) => !todo.done).length; + const done = todos.filter((todo) => todo.done).length; + const high = todos.filter((todo) => !todo.done && todo.priority === 1).length; + if (openCount) openCount.value = `${open} open`; + if (doneCount) doneCount.value = `${done} done`; + if (highCount) highCount.value = `${high} high priority`; + if (!list) return; + if (todos.length === 0) { + const empty = document.createElement("p"); + empty.className = "empty"; + empty.textContent = "No todos match the current filter."; + list.replaceChildren(empty); + return; + } + list.replaceChildren(...todos.map(renderTodo)); +} + +function renderTodo(todo: Todo) { + const row = document.createElement("article"); + row.className = todo.done ? "todo done" : "todo"; + + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.checked = todo.done; + checkbox.addEventListener("change", () => { + void window.todos.toggleTodo(todo.id).then(listTodos).catch((error) => setStatus(String(error))); + }); + + const body = document.createElement("div"); + const title = document.createElement("h2"); + title.textContent = todo.title; + const notes = document.createElement("p"); + notes.textContent = todo.notes || "No notes"; + const meta = document.createElement("div"); + meta.className = "meta"; + for (const value of [ + priorityLabel(todo.priority), + todo.area ? `area:${todo.area}` : "", + todo.context ? `context:${todo.context}` : "", + `updated ${todo.updatedAt}`, + ]) { + if (!value) continue; + const pill = document.createElement("span"); + pill.className = "pill"; + pill.textContent = value; + meta.append(pill); + } + body.append(title, notes, meta); + + const remove = document.createElement("button"); + remove.className = "secondary"; + remove.type = "button"; + remove.textContent = "Delete"; + remove.addEventListener("click", () => { + void window.todos.deleteTodo(todo.id).then(listTodos).catch((error) => setStatus(String(error))); + }); + + row.append(checkbox, body, remove); + return row; +} + +form?.addEventListener("submit", (event) => { + event.preventDefault(); + const data = new FormData(form); + const input: CreateTodoInput = { + title: String(data.get("title") ?? "").trim(), + notes: String(data.get("notes") ?? "").trim(), + area: String(data.get("area") ?? "").trim(), + context: String(data.get("context") ?? "").trim(), + priority: Number(data.get("priority") ?? 2), + }; + if (!input.title) return; + setStatus("Saving"); + window.todos + .createTodo(input) + .then(() => { + form.reset(); + setStatus("Saved"); + return listTodos(); + }) + .catch((error) => setStatus(String(error))); +}); + +search?.addEventListener("input", () => { + void listTodos().catch((error) => setStatus(String(error))); +}); + +document.querySelectorAll("[data-status]").forEach((button) => { + button.addEventListener("click", () => { + activeStatus = button.dataset.status as StatusFilter; + document + .querySelectorAll("[data-status]") + .forEach((candidate) => candidate.classList.toggle("active", candidate === button)); + void listTodos().catch((error) => setStatus(String(error))); + }); +}); + +void listTodos().catch((error) => setStatus(String(error))); diff --git a/examples/electron/src/styles.css b/examples/electron/src/styles.css new file mode 100644 index 00000000..1c8454f3 --- /dev/null +++ b/examples/electron/src/styles.css @@ -0,0 +1 @@ +@import "../../tauri/src/styles.css"; diff --git a/examples/electron/src/todos.ts b/examples/electron/src/todos.ts new file mode 100644 index 00000000..462dbbd3 --- /dev/null +++ b/examples/electron/src/todos.ts @@ -0,0 +1,154 @@ +import { join } from "node:path"; + +import { Oliphaunt, type OliphauntDatabase, type QueryResult } from "@oliphaunt/ts"; +import type { CreateTodoInput, StatusFilter, Todo } from "./types.js"; + +const schemaStatements = [ + "CREATE EXTENSION IF NOT EXISTS hstore", + "CREATE EXTENSION IF NOT EXISTS pg_trgm", + "CREATE EXTENSION IF NOT EXISTS unaccent", + `CREATE TABLE IF NOT EXISTS todos ( + id bigserial PRIMARY KEY, + title text NOT NULL, + notes text NOT NULL DEFAULT '', + tags hstore NOT NULL DEFAULT ''::hstore, + done boolean NOT NULL DEFAULT false, + priority integer NOT NULL DEFAULT 2 CHECK (priority BETWEEN 1 AND 3), + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() + )`, + "CREATE INDEX IF NOT EXISTS todos_title_trgm ON todos USING gin (title gin_trgm_ops)", +]; + +const selectTodos = ` +SELECT + id::text AS id, + title, + notes, + COALESCE(tags -> 'area', '') AS area, + COALESCE(tags -> 'context', '') AS context, + done::text AS done, + priority::text AS priority, + to_char(created_at, 'YYYY-MM-DD HH24:MI') AS created_at, + to_char(updated_at, 'YYYY-MM-DD HH24:MI') AS updated_at +FROM todos +WHERE + ( + $1::text = '' + OR unaccent(title || ' ' || notes) ILIKE '%' || unaccent($1::text) || '%' + OR COALESCE(tags -> 'area', '') ILIKE '%' || $1::text || '%' + OR COALESCE(tags -> 'context', '') ILIKE '%' || $1::text || '%' + OR tags ? $1::text + ) + AND ( + $2::text = 'all' + OR ($2::text = 'open' AND NOT done) + OR ($2::text = 'done' AND done) + ) +ORDER BY done ASC, priority ASC, updated_at DESC, id DESC +`; + +const returningTodo = ` +RETURNING + id::text AS id, + title, + notes, + COALESCE(tags -> 'area', '') AS area, + COALESCE(tags -> 'context', '') AS context, + done::text AS done, + priority::text AS priority, + to_char(created_at, 'YYYY-MM-DD HH24:MI') AS created_at, + to_char(updated_at, 'YYYY-MM-DD HH24:MI') AS updated_at +`; + +let dbPromise: Promise | undefined; + +export function getDatabase(userData: string) { + dbPromise ??= openDatabase(userData); + return dbPromise; +} + +async function openDatabase(userData: string) { + const db = await Oliphaunt.open({ + engine: "nativeBroker", + root: join(userData, "oliphaunt-native-todos"), + extensions: ["hstore", "pg_trgm", "unaccent"], + }); + for (const statement of schemaStatements) { + await db.execute(statement); + } + return db; +} + +export async function listTodos( + userData: string, + filter: { search: string; status: StatusFilter }, +) { + const db = await getDatabase(userData); + const result = await db.query(selectTodos, [filter.search, filter.status]); + return todosFromResult(result); +} + +export async function createTodo(userData: string, input: CreateTodoInput) { + const db = await getDatabase(userData); + const result = await db.query( + `INSERT INTO todos (title, notes, tags, priority) + VALUES ($1, $2, hstore(ARRAY['area', $3, 'context', $4]), $5) + ${returningTodo}`, + [input.title, input.notes, input.area, input.context, clampPriority(input.priority)], + ); + return oneTodo(result); +} + +export async function toggleTodo(userData: string, id: number) { + const db = await getDatabase(userData); + const result = await db.query( + `UPDATE todos SET done = NOT done, updated_at = now() WHERE id = $1 ${returningTodo}`, + [id], + ); + return oneTodo(result); +} + +export async function deleteTodo(userData: string, id: number) { + const db = await getDatabase(userData); + await db.query("DELETE FROM todos WHERE id = $1", [id]); +} + +export async function closeDatabase() { + if (!dbPromise) return; + const db = await dbPromise; + await db.close(); +} + +function todosFromResult(result: QueryResult) { + return Array.from({ length: result.rowCount }, (_, index) => todoFromResult(result, index)); +} + +function oneTodo(result: QueryResult) { + if (result.rowCount === 0) throw new Error("todo was not returned"); + return todoFromResult(result, 0); +} + +function todoFromResult(result: QueryResult, row: number): Todo { + return { + id: Number(required(result, row, "id")), + title: required(result, row, "title"), + notes: required(result, row, "notes"), + area: required(result, row, "area"), + context: required(result, row, "context"), + priority: Number(required(result, row, "priority")), + done: required(result, row, "done") === "true", + createdAt: required(result, row, "created_at"), + updatedAt: required(result, row, "updated_at"), + }; +} + +function required(result: QueryResult, row: number, column: string) { + const value = result.getText(row, column); + if (value === null) throw new Error(`missing ${column}`); + return value; +} + +function clampPriority(value: number) { + return Math.min(Math.max(Math.trunc(value) || 2, 1), 3); +} diff --git a/examples/electron/src/types.ts b/examples/electron/src/types.ts new file mode 100644 index 00000000..94e07d30 --- /dev/null +++ b/examples/electron/src/types.ts @@ -0,0 +1,28 @@ +export type Todo = { + id: number; + title: string; + notes: string; + area: string; + context: string; + priority: number; + done: boolean; + createdAt: string; + updatedAt: string; +}; + +export type CreateTodoInput = { + title: string; + notes: string; + area: string; + context: string; + priority: number; +}; + +export type StatusFilter = "open" | "all" | "done"; + +export type TodoApi = { + listTodos(filter: { search: string; status: StatusFilter }): Promise; + createTodo(input: CreateTodoInput): Promise; + toggleTodo(id: number): Promise; + deleteTodo(id: number): Promise; +}; diff --git a/examples/electron/tsconfig.main.json b/examples/electron/tsconfig.main.json new file mode 100644 index 00000000..739fb30d --- /dev/null +++ b/examples/electron/tsconfig.main.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022", "DOM"], + "outDir": "dist/main", + "rootDir": "src", + "strict": true, + "skipLibCheck": true, + "sourceMap": true + }, + "include": ["src/main-process.ts", "src/preload.ts", "src/todos.ts", "src/types.ts"] +} diff --git a/examples/electron/tsconfig.renderer.json b/examples/electron/tsconfig.renderer.json new file mode 100644 index 00000000..86f41c38 --- /dev/null +++ b/examples/electron/tsconfig.renderer.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "strict": true, + "skipLibCheck": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true + }, + "include": ["src/renderer.ts", "src/types.ts"] +} diff --git a/examples/electron/vite.config.ts b/examples/electron/vite.config.ts new file mode 100644 index 00000000..d09839f1 --- /dev/null +++ b/examples/electron/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + root: ".", + clearScreen: false, + server: { + port: 5174, + strictPort: true, + }, + build: { + outDir: "dist/renderer", + emptyOutDir: false, + }, +}); diff --git a/examples/tauri-wasix/.gitignore b/examples/tauri-wasix/.gitignore new file mode 100644 index 00000000..433fc4bb --- /dev/null +++ b/examples/tauri-wasix/.gitignore @@ -0,0 +1,4 @@ +dist +node_modules +src-tauri/gen +src-tauri/target diff --git a/examples/tauri-wasix/README.md b/examples/tauri-wasix/README.md new file mode 100644 index 00000000..066a2d9b --- /dev/null +++ b/examples/tauri-wasix/README.md @@ -0,0 +1,10 @@ +# Tauri WASIX Todo + +Tauri owns a Rust backend that starts `OliphauntServer` from +`oliphaunt-wasix`, then uses a one-connection SQLx pool against the local +PostgreSQL URL. The webview receives app-specific commands only. + +```sh +pnpm --dir examples/tauri-wasix install +pnpm --dir examples/tauri-wasix tauri dev +``` diff --git a/examples/tauri-wasix/index.html b/examples/tauri-wasix/index.html new file mode 100644 index 00000000..045da9ec --- /dev/null +++ b/examples/tauri-wasix/index.html @@ -0,0 +1,68 @@ + + + + + + + Oliphaunt Tauri WASIX Todo + + + +
+
+
+

Tauri / WASIX / SQLx

+

Oliphaunt Todo

+
+ Ready +
+ +
+ + +
+ + + + +
+
+ +
+ +
+ + + +
+
+ +
+ 0 open + 0 done + 0 high priority +
+ +
+
+ + diff --git a/examples/tauri-wasix/package.json b/examples/tauri-wasix/package.json new file mode 100644 index 00000000..d513d048 --- /dev/null +++ b/examples/tauri-wasix/package.json @@ -0,0 +1,20 @@ +{ + "name": "oliphaunt-example-tauri-wasix", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "tauri": "tauri" + }, + "dependencies": { + "@tauri-apps/api": "^2" + }, + "devDependencies": { + "@tauri-apps/cli": "^2", + "typescript": "catalog:", + "vite": "^6.0.3" + } +} diff --git a/examples/tauri-wasix/src-tauri/Cargo.lock b/examples/tauri-wasix/src-tauri/Cargo.lock new file mode 100644 index 00000000..ce0beb90 --- /dev/null +++ b/examples/tauri-wasix/src-tauri/Cargo.lock @@ -0,0 +1,7351 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli 0.32.3", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "any_ascii" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70033777eb8b5124a81a1889416543dddef2de240019b674c81285a2635a7e1e" + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object 0.37.3", + "rustc-demangle", + "windows-link 0.2.1", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.13.0", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn 2.0.118", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] + +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bus" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b7118d0221d84fada881b657c2ddb7cd55108db79c8764c9ee212c0c259b783" +dependencies = [ + "crossbeam-channel", + "num_cpus", + "parking_lot_core", +] + +[[package]] +name = "bytecheck" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0caa33a2c0edca0419d15ac723dff03f1956f7978329b1e3b5fdaaaed9d3ca8b" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "rancor", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +dependencies = [ + "serde", +] + +[[package]] +name = "bytesize" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e78e506b9d7633710dab98996f22f95f3d0f488e8f1aa162830556ed9fc14d" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce8d3bd5823c7504d3f579f13e7b2f3da252fcb938c594d5680ee508bf846f" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex 2.0.1", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon 0.12.16", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading 0.8.9", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cooked-waker" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147be55d677052dabc6b22252d5dd0fd4c29c8c27aa4f2fbef0f94aa003b406f" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.0", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.0", + "core-foundation", + "libc", +] + +[[package]] +name = "corosensei" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6886a0c0f263965933c438626e7179139a62b978a33aa18281cbf0cd5a975f34" +dependencies = [ + "autocfg", + "cfg-if", + "libc", + "scopeguard", + "windows-sys 0.59.0", +] + +[[package]] +name = "cpp_demangle" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.118", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.118", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "dbus" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + +[[package]] +name = "defmt" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" +dependencies = [ + "defmt 1.1.0", +] + +[[package]] +name = "defmt" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6e524506490a1953d237cb87b1cfc1e46f88c18f10a22dfe0f507dc6bfc7f7f" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0a27770e9c8f719a79d8b638281f4d828f77d8fd61e0bd94451b9b85e576a0b" +dependencies = [ + "defmt-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.118", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.118", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", +] + +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash 0.2.0", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +dependencies = [ + "serde", +] + +[[package]] +name = "embed-resource" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "enum-iterator" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "enumset" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "839c4174b41e75c8f7306110b2c51996a293b8d1d850edd529011841d9fede7d" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd536557b58c682b217b8fb199afdff47cd3eff260623f19e77074eb073d63a" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "escape8259" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5692dd7b5a1978a5aeb0ce83b7655c58ca8efdcb79d21036ea249da95afec2c6" + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap 2.14.0", + "stable_deref_trait", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash 0.2.0", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heapless" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" +dependencies = [ + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ignore" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b915661dd01db3f05050265b2477bcc6527b3792388e2749b41623cc592be67d" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "insta" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82" +dependencies = [ + "console", + "once_cell", + "regex", + "serde", + "similar", + "strip-ansi-escapes", + "tempfile", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iprange" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37209be0ad225457e63814401415e748e2453a5297f9b637338f5fb8afa4ec00" +dependencies = [ + "ipnet", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "leb128" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "lexical-sort" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c09e4591611e231daf4d4c685a66cb0410cc1e502027a20ae55f2bb9e997207a" +dependencies = [ + "any_ascii", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading 0.7.4", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "bitflags 2.13.0", + "libc", + "plain", + "redox_syscall 0.8.1", +] + +[[package]] +name = "libtest-mimic" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e6ba06f0ade6e504aff834d7c34298e5155c6baca353cc6a4aaff2f9fd7f33" +dependencies = [ + "anstream", + "anstyle", + "clap", + "escape8259", +] + +[[package]] +name = "libunwind" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6639b70a7ce854b79c70d7e83f16b5dc0137cc914f3d7d03803b513ecc67ac" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linked_hash_set" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "984fb35d06508d1e69fc91050cceba9c0b748f983e6739fa2c7a9237154c52c8" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lz4_flex" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e" +dependencies = [ + "twox-hash", +] + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + +[[package]] +name = "macho-unwind-info" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4bdc8b0ce69932332cf76d24af69c3a155242af95c226b2ab6c2e371ed1149" +dependencies = [ + "thiserror 2.0.18", + "zerocopy", + "zerocopy-derive", +] + +[[package]] +name = "managed" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "memmap2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d28bba84adfe6646737845bc5ebbfa2c08424eb1c37e94a1fd2a82adb56a872" +dependencies = [ + "libc", +] + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "more-asserts" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fafa6961cabd9c63bcd77a45d7e3b7f3b552b70417831fb0f56db717e72407e" + +[[package]] +name = "msvc-demangler" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeff6bd154a309b2ada5639b2661ca6ae4599b34e8487dc276d2cd637da2d76" +dependencies = [ + "bitflags 2.13.0", + "itoa", +] + +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "munge" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c" +dependencies = [ + "munge_macro", +] + +[[package]] +name = "munge_macro" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.0", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "5.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b" +dependencies = [ + "memchr", + "version_check", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "object" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "crc32fast", + "flate2", + "hashbrown 0.17.1", + "indexmap 2.14.0", + "memchr", + "ruzstd", +] + +[[package]] +name = "oliphaunt-example-tauri-wasix" +version = "0.1.0" +dependencies = [ + "anyhow", + "oliphaunt-wasix", + "serde", + "sqlx", + "tauri", + "tauri-build", + "thiserror 2.0.18", + "tokio", +] + +[[package]] +name = "oliphaunt-wasix" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "directories", + "dunce", + "filetime", + "flate2", + "hex", + "oliphaunt-wasix-aot-aarch64-apple-darwin", + "oliphaunt-wasix-aot-aarch64-unknown-linux-gnu", + "oliphaunt-wasix-aot-x86_64-pc-windows-msvc", + "oliphaunt-wasix-aot-x86_64-unknown-linux-gnu", + "oliphaunt-wasix-assets", + "regex", + "serde", + "serde_json", + "sha2 0.10.9", + "tar", + "tempfile", + "tokio", + "tracing", + "wasmer", + "wasmer-config", + "wasmer-types", + "wasmer-wasix", + "webc", + "zstd", +] + +[[package]] +name = "oliphaunt-wasix-aot-aarch64-apple-darwin" +version = "0.1.0" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "oliphaunt-wasix-aot-aarch64-unknown-linux-gnu" +version = "0.1.0" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "oliphaunt-wasix-aot-x86_64-pc-windows-msvc" +version = "0.1.0" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "oliphaunt-wasix-aot-x86_64-unknown-linux-gnu" +version = "0.1.0" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "oliphaunt-wasix-assets" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "path-clean" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "serde", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.118", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "ptr_meta" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "pulldown-cmark" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8" +dependencies = [ + "bitflags 1.3.2", + "memchr", + "unicase", +] + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rancor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a063ea72381527c2a0561da9c80000ef822bdd7c3241b1cc1b12100e3df081ee" +dependencies = [ + "ptr_meta", +] + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rangemap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_syscall" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "region" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b6ebd13bc009aef9cd476c1310d49ac354d36e240cf1bd753290f3dc7199a7" +dependencies = [ + "bitflags 1.3.2", + "libc", + "mach2 0.4.3", + "windows-sys 0.52.0", +] + +[[package]] +name = "rend" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "replace_with" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51743d3e274e2b18df81c4dc6caf8a5b8e15dbe799e0dca05c7617380094e884" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73389e0c99e664f919275ab5b5b0471391fe9a8de61e1dff9b1eaf56a90f16e3" +dependencies = [ + "bytecheck", + "bytes", + "hashbrown 0.17.1", + "indexmap 2.14.0", + "munge", + "ptr_meta", + "rancor", + "rend", + "rkyv_derive", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d2ed0b54125315fb36bd021e82d314d1c126548f871634b483f46b31d13cac6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty_pool" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed36cdb20de66d89a17ea04b8883fc7a386f2cf877aaedca5005583ce4876ff" +dependencies = [ + "crossbeam-channel", + "futures", + "futures-channel", + "futures-executor", + "num_cpus", +] + +[[package]] +name = "ruzstd" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7c1c839d570d835527c9a5e4db7cb2198683a988cb9d7293fc8674e6bd58fc8" +dependencies = [ + "twox-hash", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "saffron" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03fb9a628596fc7590eb7edbf7b0613287be78df107f5f97b118aad59fb2eea9" +dependencies = [ + "chrono", + "nom 5.1.3", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive 0.8.22", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "indexmap 2.14.0", + "ref-cast", + "schemars_derive 1.2.1", + "serde", + "serde_json", + "url", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.118", +] + +[[package]] +name = "schemars_derive" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.118", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.0", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.14.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "shared-buffer" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6c99835bad52957e7aa241d3975ed17c1e5f8c92026377d117a606f36b84b16" +dependencies = [ + "bytes", + "memmap2 0.6.2", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[package]] +name = "smoltcp" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f73d40463bba65efc9adc6370b56df76d563cc46e2482bba58351b4afb7535e" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "cfg-if", + "defmt 0.3.100", + "heapless", + "managed", +] + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall 0.5.18", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-postgres", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64 0.22.1", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap 2.14.0", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rustls", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.118", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.9", + "sqlx-core", + "sqlx-postgres", + "syn 2.0.118", + "tokio", + "url", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.13.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.6", + "serde", + "serde_json", + "sha2 0.10.9", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "whoami", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "symbolic-common" +version = "13.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1acef24ab2c9b307824e99ee81544a7fd5eac70b29898013580c2ab68e22104b" +dependencies = [ + "debugid", + "memmap2 0.9.11", + "stable_deref_trait", + "uuid", +] + +[[package]] +name = "symbolic-demangle" +version = "13.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eafb9860981a3611afed2ffadf834dabc8e7921ae9e6fe941ffee8d8d206888f" +dependencies = [ + "cpp_demangle", + "msvc-demangler", + "rustc-demangle", + "symbolic-common", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.0", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tauri" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2616f96cb644bf2c5c456d9de4d5d5100e592d7424c74d8b55c5cb96e359e93" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2 0.10.9", + "syn 2.0.118", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe41e015bf8fc4d6477ff4926a0ef769dc64ff34c7b0038b6f7cacae892acb5c" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.2+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[package]] +name = "terminal_size" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" +dependencies = [ + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "time" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.0", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "virtual-fs" +version = "0.702.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e66c1686d8c304c6136cb1a553cbc16c92261af8f34be365af8400b0ce82f94" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "dashmap", + "derive_more", + "dunce", + "futures", + "getrandom 0.4.3", + "indexmap 2.14.0", + "pin-project-lite", + "replace_with", + "shared-buffer", + "slab", + "thiserror 2.0.18", + "tokio", + "tracing", + "virtual-mio", + "wasmer-package", + "webc", +] + +[[package]] +name = "virtual-mio" +version = "0.702.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f86b519f58e30beca3845b5da865ebb7ea29c59b8d6b625ef8982ef1af93337" +dependencies = [ + "async-trait", + "bytes", + "futures", + "mio", + "parking", + "serde", + "socket2", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "virtual-net" +version = "0.702.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac308570c4756033af92f1b8680f0f84b82df526d25575c2136cde7bbbd838d6" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.22.1", + "bincode", + "bytecheck", + "bytes", + "derive_more", + "futures-util", + "ipnet", + "iprange", + "libc", + "mio", + "pin-project-lite", + "rkyv", + "serde", + "smoltcp", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "virtual-mio", +] + +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + +[[package]] +name = "wai-bindgen-gen-core" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa3dc41b510811122b3088197234c27e08fcad63ef936306dd8e11e2803876c" +dependencies = [ + "anyhow", + "wai-parser", +] + +[[package]] +name = "wai-bindgen-gen-rust" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19bc05e8380515c4337c40ef03b2ff233e391315b178a320de8640703d522efe" +dependencies = [ + "heck 0.3.3", + "wai-bindgen-gen-core", +] + +[[package]] +name = "wai-bindgen-gen-rust-wasm" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f35ce5e74086fac87f3a7bd50f643f00fe3559adb75c88521ecaa01c8a6199" +dependencies = [ + "heck 0.3.3", + "wai-bindgen-gen-core", + "wai-bindgen-gen-rust", +] + +[[package]] +name = "wai-bindgen-rust" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e5601c6f448c063e83a5e931b8fefcdf7e01ada424ad42372c948d2e3d67741" +dependencies = [ + "bitflags 1.3.2", + "wai-bindgen-rust-impl", +] + +[[package]] +name = "wai-bindgen-rust-impl" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdeeb5c1170246de8425a3e123e7ef260dc05ba2b522a1d369fe2315376efea4" +dependencies = [ + "proc-macro2", + "syn 1.0.109", + "wai-bindgen-gen-core", + "wai-bindgen-gen-rust-wasm", +] + +[[package]] +name = "wai-parser" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd0acb6d70885ea0c343749019ba74f015f64a9d30542e66db69b49b7e28186" +dependencies = [ + "anyhow", + "id-arena", + "pulldown-cmark", + "unicode-normalization", + "unicode-xid", +] + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.118", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.250.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2271adb766023046af314460f1fae02cc34ea16d736d93404d3b65be44270923" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmer" +version = "7.2.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "596add954aa5e3937e889839c63250fc72340ccdb0cb9adcb89f026535300f73" +dependencies = [ + "bindgen", + "bytes", + "cfg-if", + "cmake", + "corosensei", + "dashmap", + "derive_more", + "futures", + "indexmap 2.14.0", + "itertools 0.14.0", + "js-sys", + "more-asserts", + "paste", + "rkyv", + "serde", + "serde-wasm-bindgen", + "shared-buffer", + "symbolic-demangle", + "tar", + "target-lexicon 0.13.5", + "thiserror 2.0.18", + "tracing", + "wasm-bindgen", + "wasmer-compiler", + "wasmer-derive", + "wasmer-types", + "wasmer-vm", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmer-compiler" +version = "7.2.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c15b69f6d74316e1a8366911bd04d9bab1115a8712c1fb4323d37624382d84c" +dependencies = [ + "backtrace", + "bytes", + "cfg-if", + "crossbeam-channel", + "enum-iterator", + "enumset", + "itertools 0.14.0", + "leb128", + "libc", + "macho-unwind-info", + "memmap2 0.9.11", + "more-asserts", + "object 0.39.1", + "rangemap", + "rayon", + "region", + "rkyv", + "self_cell", + "shared-buffer", + "smallvec", + "target-lexicon 0.13.5", + "tempfile", + "thiserror 2.0.18", + "wasmer-types", + "wasmer-vm", + "wasmparser", + "which", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmer-config" +version = "0.702.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcff14aae6b37c51f0bdc6e73736df7b978dd0515659e5fc6db3afb74ffe323f" +dependencies = [ + "anyhow", + "bytesize", + "ciborium", + "derive_builder", + "hex", + "indexmap 2.14.0", + "saffron", + "schemars 1.2.1", + "semver", + "serde", + "serde_json", + "serde_yaml", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", +] + +[[package]] +name = "wasmer-derive" +version = "7.2.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349030f566b3fe9ef09bf4abf4b917968a937f403a5e208740aa4c88e87928e5" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "wasmer-journal" +version = "0.702.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5863066574694ff8df6cf316416e89b7d4f0c7bca866facdfd4d8369b335fa55" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.22.1", + "bincode", + "bytecheck", + "bytes", + "derive_more", + "lz4_flex", + "num_enum", + "rkyv", + "serde", + "serde_json", + "thiserror 2.0.18", + "tracing", + "virtual-fs", + "virtual-net", + "wasmer", + "wasmer-config", + "wasmer-wasix-types", +] + +[[package]] +name = "wasmer-package" +version = "0.702.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b786ad94623fa6612d4ed85e2603590797544ecd4ac5f8d414bebe677920cd5" +dependencies = [ + "anyhow", + "bytes", + "cfg-if", + "ciborium", + "flate2", + "ignore", + "insta", + "libc", + "semver", + "serde", + "serde_json", + "sha2 0.11.0", + "shared-buffer", + "tar", + "tempfile", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", + "wasmer-config", + "wasmer-types", + "webc", +] + +[[package]] +name = "wasmer-types" +version = "7.2.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aaf2baad42ce3f3ebc4508fbe8bb362fe31c08bae9048646842affd4868812d" +dependencies = [ + "bytecheck", + "crc32fast", + "enum-iterator", + "enumset", + "getrandom 0.4.3", + "hex", + "indexmap 2.14.0", + "itertools 0.14.0", + "more-asserts", + "rkyv", + "serde", + "sha2 0.11.0", + "target-lexicon 0.13.5", + "thiserror 2.0.18", + "wasmparser", +] + +[[package]] +name = "wasmer-vm" +version = "7.2.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54214dc7f3bc7c0f19eb31ac7d10796f30314a6fb3666004f4b11798646dd6e4" +dependencies = [ + "backtrace", + "bytesize", + "cc", + "cfg-if", + "corosensei", + "crossbeam-queue", + "dashmap", + "enum-iterator", + "fnv", + "gimli 0.33.0", + "indexmap 2.14.0", + "itertools 0.14.0", + "libc", + "libunwind", + "mach2 0.6.0", + "memoffset", + "more-asserts", + "parking_lot", + "region", + "rustversion", + "scopeguard", + "thiserror 2.0.18", + "wasmer-types", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmer-wasix" +version = "0.702.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6cfbfb4636accd684b014841965d19674b75b8ae8446e9327ef04f7a7e9ae9" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.22.1", + "bincode", + "blake3", + "bus", + "bytecheck", + "bytes", + "cfg-if", + "cooked-waker", + "crossbeam-channel", + "dashmap", + "derive_more", + "flate2", + "fnv", + "fs_extra", + "futures", + "getrandom 0.3.4", + "getrandom 0.4.3", + "heapless", + "hex", + "http", + "itertools 0.14.0", + "libc", + "libtest-mimic", + "linked_hash_set", + "lz4_flex", + "num_enum", + "once_cell", + "petgraph", + "pin-project", + "pin-utils", + "rand 0.10.1", + "rkyv", + "rusty_pool", + "semver", + "serde", + "serde_derive", + "serde_json", + "serde_yaml", + "sha2 0.11.0", + "shared-buffer", + "tempfile", + "terminal_size", + "termios", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "toml 1.1.2+spec-1.1.0", + "tracing", + "url", + "urlencoding", + "virtual-fs", + "virtual-mio", + "virtual-net", + "waker-fn", + "walkdir", + "wasm-encoder", + "wasmer", + "wasmer-config", + "wasmer-journal", + "wasmer-package", + "wasmer-types", + "wasmer-wasix-types", + "wasmparser", + "webc", + "weezl", + "windows-sys 0.61.2", + "xxhash-rust", + "zstd", +] + +[[package]] +name = "wasmer-wasix-types" +version = "0.702.0-alpha.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e823d48c54f97a6663844c2fd52dad4894da08fc930bcb930b93799b5d9606" +dependencies = [ + "anyhow", + "bitflags 2.13.0", + "byteorder", + "cfg-if", + "num_enum", + "serde", + "time", + "tracing", + "wai-bindgen-gen-core", + "wai-bindgen-gen-rust", + "wai-bindgen-gen-rust-wasm", + "wai-bindgen-rust", + "wai-parser", + "wasmer", + "wasmer-derive", + "wasmer-types", +] + +[[package]] +name = "wasmparser" +version = "0.250.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071d99cdfb8111603ed05500506c3298a940b58d609dd0259d3981785dd33556" +dependencies = [ + "bitflags 2.13.0", + "indexmap 2.14.0", +] + +[[package]] +name = "web-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webc" +version = "12.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cb48ee4bc7a902c0f1d9eb0c0656f0e78149f1190b7f78e1f28256e88279a84" +dependencies = [ + "anyhow", + "base64 0.22.1", + "bytes", + "cfg-if", + "ciborium", + "document-features", + "ignore", + "indexmap 2.14.0", + "leb128", + "lexical-sort", + "libc", + "once_cell", + "path-clean", + "rand 0.9.4", + "serde", + "serde_json", + "sha2 0.10.9", + "shared-buffer", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.8", +] + +[[package]] +name = "webpki-roots" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "weezl" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ca08e5ef825b65b056d9efbd95c8750683f0a6d0466d02e96dc2e4e360f3d2" + +[[package]] +name = "which" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d7cd18d4acb58fb3cdfe9ea54e6cd96a4e7d4cc45c56338b236e82dad47248" +dependencies = [ + "libc", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2 0.10.9", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/examples/tauri-wasix/src-tauri/Cargo.toml b/examples/tauri-wasix/src-tauri/Cargo.toml new file mode 100644 index 00000000..5ea40bd1 --- /dev/null +++ b/examples/tauri-wasix/src-tauri/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "oliphaunt-example-tauri-wasix" +version = "0.1.0" +description = "Tauri todo app backed by oliphaunt-wasix and SQLx" +edition = "2021" +publish = false + +[workspace] + +[lib] +name = "oliphaunt_example_tauri_wasix_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +anyhow = "1" +oliphaunt-wasix = { path = "../../../src/bindings/wasix-rust/crates/oliphaunt-wasix", features = ["extensions"] } +serde = { version = "1", features = ["derive"] } +sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio-rustls", "postgres"] } +tauri = { version = "2", features = [] } +thiserror = "2" +tokio = { version = "1", features = ["sync"] } diff --git a/examples/tauri-wasix/src-tauri/build.rs b/examples/tauri-wasix/src-tauri/build.rs new file mode 100644 index 00000000..261851f6 --- /dev/null +++ b/examples/tauri-wasix/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build(); +} diff --git a/examples/tauri-wasix/src-tauri/capabilities/default.json b/examples/tauri-wasix/src-tauri/capabilities/default.json new file mode 100644 index 00000000..0c61c5d9 --- /dev/null +++ b/examples/tauri-wasix/src-tauri/capabilities/default.json @@ -0,0 +1,7 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Default desktop permissions", + "windows": ["main"], + "permissions": ["core:default"] +} diff --git a/examples/tauri-wasix/src-tauri/src/lib.rs b/examples/tauri-wasix/src-tauri/src/lib.rs new file mode 100644 index 00000000..777060d2 --- /dev/null +++ b/examples/tauri-wasix/src-tauri/src/lib.rs @@ -0,0 +1,255 @@ +use std::path::PathBuf; +use std::time::Duration; + +use anyhow::{Context, Result}; +use oliphaunt_wasix::{extensions, OliphauntServer}; +use serde::{Deserialize, Serialize}; +use serde::ser::Serializer; +use sqlx::postgres::PgPoolOptions; +use sqlx::{PgPool, Row}; +use tauri::Manager; +use tokio::sync::Mutex; + +const CREATE_EXTENSIONS: &[&str] = &[ + "CREATE EXTENSION IF NOT EXISTS hstore", + "CREATE EXTENSION IF NOT EXISTS pg_trgm", + "CREATE EXTENSION IF NOT EXISTS unaccent", +]; + +const CREATE_TABLE: &str = r#" +CREATE TABLE IF NOT EXISTS todos ( + id bigserial PRIMARY KEY, + title text NOT NULL, + notes text NOT NULL DEFAULT '', + tags hstore NOT NULL DEFAULT ''::hstore, + done boolean NOT NULL DEFAULT false, + priority integer NOT NULL DEFAULT 2 CHECK (priority BETWEEN 1 AND 3), + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +) +"#; + +const CREATE_INDEX: &str = "CREATE INDEX IF NOT EXISTS todos_title_trgm ON todos USING gin (title gin_trgm_ops)"; + +const SELECT_TODOS: &str = r#" +SELECT + id, + title, + notes, + COALESCE(tags -> 'area', '') AS area, + COALESCE(tags -> 'context', '') AS context, + done, + priority, + to_char(created_at, 'YYYY-MM-DD HH24:MI') AS created_at, + to_char(updated_at, 'YYYY-MM-DD HH24:MI') AS updated_at +FROM todos +WHERE + ( + $1::text = '' + OR unaccent(title || ' ' || notes) ILIKE '%' || unaccent($1::text) || '%' + OR COALESCE(tags -> 'area', '') ILIKE '%' || $1::text || '%' + OR COALESCE(tags -> 'context', '') ILIKE '%' || $1::text || '%' + OR tags ? $1::text + ) + AND ( + $2::text = 'all' + OR ($2::text = 'open' AND NOT done) + OR ($2::text = 'done' AND done) + ) +ORDER BY done ASC, priority ASC, updated_at DESC, id DESC +"#; + +const RETURNING_TODO: &str = r#" +RETURNING + id, + title, + notes, + COALESCE(tags -> 'area', '') AS area, + COALESCE(tags -> 'context', '') AS context, + done, + priority, + to_char(created_at, 'YYYY-MM-DD HH24:MI') AS created_at, + to_char(updated_at, 'YYYY-MM-DD HH24:MI') AS updated_at +"#; + +struct TodoStore { + inner: Mutex, +} + +struct TodoDatabase { + pool: PgPool, + _server: OliphauntServer, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct CreateTodo { + title: String, + notes: String, + area: String, + context: String, + priority: i32, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct Todo { + id: i64, + title: String, + notes: String, + area: String, + context: String, + priority: i32, + done: bool, + created_at: String, + updated_at: String, +} + +#[derive(Debug, thiserror::Error)] +enum CommandError { + #[error("{0}")] + Runtime(String), +} + +impl serde::Serialize for CommandError { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl From for CommandError { + fn from(value: anyhow::Error) -> Self { + Self::Runtime(format!("{value:#}")) + } +} + +impl From for CommandError { + fn from(value: sqlx::Error) -> Self { + Self::Runtime(value.to_string()) + } +} + +async fn open_database(root: PathBuf) -> Result { + let server = OliphauntServer::builder() + .path(root) + .extensions([extensions::HSTORE, extensions::PG_TRGM, extensions::UNACCENT]) + .start() + .context("start oliphaunt-wasix server")?; + let pool = PgPoolOptions::new() + .max_connections(1) + .acquire_timeout(Duration::from_secs(30)) + .connect(&server.connection_uri()) + .await + .context("connect SQLx pool to oliphaunt-wasix server")?; + init_schema(&pool).await?; + Ok(TodoDatabase { + pool, + _server: server, + }) +} + +async fn init_schema(pool: &PgPool) -> Result<()> { + for statement in CREATE_EXTENSIONS { + sqlx::query(statement).execute(pool).await?; + } + sqlx::query(CREATE_TABLE).execute(pool).await?; + sqlx::query(CREATE_INDEX).execute(pool).await?; + Ok(()) +} + +#[tauri::command] +async fn list_todos( + state: tauri::State<'_, TodoStore>, + search: String, + status: String, +) -> Result, CommandError> { + let db = state.inner.lock().await; + let rows = sqlx::query(SELECT_TODOS) + .bind(search) + .bind(status) + .fetch_all(&db.pool) + .await?; + rows.into_iter() + .map(|row| todo_from_row(&row).map_err(CommandError::from)) + .collect() +} + +#[tauri::command] +async fn create_todo( + state: tauri::State<'_, TodoStore>, + input: CreateTodo, +) -> Result { + let db = state.inner.lock().await; + let sql = format!( + "INSERT INTO todos (title, notes, tags, priority) + VALUES ($1, $2, hstore(ARRAY['area', $3, 'context', $4]), $5) + {RETURNING_TODO}" + ); + let row = sqlx::query(&sql) + .bind(input.title) + .bind(input.notes) + .bind(input.area) + .bind(input.context) + .bind(input.priority.clamp(1, 3)) + .fetch_one(&db.pool) + .await?; + todo_from_row(&row).map_err(CommandError::from) +} + +#[tauri::command] +async fn toggle_todo(state: tauri::State<'_, TodoStore>, id: i64) -> Result { + let db = state.inner.lock().await; + let sql = format!( + "UPDATE todos SET done = NOT done, updated_at = now() WHERE id = $1 {RETURNING_TODO}" + ); + let row = sqlx::query(&sql).bind(id).fetch_one(&db.pool).await?; + todo_from_row(&row).map_err(CommandError::from) +} + +#[tauri::command] +async fn delete_todo(state: tauri::State<'_, TodoStore>, id: i64) -> Result<(), CommandError> { + let db = state.inner.lock().await; + sqlx::query("DELETE FROM todos WHERE id = $1") + .bind(id) + .execute(&db.pool) + .await?; + Ok(()) +} + +fn todo_from_row(row: &sqlx::postgres::PgRow) -> Result { + Ok(Todo { + id: row.try_get("id")?, + title: row.try_get("title")?, + notes: row.try_get("notes")?, + area: row.try_get("area")?, + context: row.try_get("context")?, + priority: row.try_get("priority")?, + done: row.try_get("done")?, + created_at: row.try_get("created_at")?, + updated_at: row.try_get("updated_at")?, + }) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .setup(|app| { + let root = app.path().app_data_dir()?.join("oliphaunt-wasix-todos"); + let db = tauri::async_runtime::block_on(open_database(root))?; + app.manage(TodoStore { + inner: Mutex::new(db), + }); + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + list_todos, + create_todo, + toggle_todo, + delete_todo + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/examples/tauri-wasix/src-tauri/src/main.rs b/examples/tauri-wasix/src-tauri/src/main.rs new file mode 100644 index 00000000..5e4a42e9 --- /dev/null +++ b/examples/tauri-wasix/src-tauri/src/main.rs @@ -0,0 +1,6 @@ +// Prevents an extra console window on Windows in release builds. +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + oliphaunt_example_tauri_wasix_lib::run(); +} diff --git a/examples/tauri-wasix/src-tauri/tauri.conf.json b/examples/tauri-wasix/src-tauri/tauri.conf.json new file mode 100644 index 00000000..5d5dde43 --- /dev/null +++ b/examples/tauri-wasix/src-tauri/tauri.conf.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "Oliphaunt Tauri WASIX Todo", + "version": "0.1.0", + "identifier": "dev.oliphaunt.examples.tauri.wasix.todo", + "build": { + "beforeDevCommand": "pnpm run dev", + "devUrl": "http://localhost:1422", + "beforeBuildCommand": "pnpm run build", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "title": "Oliphaunt Tauri WASIX Todo", + "width": 1100, + "height": 760 + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": false, + "icon": [ + "../../../src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/icon.png" + ] + } +} diff --git a/examples/tauri-wasix/src/main.ts b/examples/tauri-wasix/src/main.ts new file mode 100644 index 00000000..876c4d84 --- /dev/null +++ b/examples/tauri-wasix/src/main.ts @@ -0,0 +1 @@ +import "../../tauri/src/main.ts"; diff --git a/examples/tauri-wasix/src/styles.css b/examples/tauri-wasix/src/styles.css new file mode 100644 index 00000000..1c8454f3 --- /dev/null +++ b/examples/tauri-wasix/src/styles.css @@ -0,0 +1 @@ +@import "../../tauri/src/styles.css"; diff --git a/examples/tauri-wasix/tsconfig.json b/examples/tauri-wasix/tsconfig.json new file mode 100644 index 00000000..48d633fe --- /dev/null +++ b/examples/tauri-wasix/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "strict": true + }, + "include": ["src"] +} diff --git a/examples/tauri-wasix/vite.config.ts b/examples/tauri-wasix/vite.config.ts new file mode 100644 index 00000000..93eef2a3 --- /dev/null +++ b/examples/tauri-wasix/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + clearScreen: false, + server: { + port: 1422, + strictPort: true, + }, +}); diff --git a/examples/tauri/.gitignore b/examples/tauri/.gitignore new file mode 100644 index 00000000..433fc4bb --- /dev/null +++ b/examples/tauri/.gitignore @@ -0,0 +1,4 @@ +dist +node_modules +src-tauri/gen +src-tauri/target diff --git a/examples/tauri/README.md b/examples/tauri/README.md new file mode 100644 index 00000000..cf9e10ea --- /dev/null +++ b/examples/tauri/README.md @@ -0,0 +1,11 @@ +# Tauri Native Todo + +Tauri v2 owns an `oliphaunt` Rust SDK handle in backend state and exposes +app-specific commands to the webview. The native runtime is selected in Rust, +the persistent root lives under the app data directory, and the exact extension +set is declared in `src-tauri/Cargo.toml`. + +```sh +pnpm --dir examples/tauri install +pnpm --dir examples/tauri tauri dev +``` diff --git a/examples/tauri/index.html b/examples/tauri/index.html new file mode 100644 index 00000000..0d0f6268 --- /dev/null +++ b/examples/tauri/index.html @@ -0,0 +1,68 @@ + + + + + + + Oliphaunt Tauri Todo + + + +
+
+
+

Tauri / native Rust SDK

+

Oliphaunt Todo

+
+ Ready +
+ +
+ + +
+ + + + +
+
+ +
+ +
+ + + +
+
+ +
+ 0 open + 0 done + 0 high priority +
+ +
+
+ + diff --git a/examples/tauri/package.json b/examples/tauri/package.json new file mode 100644 index 00000000..b5a621be --- /dev/null +++ b/examples/tauri/package.json @@ -0,0 +1,20 @@ +{ + "name": "oliphaunt-example-tauri", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "tauri": "tauri" + }, + "dependencies": { + "@tauri-apps/api": "^2" + }, + "devDependencies": { + "@tauri-apps/cli": "^2", + "typescript": "catalog:", + "vite": "^6.0.3" + } +} diff --git a/examples/tauri/src-tauri/Cargo.lock b/examples/tauri/src-tauri/Cargo.lock new file mode 100644 index 00000000..0ac8d072 --- /dev/null +++ b/examples/tauri/src-tauri/Cargo.lock @@ -0,0 +1,4589 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce8d3bd5823c7504d3f579f13e7b2f3da252fcb938c594d5680ee508bf846f" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.0", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.0", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.118", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dbus" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.118", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading 0.7.4", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.0", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "oliphaunt" +version = "0.1.0" +dependencies = [ + "crossbeam-channel", + "flate2", + "fs2", + "getrandom 0.3.4", + "libloading 0.8.9", + "serde", + "sha2", + "tar", + "toml 0.9.12+spec-1.1.0", + "zip", + "zstd", +] + +[[package]] +name = "oliphaunt-build" +version = "0.1.0" +dependencies = [ + "serde", + "sha2", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "oliphaunt-example-tauri" +version = "0.1.0" +dependencies = [ + "anyhow", + "oliphaunt", + "oliphaunt-build", + "serde", + "tauri", + "tauri-build", + "thiserror 2.0.18", + "tokio", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.118", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.0", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.0", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2616f96cb644bf2c5c456d9de4d5d5100e592d7424c74d8b55c5cb96e359e93" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.118", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe41e015bf8fc4d6477ff4926a0ef769dc64ff34c7b0038b6f7cacae892acb5c" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.2+spec-1.1.0", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "time" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.0", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.118", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap 2.14.0", + "memchr", + "thiserror 2.0.18", + "zopfli", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/examples/tauri/src-tauri/Cargo.toml b/examples/tauri/src-tauri/Cargo.toml new file mode 100644 index 00000000..40bd5b96 --- /dev/null +++ b/examples/tauri/src-tauri/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "oliphaunt-example-tauri" +version = "0.1.0" +description = "Tauri todo app backed by the Oliphaunt native Rust SDK" +edition = "2021" +publish = false + +[workspace] + +[lib] +name = "oliphaunt_example_tauri_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[package.metadata.oliphaunt] +runtime = "liboliphaunt-native" +runtime-version = "0.1.0" +extensions = ["hstore", "pg_trgm", "unaccent"] + +[build-dependencies] +oliphaunt-build = { path = "../../../src/sdks/rust/crates/oliphaunt-build" } +tauri-build = { version = "2", features = [] } + +[dependencies] +anyhow = "1" +oliphaunt = { path = "../../../src/sdks/rust" } +serde = { version = "1", features = ["derive"] } +tauri = { version = "2", features = [] } +thiserror = "2" +tokio = { version = "1", features = ["sync"] } diff --git a/examples/tauri/src-tauri/build.rs b/examples/tauri/src-tauri/build.rs new file mode 100644 index 00000000..c26929e0 --- /dev/null +++ b/examples/tauri/src-tauri/build.rs @@ -0,0 +1,4 @@ +fn main() { + oliphaunt_build::configure(); + tauri_build::build(); +} diff --git a/examples/tauri/src-tauri/capabilities/default.json b/examples/tauri/src-tauri/capabilities/default.json new file mode 100644 index 00000000..0c61c5d9 --- /dev/null +++ b/examples/tauri/src-tauri/capabilities/default.json @@ -0,0 +1,7 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Default desktop permissions", + "windows": ["main"], + "permissions": ["core:default"] +} diff --git a/examples/tauri/src-tauri/src/lib.rs b/examples/tauri/src-tauri/src/lib.rs new file mode 100644 index 00000000..d1de354b --- /dev/null +++ b/examples/tauri/src-tauri/src/lib.rs @@ -0,0 +1,234 @@ +use std::path::PathBuf; + +use oliphaunt::{Extension, Oliphaunt, QueryResult}; +use serde::{Deserialize, Serialize}; +use serde::ser::Serializer; +use tauri::Manager; +use tokio::sync::Mutex; + +const SCHEMA: &str = r#" +CREATE EXTENSION IF NOT EXISTS hstore; +CREATE EXTENSION IF NOT EXISTS pg_trgm; +CREATE EXTENSION IF NOT EXISTS unaccent; + +CREATE TABLE IF NOT EXISTS todos ( + id bigserial PRIMARY KEY, + title text NOT NULL, + notes text NOT NULL DEFAULT '', + tags hstore NOT NULL DEFAULT ''::hstore, + done boolean NOT NULL DEFAULT false, + priority integer NOT NULL DEFAULT 2 CHECK (priority BETWEEN 1 AND 3), + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS todos_title_trgm + ON todos USING gin (title gin_trgm_ops); +"#; + +const SELECT_TODOS: &str = r#" +SELECT + id::text AS id, + title, + notes, + COALESCE(tags -> 'area', '') AS area, + COALESCE(tags -> 'context', '') AS context, + done::text AS done, + priority::text AS priority, + to_char(created_at, 'YYYY-MM-DD HH24:MI') AS created_at, + to_char(updated_at, 'YYYY-MM-DD HH24:MI') AS updated_at +FROM todos +WHERE + ( + $1::text = '' + OR unaccent(title || ' ' || notes) ILIKE '%' || unaccent($1::text) || '%' + OR COALESCE(tags -> 'area', '') ILIKE '%' || $1::text || '%' + OR COALESCE(tags -> 'context', '') ILIKE '%' || $1::text || '%' + OR tags ? $1::text + ) + AND ( + $2::text = 'all' + OR ($2::text = 'open' AND NOT done) + OR ($2::text = 'done' AND done) + ) +ORDER BY done ASC, priority ASC, updated_at DESC, id DESC +"#; + +const RETURNING_TODO: &str = r#" +RETURNING + id::text AS id, + title, + notes, + COALESCE(tags -> 'area', '') AS area, + COALESCE(tags -> 'context', '') AS context, + done::text AS done, + priority::text AS priority, + to_char(created_at, 'YYYY-MM-DD HH24:MI') AS created_at, + to_char(updated_at, 'YYYY-MM-DD HH24:MI') AS updated_at +"#; + +struct TodoStore { + db: Mutex, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct CreateTodo { + title: String, + notes: String, + area: String, + context: String, + priority: i32, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct Todo { + id: i64, + title: String, + notes: String, + area: String, + context: String, + priority: i32, + done: bool, + created_at: String, + updated_at: String, +} + +#[derive(Debug, thiserror::Error)] +enum CommandError { + #[error("{0}")] + Runtime(String), +} + +impl serde::Serialize for CommandError { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl From for CommandError { + fn from(value: anyhow::Error) -> Self { + Self::Runtime(format!("{value:#}")) + } +} + +impl From for CommandError { + fn from(value: oliphaunt::Error) -> Self { + Self::Runtime(value.to_string()) + } +} + +async fn open_database(root: PathBuf) -> anyhow::Result { + let db = Oliphaunt::builder() + .path(root) + .native_direct() + .extensions([Extension::Hstore, Extension::PgTrgm, Extension::Unaccent]) + .open() + .await?; + db.execute(SCHEMA).await?; + Ok(db) +} + +#[tauri::command] +async fn list_todos( + state: tauri::State<'_, TodoStore>, + search: String, + status: String, +) -> Result, CommandError> { + let db = state.db.lock().await; + let result = db.query_params(SELECT_TODOS, [search, status]).await?; + todos_from_result(&result).map_err(CommandError::from) +} + +#[tauri::command] +async fn create_todo( + state: tauri::State<'_, TodoStore>, + input: CreateTodo, +) -> Result { + let db = state.db.lock().await; + let priority = input.priority.clamp(1, 3).to_string(); + let sql = format!( + "INSERT INTO todos (title, notes, tags, priority) + VALUES ($1, $2, hstore(ARRAY['area', $3, 'context', $4]), $5::integer) + {RETURNING_TODO}" + ); + let result = db + .query_params( + &sql, + [input.title, input.notes, input.area, input.context, priority], + ) + .await?; + one_todo(&result).map_err(CommandError::from) +} + +#[tauri::command] +async fn toggle_todo(state: tauri::State<'_, TodoStore>, id: i64) -> Result { + let db = state.db.lock().await; + let sql = format!( + "UPDATE todos + SET done = NOT done, updated_at = now() + WHERE id = $1 + {RETURNING_TODO}" + ); + let result = db.query_params(&sql, [id]).await?; + one_todo(&result).map_err(CommandError::from) +} + +#[tauri::command] +async fn delete_todo(state: tauri::State<'_, TodoStore>, id: i64) -> Result<(), CommandError> { + let db = state.db.lock().await; + db.query_params("DELETE FROM todos WHERE id = $1 RETURNING id::text AS id", [id]) + .await?; + Ok(()) +} + +fn todos_from_result(result: &QueryResult) -> anyhow::Result> { + (0..result.row_count()).map(|row| todo_from_result(result, row)).collect() +} + +fn one_todo(result: &QueryResult) -> anyhow::Result { + todo_from_result(result, 0) +} + +fn todo_from_result(result: &QueryResult, row: usize) -> anyhow::Result { + Ok(Todo { + id: required(result, row, "id")?.parse()?, + title: required(result, row, "title")?.to_owned(), + notes: required(result, row, "notes")?.to_owned(), + area: required(result, row, "area")?.to_owned(), + context: required(result, row, "context")?.to_owned(), + priority: required(result, row, "priority")?.parse()?, + done: required(result, row, "done")? == "true", + created_at: required(result, row, "created_at")?.to_owned(), + updated_at: required(result, row, "updated_at")?.to_owned(), + }) +} + +fn required<'a>(result: &'a QueryResult, row: usize, column: &str) -> anyhow::Result<&'a str> { + result + .get_text(row, column)? + .ok_or_else(|| anyhow::anyhow!("missing {column}")) +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .setup(|app| { + let root = app.path().app_data_dir()?.join("oliphaunt-native-todos"); + let db = tauri::async_runtime::block_on(open_database(root))?; + app.manage(TodoStore { db: Mutex::new(db) }); + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + list_todos, + create_todo, + toggle_todo, + delete_todo + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/examples/tauri/src-tauri/src/main.rs b/examples/tauri/src-tauri/src/main.rs new file mode 100644 index 00000000..e9cd563c --- /dev/null +++ b/examples/tauri/src-tauri/src/main.rs @@ -0,0 +1,6 @@ +// Prevents an extra console window on Windows in release builds. +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + oliphaunt_example_tauri_lib::run(); +} diff --git a/examples/tauri/src-tauri/tauri.conf.json b/examples/tauri/src-tauri/tauri.conf.json new file mode 100644 index 00000000..2b305869 --- /dev/null +++ b/examples/tauri/src-tauri/tauri.conf.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "Oliphaunt Tauri Todo", + "version": "0.1.0", + "identifier": "dev.oliphaunt.examples.tauri.todo", + "build": { + "beforeDevCommand": "pnpm run dev", + "devUrl": "http://localhost:1421", + "beforeBuildCommand": "pnpm run build", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "title": "Oliphaunt Tauri Todo", + "width": 1100, + "height": 760 + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": false, + "icon": [ + "../../../src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/icons/icon.png" + ] + } +} diff --git a/examples/tauri/src/main.ts b/examples/tauri/src/main.ts new file mode 100644 index 00000000..09ce9734 --- /dev/null +++ b/examples/tauri/src/main.ts @@ -0,0 +1,160 @@ +import { invoke } from "@tauri-apps/api/core"; + +type Todo = { + id: number; + title: string; + notes: string; + area: string; + context: string; + priority: number; + done: boolean; + createdAt: string; + updatedAt: string; +}; + +type CreateTodoInput = { + title: string; + notes: string; + area: string; + context: string; + priority: number; +}; + +type StatusFilter = "open" | "all" | "done"; + +const form = document.querySelector("#todo-form"); +const list = document.querySelector("#todo-list"); +const status = document.querySelector("#status"); +const search = document.querySelector("#search"); +const openCount = document.querySelector("#open-count"); +const doneCount = document.querySelector("#done-count"); +const highCount = document.querySelector("#high-count"); +let activeStatus: StatusFilter = "open"; +let todos: Todo[] = []; + +async function listTodos() { + todos = await invoke("list_todos", { + search: search?.value.trim() ?? "", + status: activeStatus, + }); + render(); +} + +async function createTodo(input: CreateTodoInput) { + await invoke("create_todo", { input }); + await listTodos(); +} + +async function toggleTodo(id: number) { + await invoke("toggle_todo", { id }); + await listTodos(); +} + +async function deleteTodo(id: number) { + await invoke("delete_todo", { id }); + await listTodos(); +} + +function setStatus(message: string) { + if (status) status.value = message; +} + +function priorityLabel(priority: number) { + if (priority === 1) return "High"; + if (priority === 3) return "Low"; + return "Normal"; +} + +function render() { + const open = todos.filter((todo) => !todo.done).length; + const done = todos.filter((todo) => todo.done).length; + const high = todos.filter((todo) => !todo.done && todo.priority === 1).length; + if (openCount) openCount.value = `${open} open`; + if (doneCount) doneCount.value = `${done} done`; + if (highCount) highCount.value = `${high} high priority`; + if (!list) return; + if (todos.length === 0) { + const empty = document.createElement("p"); + empty.className = "empty"; + empty.textContent = "No todos match the current filter."; + list.replaceChildren(empty); + return; + } + list.replaceChildren(...todos.map(renderTodo)); +} + +function renderTodo(todo: Todo) { + const row = document.createElement("article"); + row.className = todo.done ? "todo done" : "todo"; + + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.checked = todo.done; + checkbox.addEventListener("change", () => void toggleTodo(todo.id)); + + const body = document.createElement("div"); + const title = document.createElement("h2"); + title.textContent = todo.title; + const notes = document.createElement("p"); + notes.textContent = todo.notes || "No notes"; + const meta = document.createElement("div"); + meta.className = "meta"; + for (const value of [ + priorityLabel(todo.priority), + todo.area ? `area:${todo.area}` : "", + todo.context ? `context:${todo.context}` : "", + `updated ${todo.updatedAt}`, + ]) { + if (!value) continue; + const pill = document.createElement("span"); + pill.className = "pill"; + pill.textContent = value; + meta.append(pill); + } + body.append(title, notes, meta); + + const remove = document.createElement("button"); + remove.className = "secondary"; + remove.type = "button"; + remove.textContent = "Delete"; + remove.addEventListener("click", () => void deleteTodo(todo.id)); + + row.append(checkbox, body, remove); + return row; +} + +form?.addEventListener("submit", (event) => { + event.preventDefault(); + const data = new FormData(form); + const input: CreateTodoInput = { + title: String(data.get("title") ?? "").trim(), + notes: String(data.get("notes") ?? "").trim(), + area: String(data.get("area") ?? "").trim(), + context: String(data.get("context") ?? "").trim(), + priority: Number(data.get("priority") ?? 2), + }; + if (!input.title) return; + setStatus("Saving"); + createTodo(input) + .then(() => { + form.reset(); + setStatus("Saved"); + }) + .catch((error) => setStatus(String(error))); +}); + +search?.addEventListener("input", () => { + void listTodos().catch((error) => setStatus(String(error))); +}); + +document.querySelectorAll("[data-status]").forEach((button) => { + button.addEventListener("click", () => { + activeStatus = button.dataset.status as StatusFilter; + document + .querySelectorAll("[data-status]") + .forEach((candidate) => candidate.classList.toggle("active", candidate === button)); + void listTodos().catch((error) => setStatus(String(error))); + }); +}); + +void listTodos().catch((error) => setStatus(String(error))); diff --git a/examples/tauri/src/styles.css b/examples/tauri/src/styles.css new file mode 100644 index 00000000..ab5387f8 --- /dev/null +++ b/examples/tauri/src/styles.css @@ -0,0 +1,231 @@ +:root { + color: #1f2933; + background: #f5f7f9; + font-family: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; +} + +button, +input, +select, +textarea { + font: inherit; +} + +button { + border: 0; + border-radius: 6px; + background: #23424f; + color: #ffffff; + cursor: pointer; + font-weight: 700; + min-height: 42px; + padding: 0 14px; +} + +button.secondary { + background: #d9e2e7; + color: #1f2933; +} + +.shell { + inline-size: min(1120px, calc(100vw - 32px)); + margin: 0 auto; + padding: 28px 0 40px; +} + +.topbar, +.filters, +.summary, +.todo { + border: 1px solid #d8e0e6; + background: #ffffff; +} + +.topbar { + align-items: center; + border-radius: 8px; + display: flex; + justify-content: space-between; + padding: 20px; +} + +.eyebrow { + color: #60707c; + font-size: 0.78rem; + font-weight: 800; + letter-spacing: 0; + margin: 0 0 6px; + text-transform: uppercase; +} + +h1 { + font-size: clamp(1.8rem, 4vw, 3rem); + line-height: 1; + margin: 0; +} + +output { + color: #3b4b55; + font-weight: 700; +} + +.composer { + display: grid; + gap: 14px; + margin-block: 18px; +} + +label { + display: grid; + gap: 6px; + font-weight: 700; +} + +label span { + color: #52636f; + font-size: 0.82rem; +} + +input, +select, +textarea { + border: 1px solid #c8d3db; + border-radius: 6px; + color: #1f2933; + inline-size: 100%; + min-block-size: 42px; + padding: 10px 12px; +} + +textarea { + resize: vertical; +} + +.form-grid { + display: grid; + gap: 14px; + grid-template-columns: 1fr 1fr 160px 140px; +} + +.filters { + align-items: center; + border-radius: 8px; + display: grid; + gap: 14px; + grid-template-columns: 1fr auto; + padding: 14px; +} + +.segments { + display: inline-grid; + grid-template-columns: repeat(3, 88px); +} + +.segments button { + background: #eef3f6; + border-radius: 0; + color: #33444f; +} + +.segments button:first-child { + border-radius: 6px 0 0 6px; +} + +.segments button:last-child { + border-radius: 0 6px 6px 0; +} + +.segments button.active { + background: #23424f; + color: #ffffff; +} + +.summary { + border-radius: 8px; + display: grid; + gap: 12px; + grid-template-columns: repeat(3, 1fr); + margin-block: 18px; + padding: 14px; +} + +.todo-list { + display: grid; + gap: 12px; +} + +.todo { + border-radius: 8px; + display: grid; + gap: 12px; + grid-template-columns: auto 1fr auto; + padding: 14px; +} + +.todo.done { + opacity: 0.68; +} + +.todo h2 { + font-size: 1rem; + margin: 0 0 4px; +} + +.todo p { + color: #52636f; + margin: 0; +} + +.meta { + color: #60707c; + display: flex; + flex-wrap: wrap; + font-size: 0.82rem; + gap: 8px; + margin-top: 10px; +} + +.pill { + background: #edf7f3; + border: 1px solid #c9e8dc; + border-radius: 999px; + padding: 3px 8px; +} + +.empty { + color: #60707c; + padding: 24px; + text-align: center; +} + +@media (max-width: 760px) { + .topbar, + .filters, + .todo { + align-items: stretch; + grid-template-columns: 1fr; + } + + .topbar { + display: grid; + gap: 12px; + } + + .form-grid, + .summary { + grid-template-columns: 1fr; + } + + .segments { + grid-template-columns: repeat(3, 1fr); + } +} diff --git a/examples/tauri/tsconfig.json b/examples/tauri/tsconfig.json new file mode 100644 index 00000000..48d633fe --- /dev/null +++ b/examples/tauri/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "strict": true + }, + "include": ["src"] +} diff --git a/examples/tauri/vite.config.ts b/examples/tauri/vite.config.ts new file mode 100644 index 00000000..0deb512b --- /dev/null +++ b/examples/tauri/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + clearScreen: false, + server: { + port: 1421, + strictPort: true, + }, +}); diff --git a/examples/tools/check-examples.sh b/examples/tools/check-examples.sh index 5d11a0a3..80e5d2f7 100755 --- a/examples/tools/check-examples.sh +++ b/examples/tools/check-examples.sh @@ -14,7 +14,7 @@ run() { run examples/tools/check-lockfiles.sh --check -allowed_root_examples='^(examples/moon\.yml|examples/tools/[^/]+)$' +allowed_root_examples='^(examples/moon\.yml|examples/README\.md|examples/tools/[^/]+|examples/(tauri|tauri-wasix|electron|electron-wasix)(/.*)?)$' violations="$( git ls-files examples | grep -Ev "$allowed_root_examples" || true )" @@ -55,6 +55,14 @@ require_file "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Carg require_text "src/bindings/wasix-rust/moon.yml" '^ example-check:$' require_text "src/bindings/wasix-rust/moon.yml" 'tags: \["examples", "quality", "ci-wasm-regression"\]' +for example in tauri tauri-wasix electron electron-wasix; do + require_file "examples/$example/package.json" + require_file "examples/$example/README.md" +done +require_file "examples/tauri/src-tauri/Cargo.toml" +require_file "examples/tauri-wasix/src-tauri/Cargo.toml" +require_file "examples/electron-wasix/src-wasix/Cargo.toml" + require_file "src/sdks/react-native/examples/expo/package.json" require_file "src/sdks/react-native/examples/expo/maestro/installed-smoke.yaml" require_text "src/sdks/react-native/moon.yml" '^ mobile-build-android:$' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28c6bd60..0be54297 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,79 @@ importers: .: {} + examples/electron: + dependencies: + '@oliphaunt/ts': + specifier: workspace:* + version: link:../../src/sdks/js + devDependencies: + '@types/node': + specifier: ^24.10.1 + version: 24.12.4 + electron: + specifier: ^39.2.5 + version: 39.8.10 + typescript: + specifier: 'catalog:' + version: 5.9.3 + vite: + specifier: ^6.0.3 + version: 6.4.2(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0) + + examples/electron-wasix: + dependencies: + pg: + specifier: ^8.16.3 + version: 8.22.0 + devDependencies: + '@types/node': + specifier: ^24.10.1 + version: 24.12.4 + '@types/pg': + specifier: ^8.15.6 + version: 8.20.0 + electron: + specifier: ^39.2.5 + version: 39.8.10 + typescript: + specifier: 'catalog:' + version: 5.9.3 + vite: + specifier: ^6.0.3 + version: 6.4.2(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0) + + examples/tauri: + dependencies: + '@tauri-apps/api': + specifier: ^2 + version: 2.11.0 + devDependencies: + '@tauri-apps/cli': + specifier: ^2 + version: 2.11.2 + typescript: + specifier: 'catalog:' + version: 5.9.3 + vite: + specifier: ^6.0.3 + version: 6.4.2(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0) + + examples/tauri-wasix: + dependencies: + '@tauri-apps/api': + specifier: ^2 + version: 2.11.0 + devDependencies: + '@tauri-apps/cli': + specifier: ^2 + version: 2.11.2 + typescript: + specifier: 'catalog:' + version: 5.9.3 + vite: + specifier: ^6.0.3 + version: 6.4.2(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.3)(yaml@2.9.0) + src/bindings/wasix-rust/examples/tauri-sqlx-vanilla: dependencies: '@tauri-apps/api': @@ -782,6 +855,10 @@ packages: resolution: {integrity: sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==} engines: {node: '>=0.8.0'} + '@electron/get@2.0.3': + resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} + engines: {node: '>=12'} + '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -2315,12 +2392,20 @@ packages: '@sinclair/typebox@0.27.10': resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@szmarczak/http-timer@4.0.6': + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + '@tailwindcss/node@4.3.0': resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} @@ -2508,6 +2593,9 @@ packages: '@tybys/wasm-util@0.10.2': resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + '@types/cacheable-request@6.0.3': + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -2532,6 +2620,9 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/http-cache-semantics@4.2.0': + resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==} + '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -2547,6 +2638,9 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/keyv@3.1.4': + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -2562,6 +2656,9 @@ packages: '@types/node@24.12.4': resolution: {integrity: sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==} + '@types/pg@8.20.0': + resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -2576,6 +2673,9 @@ packages: '@types/react@19.2.16': resolution: {integrity: sha512-esJiCAnl0kfpNdE69f3So4WJUXy95dLZydX0KwK46riIHDzHM7O9Vtf9xCHW0PXIqvgqNrswl522kA/5yx+F4w==} + '@types/responselike@1.0.3': + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -2588,6 +2688,9 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + '@typescript-eslint/eslint-plugin@8.59.4': resolution: {integrity: sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3017,6 +3120,10 @@ packages: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} + boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + bplist-creator@0.1.0: resolution: {integrity: sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==} @@ -3047,6 +3154,9 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -3054,6 +3164,14 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + cacheable-lookup@5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + + cacheable-request@7.0.4: + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -3141,6 +3259,9 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} @@ -3294,6 +3415,10 @@ packages: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -3304,6 +3429,10 @@ packages: defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -3331,6 +3460,9 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -3354,6 +3486,11 @@ packages: electron-to-chromium@1.5.361: resolution: {integrity: sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==} + electron@39.8.10: + resolution: {integrity: sha512-zbYtGPYUI7PzqLAzkk21Rk6j67WN0hxn0Mq/njErZo1d0HSf33is4f8ICI5fMLy5vYe0JtCtM5sYunNOaochSQ==} + engines: {node: '>= 12.20.55'} + hasBin: true + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -3365,6 +3502,9 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + enhanced-resolve@5.22.1: resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} engines: {node: '>=10.13.0'} @@ -3377,6 +3517,10 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + error-stack-parser@2.1.4: resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} @@ -3415,6 +3559,9 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + esast-util-from-estree@2.0.0: resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} @@ -3847,6 +3994,11 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -3873,6 +4025,9 @@ packages: fbjs@3.0.5: resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==} + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -3956,6 +4111,10 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -4105,6 +4264,10 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + get-symbol-description@1.1.0: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} @@ -4133,6 +4296,10 @@ packages: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} + global-agent@3.0.0: + resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} + engines: {node: '>=10.0'} + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -4149,6 +4316,10 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + got@11.8.6: + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -4248,10 +4419,17 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} + http2-wrapper@1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -4535,6 +4713,9 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -4544,6 +4725,9 @@ packages: engines: {node: '>=6'} hasBin: true + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + jsr@0.14.3: resolution: {integrity: sha512-PGxnDepx7vwJoZQe2SHbyBiFfpGwsOKmX4kn/wZZqfMafV7fjXqTxSaX6lp9QHYkSTLKkER+P/wmrZY3gVJNzg==} hasBin: true @@ -4675,6 +4859,10 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -4720,6 +4908,10 @@ packages: marky@1.3.0: resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} + matcher@3.0.0: + resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} + engines: {node: '>=10'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -4984,6 +5176,14 @@ packages: resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==} engines: {node: '>=4'} + mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -5130,6 +5330,10 @@ packages: resolution: {integrity: sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==} engines: {node: '>=0.12.0'} + normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + npm-package-arg@11.0.3: resolution: {integrity: sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==} engines: {node: ^16.14.0 || >=18.0.0} @@ -5217,6 +5421,10 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} + p-cancelable@2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -5267,6 +5475,43 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + pg-cloudflare@1.4.0: + resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} + + pg-connection-string@2.14.0: + resolution: {integrity: sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.14.0: + resolution: {integrity: sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.15.0: + resolution: {integrity: sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.22.0: + resolution: {integrity: sha512-8wih1vVIBMxoUM2oB4soJsD9tDnDpLv4OXBJ+EJzFsvycD+lfyIreC2gGHq78f8jbLLt+bvlPTFdFZfJkOuzAA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -5305,6 +5550,22 @@ packages: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -5341,6 +5602,9 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + punycode.js@2.3.1: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} @@ -5360,6 +5624,10 @@ packages: queue@6.0.2: resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -5600,6 +5868,9 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -5624,10 +5895,17 @@ packages: engines: {node: '>= 0.4'} hasBin: true + responselike@2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + restore-cursor@2.0.0: resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==} engines: {node: '>=4'} + roarr@2.15.4: + resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} + engines: {node: '>=8.0'} + rollup@4.60.4: resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -5669,6 +5947,9 @@ packages: resolution: {integrity: sha512-QNI2ChmuioGC1/xjyYwyZYADILWyW6AmS1UH6gDj/SFUUUS4MBAWs/7mxnkRPc/F4iHezDP+O8t0dO8WHiEOdg==} engines: {node: '>=6'} + semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -5690,6 +5971,10 @@ packages: resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==} engines: {node: '>=0.10.0'} + serialize-error@7.0.1: + resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} + engines: {node: '>=10'} + serve-static@1.16.3: resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} engines: {node: '>= 0.8.0'} @@ -5815,6 +6100,13 @@ packages: resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} engines: {node: '>=6'} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} @@ -5922,6 +6214,10 @@ packages: styleq@0.1.3: resolution: {integrity: sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==} + sumchecker@3.0.1: + resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} + engines: {node: '>= 8.0'} + supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -6023,6 +6319,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} @@ -6134,6 +6434,10 @@ packages: unist-util-visit@5.1.0: resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -6395,6 +6699,10 @@ packages: resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} engines: {node: '>=8.0'} + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -6415,6 +6723,9 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -6960,6 +7271,20 @@ snapshots: dependencies: '@types/hammerjs': 2.0.46 + '@electron/get@2.0.3': + dependencies: + debug: 4.4.3 + env-paths: 2.2.1 + fs-extra: 8.1.0 + got: 11.8.6 + progress: 2.0.3 + semver: 6.3.1 + sumchecker: 3.0.1 + optionalDependencies: + global-agent: 3.0.0 + transitivePeerDependencies: + - supports-color + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -8657,12 +8982,18 @@ snapshots: '@sinclair/typebox@0.27.10': {} + '@sindresorhus/is@4.6.0': {} + '@standard-schema/spec@1.1.0': {} '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 + '@szmarczak/http-timer@4.0.6': + dependencies: + defer-to-connect: 2.0.1 + '@tailwindcss/node@4.3.0': dependencies: '@jridgewell/remapping': 2.3.5 @@ -8801,6 +9132,13 @@ snapshots: tslib: 2.8.1 optional: true + '@types/cacheable-request@6.0.3': + dependencies: + '@types/http-cache-semantics': 4.2.0 + '@types/keyv': 3.1.4 + '@types/node': 24.12.4 + '@types/responselike': 1.0.3 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -8826,6 +9164,8 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/http-cache-semantics@4.2.0': {} + '@types/istanbul-lib-coverage@2.0.6': {} '@types/istanbul-lib-report@3.0.3': @@ -8840,6 +9180,10 @@ snapshots: '@types/json5@0.0.29': {} + '@types/keyv@3.1.4': + dependencies: + '@types/node': 24.12.4 + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -8856,6 +9200,12 @@ snapshots: dependencies: undici-types: 7.16.0 + '@types/pg@8.20.0': + dependencies: + '@types/node': 24.12.4 + pg-protocol: 1.15.0 + pg-types: 2.2.0 + '@types/react-dom@19.2.3(@types/react@19.2.15)': dependencies: '@types/react': 19.2.15 @@ -8877,6 +9227,10 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/responselike@1.0.3': + dependencies: + '@types/node': 24.12.4 + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -8887,6 +9241,11 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 24.12.4 + optional: true + '@typescript-eslint/eslint-plugin@8.59.4(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -9384,6 +9743,9 @@ snapshots: transitivePeerDependencies: - supports-color + boolean@3.2.0: + optional: true + bplist-creator@0.1.0: dependencies: stream-buffers: 2.2.0 @@ -9421,10 +9783,24 @@ snapshots: dependencies: node-int64: 0.4.0 + buffer-crc32@0.2.13: {} + buffer-from@1.1.2: {} bytes@3.1.2: {} + cacheable-lookup@5.0.4: {} + + cacheable-request@7.0.4: + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.2.0 + keyv: 4.5.4 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.1 + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -9516,6 +9892,10 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + clone-response@1.0.3: + dependencies: + mimic-response: 1.0.1 + clone@1.0.4: {} clsx@2.1.1: {} @@ -9658,6 +10038,10 @@ snapshots: decode-uri-component@0.2.2: {} + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -9666,6 +10050,8 @@ snapshots: dependencies: clone: 1.0.4 + defer-to-connect@2.0.1: {} + define-data-property@1.1.4: dependencies: es-define-property: 1.0.1 @@ -9688,6 +10074,9 @@ snapshots: detect-node-es@1.1.0: {} + detect-node@2.1.0: + optional: true + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -9710,12 +10099,24 @@ snapshots: electron-to-chromium@1.5.361: {} + electron@39.8.10: + dependencies: + '@electron/get': 2.0.3 + '@types/node': 22.19.19 + extract-zip: 2.0.1 + transitivePeerDependencies: + - supports-color + emoji-regex@8.0.0: {} encodeurl@1.0.2: {} encodeurl@2.0.0: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + enhanced-resolve@5.22.1: dependencies: graceful-fs: 4.2.11 @@ -9725,6 +10126,8 @@ snapshots: entities@6.0.1: {} + env-paths@2.2.1: {} + error-stack-parser@2.1.4: dependencies: stackframe: 1.3.4 @@ -9832,6 +10235,9 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + es6-error@4.1.1: + optional: true + esast-util-from-estree@2.0.0: dependencies: '@types/estree-jsx': 1.0.5 @@ -10489,6 +10895,16 @@ snapshots: extend@3.0.2: {} + extract-zip@2.0.1: + dependencies: + debug: 4.4.3 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + fast-deep-equal@3.1.3: {} fast-json-stable-stringify@2.1.0: {} @@ -10517,6 +10933,10 @@ snapshots: transitivePeerDependencies: - encoding + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 @@ -10596,6 +11016,12 @@ snapshots: fresh@2.0.0: {} + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + fsevents@2.3.3: optional: true @@ -10735,6 +11161,10 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.2 + get-stream@5.2.0: + dependencies: + pump: 3.0.4 + get-symbol-description@1.1.0: dependencies: call-bound: 1.0.4 @@ -10768,6 +11198,16 @@ snapshots: minipass: 7.1.3 path-scurry: 2.0.2 + global-agent@3.0.0: + dependencies: + boolean: 3.2.0 + es6-error: 4.1.1 + matcher: 3.0.0 + roarr: 2.15.4 + semver: 7.8.1 + serialize-error: 7.0.1 + optional: true + globals@14.0.0: {} globals@16.5.0: {} @@ -10779,6 +11219,20 @@ snapshots: gopd@1.2.0: {} + got@11.8.6: + dependencies: + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.4 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.1 + graceful-fs@4.2.11: {} has-bigints@1.1.0: {} @@ -10947,6 +11401,8 @@ snapshots: html-void-elements@3.0.0: {} + http-cache-semantics@4.2.0: {} + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -10955,6 +11411,11 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 + http2-wrapper@1.0.3: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 @@ -11229,12 +11690,19 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + json-stringify-safe@5.0.1: + optional: true + json5@1.0.2: dependencies: minimist: 1.2.8 json5@2.2.3: {} + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + jsr@0.14.3: dependencies: node-stream-zip: 1.15.0 @@ -11342,6 +11810,8 @@ snapshots: dependencies: js-tokens: 4.0.0 + lowercase-keys@2.0.0: {} + lru-cache@10.4.3: {} lru-cache@11.5.0: {} @@ -11389,6 +11859,11 @@ snapshots: marky@1.3.0: {} + matcher@3.0.0: + dependencies: + escape-string-regexp: 4.0.0 + optional: true + math-intrinsics@1.1.0: {} mdast-util-find-and-replace@3.0.2: @@ -12025,6 +12500,10 @@ snapshots: mimic-fn@1.2.0: {} + mimic-response@1.0.1: {} + + mimic-response@3.1.0: {} + min-indent@1.0.1: {} minimatch@10.2.5: @@ -12145,6 +12624,8 @@ snapshots: node-stream-zip@1.15.0: {} + normalize-url@6.1.0: {} + npm-package-arg@11.0.3: dependencies: hosted-git-info: 7.0.2 @@ -12257,6 +12738,8 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 + p-cancelable@2.1.1: {} + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -12306,6 +12789,43 @@ snapshots: pathe@2.0.3: {} + pend@1.2.0: {} + + pg-cloudflare@1.4.0: + optional: true + + pg-connection-string@2.14.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.14.0(pg@8.22.0): + dependencies: + pg: 8.22.0 + + pg-protocol@1.15.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.22.0: + dependencies: + pg-connection-string: 2.14.0 + pg-pool: 3.14.0(pg@8.22.0) + pg-protocol: 1.15.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.4.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -12338,6 +12858,16 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + prelude-ls@1.2.1: {} pretty-format@29.7.0: @@ -12376,6 +12906,11 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + punycode.js@2.3.1: {} punycode@2.3.1: {} @@ -12395,6 +12930,8 @@ snapshots: dependencies: inherits: 2.0.4 + quick-lru@5.1.1: {} + range-parser@1.2.1: {} raw-body@3.0.2: @@ -12816,6 +13353,8 @@ snapshots: require-from-string@2.0.2: {} + resolve-alpn@1.2.1: {} + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -12840,11 +13379,25 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + responselike@2.0.1: + dependencies: + lowercase-keys: 2.0.0 + restore-cursor@2.0.0: dependencies: onetime: 2.0.1 signal-exit: 3.0.7 + roarr@2.15.4: + dependencies: + boolean: 3.2.0 + detect-node: 2.1.0 + globalthis: 1.0.4 + json-stringify-safe: 5.0.1 + semver-compare: 1.0.0 + sprintf-js: 1.1.3 + optional: true + rollup@4.60.4: dependencies: '@types/estree': 1.0.8 @@ -12919,6 +13472,9 @@ snapshots: semiver@1.1.0: {} + semver-compare@1.0.0: + optional: true + semver@6.3.1: {} semver@7.8.1: {} @@ -12959,6 +13515,11 @@ snapshots: serialize-error@2.1.0: {} + serialize-error@7.0.1: + dependencies: + type-fest: 0.13.1 + optional: true + serve-static@1.16.3: dependencies: encodeurl: 2.0.0 @@ -13127,6 +13688,11 @@ snapshots: split-on-first@1.1.0: {} + split2@4.2.0: {} + + sprintf-js@1.1.3: + optional: true + stable-hash@0.0.5: {} stackback@0.0.2: {} @@ -13240,6 +13806,12 @@ snapshots: styleq@0.1.3: {} + sumchecker@3.0.1: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + supports-color@5.5.0: dependencies: has-flag: 3.0.0 @@ -13329,6 +13901,9 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@0.13.1: + optional: true + type-fest@0.21.3: {} type-fest@0.7.1: {} @@ -13457,6 +14032,8 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 + universalify@0.1.2: {} + unpipe@1.0.0: {} unrs-resolver@1.12.2: @@ -13725,6 +14302,8 @@ snapshots: xmlbuilder@15.1.1: {} + xtend@4.0.2: {} + y18n@5.0.8: {} yallist@3.1.1: {} @@ -13743,6 +14322,11 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + yocto-queue@0.1.0: {} zod-to-json-schema@3.25.2(zod@3.25.76): diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9a03208e..d9f8d951 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,6 +9,10 @@ packages: - "src/sdks/react-native" - "src/sdks/react-native/examples/expo" - "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla" + - "examples/tauri" + - "examples/tauri-wasix" + - "examples/electron" + - "examples/electron-wasix" catalog: "@vitest/coverage-v8": ^4.1.8 @@ -27,6 +31,7 @@ verifyDepsBeforeRun: false allowBuilds: core-js: false + electron: true esbuild: true msgpackr-extract: true sharp: true diff --git a/src/runtimes/liboliphaunt/icu/build.rs b/src/runtimes/liboliphaunt/icu/build.rs index e42f5216..64dc17ae 100644 --- a/src/runtimes/liboliphaunt/icu/build.rs +++ b/src/runtimes/liboliphaunt/icu/build.rs @@ -1,7 +1,7 @@ use std::env; use std::fs; use std::io::{self, Read}; -use std::path::{Path, PathBuf}; +use std::path::{Component, Path, PathBuf}; use sha2::{Digest, Sha256}; @@ -9,6 +9,7 @@ const ARTIFACT_SCHEMA: &str = "oliphaunt-artifact-manifest-v1"; const ARTIFACT_PRODUCT: &str = "oliphaunt-icu"; const ARTIFACT_KIND: &str = "icu-data"; const ARTIFACT_TARGET: &str = "portable"; +const PACKAGED_ICU_ARCHIVE: &str = "payload/icu-data.tar.zst"; fn main() { println!("cargo:rerun-if-env-changed=OLIPHAUNT_ICU_DATA_DIR"); @@ -16,7 +17,12 @@ fn main() { let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set by Cargo")); let out = out_dir.join("generated_icu.rs"); - if let Some(icu_root) = find_icu_data_root() { + if let Some(archive) = find_packaged_icu_archive() { + println!("cargo:rerun-if-changed={}", archive.display()); + let extracted_root = unpack_icu_archive(&archive, &out_dir.join("icu-data-expanded")); + write_generated_icu(&out, Some(&archive)); + emit_artifact_manifest(&out_dir, &extracted_root); + } else if let Some(icu_root) = find_icu_data_root() { emit_rerun_directives(&icu_root); let archive = out_dir.join("icu-data.tar.zst"); write_icu_archive(&icu_root, &archive); @@ -24,12 +30,21 @@ fn main() { emit_artifact_manifest(&out_dir, &icu_root); } else { if env::var_os("OLIPHAUNT_ARTIFACT_CRATE_REQUIRE_PAYLOAD").is_some() { - panic!("release packaging requires package-local ICU data under payload/share/icu"); + panic!( + "release packaging requires package-local ICU data under payload/icu-data.tar.zst or payload/share/icu" + ); } write_generated_icu(&out, None); } } +fn find_packaged_icu_archive() -> Option { + let manifest_dir = + PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set")); + let archive = manifest_dir.join(PACKAGED_ICU_ARCHIVE); + archive.is_file().then_some(archive) +} + fn find_icu_data_root() -> Option { let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set")); @@ -77,6 +92,72 @@ fn repo_root_from_manifest_dir(manifest_dir: &Path) -> Option<&Path> { }) } +fn unpack_icu_archive(archive: &Path, destination: &Path) -> PathBuf { + if destination.exists() { + fs::remove_dir_all(destination).expect("remove previously unpacked ICU data archive"); + } + fs::create_dir_all(destination).expect("create ICU data archive destination"); + let file = fs::File::open(archive).expect("open packaged ICU data archive"); + let decoder = zstd::stream::read::Decoder::new(file).expect("decode packaged ICU data archive"); + let mut archive_reader = tar::Archive::new(decoder); + let entries = archive_reader + .entries() + .expect("read packaged ICU data archive entries"); + for entry in entries { + let mut entry = entry.expect("read packaged ICU data archive entry"); + let path = entry + .path() + .expect("read packaged ICU data archive entry path") + .into_owned(); + let relative = icu_archive_relative_path(&path); + let destination_path = destination.join(&relative); + let entry_type = entry.header().entry_type(); + if entry_type.is_dir() { + fs::create_dir_all(&destination_path).expect("create ICU data archive directory"); + continue; + } + if !entry_type.is_file() { + panic!( + "packaged ICU data archive entry {} has unsupported type {:?}", + path.display(), + entry_type + ); + } + if let Some(parent) = destination_path.parent() { + fs::create_dir_all(parent).expect("create ICU data archive entry parent"); + } + entry + .unpack(&destination_path) + .expect("unpack packaged ICU data archive entry"); + } + let root = destination.join("share/icu"); + canonical_icu_data_root(&root).expect("packaged ICU data archive contains share/icu data") +} + +fn icu_archive_relative_path(path: &Path) -> PathBuf { + let mut relative = PathBuf::new(); + let mut components = Vec::new(); + for component in path.components() { + match component { + Component::CurDir => {} + Component::Normal(part) => { + relative.push(part); + components.push(part.to_owned()); + } + _ => panic!("unsafe packaged ICU data archive entry {}", path.display()), + } + } + let under_share_icu = components.first().and_then(|part| part.to_str()) == Some("share") + && components.get(1).and_then(|part| part.to_str()) == Some("icu"); + if !under_share_icu { + panic!( + "packaged ICU data archive entry {} must stay under share/icu", + path.display() + ); + } + relative +} + fn canonical_icu_data_root(candidate: &Path) -> Option { if icu_root_contains_data(candidate) { return Some(candidate.to_path_buf()); diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py new file mode 100755 index 00000000..b7906a83 --- /dev/null +++ b/tools/release/local_registry_publish.py @@ -0,0 +1,754 @@ +#!/usr/bin/env python3 +"""Stage Oliphaunt release artifacts into local package registries. + +The script intentionally consumes the same artifact shape produced by CI: + +* npm package tarballs under ``target/sdk-artifacts`` or a downloaded artifact + directory are published to a local Verdaccio. +* Rust ``.crate`` files are indexed into a local Cargo git registry whose + downloads point at local files. +* Maven repository trees are copied into a local filesystem Maven repository. +* SwiftPM artifacts are staged for inspection; the Swift product currently + releases through a source tag rather than a registry publish. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import shutil +import subprocess +import sys +import tarfile +import tempfile +import time +import tomllib +import urllib.error +import urllib.request +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Iterable + + +ROOT = Path(__file__).resolve().parents[2] +DEFAULT_RUN_ID = "28049923289" +DEFAULT_REPO = "f0rr0/oliphaunt" +DEFAULT_REGISTRY_ROOT = ROOT / "target" / "local-registries" +DEFAULT_ARTIFACT_ROOT = ROOT / "target" / "local-registry-artifacts" + +LOCAL_PUBLISH_ARTIFACTS = [ + "liboliphaunt-native-release-assets", + "liboliphaunt-native-release-assets-android-arm64-v8a", + "liboliphaunt-native-release-assets-android-x86_64", + "liboliphaunt-native-release-assets-ios-xcframework", + "liboliphaunt-native-release-assets-linux-arm64-gnu", + "liboliphaunt-native-release-assets-linux-x64-gnu", + "liboliphaunt-native-release-assets-macos-arm64", + "liboliphaunt-native-release-assets-windows-x64-msvc", + "liboliphaunt-wasix-extension-artifacts-wasix-portable", + "liboliphaunt-wasix-release-assets", + "liboliphaunt-wasix-runtime-aot-linux-arm64-gnu", + "liboliphaunt-wasix-runtime-aot-linux-x64-gnu", + "liboliphaunt-wasix-runtime-aot-macos-arm64", + "liboliphaunt-wasix-runtime-aot-windows-x64-msvc", + "liboliphaunt-wasix-runtime-portable", + "oliphaunt-broker-release-assets-linux-arm64-gnu", + "oliphaunt-broker-release-assets-linux-x64-gnu", + "oliphaunt-broker-release-assets-macos-arm64", + "oliphaunt-broker-release-assets-windows-x64-msvc", + "oliphaunt-extension-package-artifacts", + "oliphaunt-rust-sdk-package-artifacts", + "oliphaunt-wasix-rust-package-artifacts", + "oliphaunt-js-sdk-package-artifacts", + "oliphaunt-react-native-sdk-package-artifacts", + "oliphaunt-kotlin-sdk-package-artifacts", + "oliphaunt-swift-sdk-package-artifacts", + "oliphaunt-mobile-extension-package-artifacts", + "oliphaunt-node-direct-npm-package-linux-x64-gnu", + "oliphaunt-node-direct-npm-package-linux-arm64-gnu", + "oliphaunt-node-direct-npm-package-macos-arm64", + "oliphaunt-node-direct-npm-package-windows-x64-msvc", + "oliphaunt-node-direct-release-assets-linux-arm64-gnu", + "oliphaunt-node-direct-release-assets-linux-x64-gnu", + "oliphaunt-node-direct-release-assets-macos-arm64", + "oliphaunt-node-direct-release-assets-windows-x64-msvc", +] + + +def rel(path: Path) -> str: + try: + return str(path.relative_to(ROOT)) + except ValueError: + return str(path) + + +def run( + args: list[str], + *, + cwd: Path = ROOT, + check: bool = True, + capture: bool = False, + env: dict[str, str] | None = None, + timeout: float | None = None, +) -> subprocess.CompletedProcess[str]: + kwargs: dict[str, Any] = { + "cwd": cwd, + "check": check, + "text": True, + "env": env, + "timeout": timeout, + } + if capture: + kwargs["stdout"] = subprocess.PIPE + kwargs["stderr"] = subprocess.PIPE + return subprocess.run(args, **kwargs) + + +def require_command(name: str) -> str: + resolved = shutil.which(name) + if not resolved: + raise RuntimeError(f"missing required command: {name}") + return resolved + + +@dataclass +class SurfaceResult: + surface: str + published: list[str] = field(default_factory=list) + staged: list[str] = field(default_factory=list) + skipped: list[str] = field(default_factory=list) + + def add_skip(self, message: str) -> None: + self.skipped.append(message) + + +def discover_roots(extra_roots: Iterable[Path]) -> list[Path]: + roots = [ + DEFAULT_ARTIFACT_ROOT, + ROOT / "target" / "sdk-artifacts", + ROOT / "target" / "package" / "tmp-crate", + ROOT / "target" / "package" / "tmp-registry", + ROOT / "target" / "oliphaunt-wasix" / "cargo-artifacts", + ROOT / "target" / "oliphaunt-wasix" / "release-assets", + ROOT / "target" / "extension-artifacts", + ] + roots.extend(extra_roots) + seen: set[Path] = set() + result: list[Path] = [] + for root in roots: + resolved = root.resolve() + if resolved in seen or not resolved.exists(): + continue + seen.add(resolved) + result.append(resolved) + return result + + +def list_ci_artifacts(repo: str, run_id: str) -> list[dict[str, Any]]: + require_command("gh") + completed = run( + [ + "gh", + "api", + f"repos/{repo}/actions/runs/{run_id}/artifacts?per_page=100", + "--paginate", + ], + capture=True, + ) + data = json.loads(completed.stdout) + if isinstance(data, list): + artifacts: list[dict[str, Any]] = [] + for page in data: + artifacts.extend(page.get("artifacts", [])) + return artifacts + return data.get("artifacts", []) + + +def download_artifacts(args: argparse.Namespace) -> None: + artifacts = list(args.artifact) + if args.preset == "local-publish": + artifacts.extend(LOCAL_PUBLISH_ARTIFACTS) + artifacts = sorted(set(artifacts)) + if not artifacts: + print("No artifacts selected; pass --artifact or --preset local-publish.", file=sys.stderr) + raise SystemExit(2) + + available = {artifact["name"]: artifact for artifact in list_ci_artifacts(args.repo, args.run_id)} + missing = [artifact for artifact in artifacts if artifact not in available] + if missing: + print(f"Run {args.run_id} is missing artifacts: {', '.join(missing)}", file=sys.stderr) + raise SystemExit(1) + if args.dry_run: + for artifact in artifacts: + row = available[artifact] + print(f"{artifact}\t{row.get('size_in_bytes', 0)}") + return + + args.destination.mkdir(parents=True, exist_ok=True) + for artifact in artifacts: + artifact_dir = args.destination / artifact + if artifact_dir.exists() and any(artifact_dir.iterdir()) and not args.force: + print(f"Skipping existing {rel(artifact_dir)}") + continue + shutil.rmtree(artifact_dir, ignore_errors=True) + artifact_dir.mkdir(parents=True, exist_ok=True) + print(f"Downloading {artifact} from {args.repo} run {args.run_id}") + run( + [ + "gh", + "run", + "download", + args.run_id, + "--repo", + args.repo, + "--name", + artifact, + "--dir", + str(artifact_dir), + ] + ) + + +def discover_files(roots: list[Path], suffixes: tuple[str, ...]) -> list[Path]: + files: list[Path] = [] + for root in roots: + if root.is_file() and root.name.endswith(suffixes): + files.append(root) + continue + if root.is_dir(): + files.extend(path for path in root.rglob("*") if path.is_file() and path.name.endswith(suffixes)) + return sorted(set(files)) + + +def write_verdaccio_config(root: Path, port: int) -> tuple[Path, bool]: + config = root / "config.yaml" + storage = root / "storage" + storage.mkdir(parents=True, exist_ok=True) + (root / "plugins").mkdir(parents=True, exist_ok=True) + text = "\n".join( + [ + f"storage: {storage}", + "auth:", + " htpasswd:", + f" file: {root / 'htpasswd'}", + "uplinks:", + " npmjs:", + " url: https://registry.npmjs.org/", + "packages:", + " '@oliphaunt/*':", + " access: $all", + " publish: $authenticated", + " unpublish: $authenticated", + " proxy: npmjs", + " '**':", + " access: $all", + " publish: $authenticated", + " unpublish: $authenticated", + " proxy: npmjs", + "middlewares:", + " audit:", + " enabled: false", + "log:", + " - {type: stdout, format: pretty, level: http}", + "", + ] + ) + previous = config.read_text(encoding="utf-8") if config.exists() else None + config.write_text(text, encoding="utf-8") + (root / "registry-url.txt").write_text(f"http://127.0.0.1:{port}\n", encoding="utf-8") + return config, previous != text + + +def stop_recorded_verdaccio(root: Path) -> None: + pid_file = root / "verdaccio.pid" + if not pid_file.is_file(): + return + try: + pid = int(pid_file.read_text(encoding="utf-8").strip()) + except ValueError: + pid_file.unlink(missing_ok=True) + return + try: + os.kill(pid, 15) + except ProcessLookupError: + pid_file.unlink(missing_ok=True) + return + for _ in range(30): + try: + os.kill(pid, 0) + except ProcessLookupError: + pid_file.unlink(missing_ok=True) + return + time.sleep(0.1) + try: + os.kill(pid, 9) + except ProcessLookupError: + pass + pid_file.unlink(missing_ok=True) + + +def npm_ping(registry_url: str) -> bool: + if not shutil.which("npm"): + return False + try: + result = run( + [ + "npm", + "ping", + "--registry", + registry_url, + "--fetch-timeout=1000", + "--fetch-retries=0", + ], + check=False, + capture=True, + timeout=3, + ) + return result.returncode == 0 + except subprocess.TimeoutExpired: + return False + + +def ensure_verdaccio(root: Path, port: int, dry_run: bool) -> str: + registry_url = f"http://127.0.0.1:{port}" + config, changed = write_verdaccio_config(root, port) + if changed and not dry_run: + stop_recorded_verdaccio(root) + if npm_ping(registry_url): + return registry_url + if dry_run: + return registry_url + + if not shutil.which("pnpm"): + raise RuntimeError("pnpm is required to start Verdaccio") + log_path = root / "verdaccio.log" + log = log_path.open("a", encoding="utf-8") + process = subprocess.Popen( + [ + "pnpm", + "dlx", + "verdaccio@6", + "--config", + str(config), + "--listen", + registry_url, + ], + cwd=ROOT, + stdout=log, + stderr=subprocess.STDOUT, + text=True, + start_new_session=True, + ) + (root / "verdaccio.pid").write_text(f"{process.pid}\n", encoding="utf-8") + for _ in range(60): + if npm_ping(registry_url): + return registry_url + if process.poll() is not None: + raise RuntimeError(f"Verdaccio exited early; see {rel(log_path)}") + time.sleep(1) + raise RuntimeError(f"Timed out waiting for Verdaccio; see {rel(log_path)}") + + +def ensure_verdaccio_npmrc(root: Path, registry_url: str, dry_run: bool) -> Path | None: + if dry_run: + return None + npmrc = root / "npmrc" + if npmrc.is_file(): + text = npmrc.read_text(encoding="utf-8") + if "always-auth" in text: + npmrc.write_text( + "\n".join(line for line in text.splitlines() if not line.startswith("always-auth=")) + "\n", + encoding="utf-8", + ) + return npmrc + username = "oliphaunt-local" + password = "oliphaunt-local" + payload = json.dumps( + { + "name": username, + "password": password, + "email": "local-registry@oliphaunt.invalid", + "type": "user", + "roles": [], + "date": time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime()), + } + ).encode("utf-8") + request = urllib.request.Request( + f"{registry_url}/-/user/org.couchdb.user:{username}", + data=payload, + method="PUT", + headers={"content-type": "application/json"}, + ) + try: + with urllib.request.urlopen(request, timeout=10) as response: + data = json.loads(response.read().decode("utf-8")) + except urllib.error.HTTPError as error: + body = error.read().decode("utf-8", errors="replace") + raise RuntimeError(f"failed to create local Verdaccio user: HTTP {error.code}: {body}") from error + token = data.get("token") + if not isinstance(token, str) or not token: + raise RuntimeError("Verdaccio did not return an auth token for the local user") + host = registry_url.removeprefix("http://").removeprefix("https://") + npmrc.write_text( + "\n".join( + [ + f"registry={registry_url}/", + f"//{host}/:_authToken={token}", + "", + ] + ), + encoding="utf-8", + ) + return npmrc + + +def publish_npm(roots: list[Path], registry_root: Path, dry_run: bool, strict: bool, port: int) -> SurfaceResult: + result = SurfaceResult("npm") + tarballs = discover_files(roots, (".tgz",)) + if not tarballs: + result.add_skip("no npm .tgz artifacts found") + if strict: + raise RuntimeError(result.skipped[-1]) + return result + + verdaccio_root = registry_root / "verdaccio" + registry_url = ensure_verdaccio(verdaccio_root, port, dry_run) + npmrc = ensure_verdaccio_npmrc(verdaccio_root, registry_url, dry_run) + result.staged.append(f"verdaccio={registry_url}") + for tarball in tarballs: + if dry_run: + result.published.append(f"dry-run npm publish {rel(tarball)}") + continue + command = [ + "npm", + "publish", + str(tarball), + "--registry", + registry_url, + "--provenance=false", + "--ignore-scripts", + "--access", + "public", + ] + if npmrc is not None: + command.extend(["--userconfig", str(npmrc)]) + run(command) + result.published.append(rel(tarball)) + return result + + +def crate_index_path(name: str) -> Path: + lower = name.lower() + if len(lower) == 1: + return Path("1") / lower + if len(lower) == 2: + return Path("2") / lower + if len(lower) == 3: + return Path("3") / lower[:1] / lower + return Path(lower[:2]) / lower[2:4] / lower + + +def cargo_metadata_for_crate(crate_path: Path) -> dict[str, Any]: + with tempfile.TemporaryDirectory(prefix="oliphaunt-crate-") as temp: + temp_path = Path(temp) + with tarfile.open(crate_path, "r:gz") as archive: + archive.extractall(temp_path, filter="data") + manifests = sorted(temp_path.glob("*/Cargo.toml")) + if not manifests: + raise RuntimeError(f"{rel(crate_path)} does not contain Cargo.toml") + cargo_toml = tomllib.loads(manifests[0].read_text(encoding="utf-8")) + metadata = run( + [ + "cargo", + "metadata", + "--manifest-path", + str(manifests[0]), + "--format-version", + "1", + "--no-deps", + ], + capture=True, + ) + package = json.loads(metadata.stdout)["packages"][0] + package["_oliphaunt_links"] = cargo_toml.get("package", {}).get("links") + return package + + +def cargo_index_dependency(dep: dict[str, Any]) -> dict[str, Any]: + registry = dep.get("registry") + return { + "name": dep["name"], + "req": dep.get("req", "*"), + "features": dep.get("features") or [], + "optional": bool(dep.get("optional")), + "default_features": bool(dep.get("uses_default_features", dep.get("default_features", True))), + "target": dep.get("target"), + "kind": dep.get("kind") or "normal", + "registry": registry, + "package": dep.get("rename") or dep.get("package"), + } + + +def cargo_index_entry(crate_path: Path) -> dict[str, Any]: + package = cargo_metadata_for_crate(crate_path) + checksum = hashlib.sha256(crate_path.read_bytes()).hexdigest() + return { + "name": package["name"], + "vers": package["version"], + "deps": [cargo_index_dependency(dep) for dep in package.get("dependencies", [])], + "features": package.get("features", {}), + "features2": None, + "cksum": checksum, + "yanked": False, + "links": package.get("_oliphaunt_links"), + "rust_version": package.get("rust_version"), + "v": 2, + } + + +def publish_cargo(roots: list[Path], registry_root: Path, dry_run: bool, strict: bool) -> SurfaceResult: + result = SurfaceResult("cargo") + crates = discover_files(roots, (".crate",)) + if not crates: + result.add_skip("no .crate artifacts found") + if strict: + raise RuntimeError(result.skipped[-1]) + return result + require_command("cargo") + + cargo_root = registry_root / "cargo" + crates_dir = cargo_root / "crates" + index_dir = cargo_root / "index" + config_snippet = cargo_root / "config.toml" + if dry_run: + result.published.extend(f"dry-run cargo index {rel(path)}" for path in crates) + return result + + shutil.rmtree(cargo_root, ignore_errors=True) + crates_dir.mkdir(parents=True, exist_ok=True) + index_dir.mkdir(parents=True, exist_ok=True) + (index_dir / "config.json").write_text( + json.dumps({"dl": f"file://{crates_dir}/{{crate}}-{{version}}.crate"}, sort_keys=True) + "\n", + encoding="utf-8", + ) + + entries_by_path: dict[Path, list[dict[str, Any]]] = {} + copied: set[str] = set() + for crate_path in crates: + try: + entry = cargo_index_entry(crate_path) + except RuntimeError as error: + result.add_skip(str(error)) + if strict: + raise + continue + target_name = f"{entry['name']}-{entry['vers']}.crate" + if target_name in copied: + continue + shutil.copy2(crate_path, crates_dir / target_name) + copied.add(target_name) + entries_by_path.setdefault(crate_index_path(entry["name"]), []).append(entry) + result.published.append(target_name) + + for path, entries in entries_by_path.items(): + target = index_dir / path + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text( + "".join(json.dumps(entry, sort_keys=True, separators=(",", ":")) + "\n" for entry in entries), + encoding="utf-8", + ) + + run(["git", "init"], cwd=index_dir) + run(["git", "config", "user.name", "Oliphaunt Local Registry"], cwd=index_dir) + run(["git", "config", "user.email", "local-registry@oliphaunt.invalid"], cwd=index_dir) + run(["git", "add", "."], cwd=index_dir) + run(["git", "commit", "-m", "local cargo registry"], cwd=index_dir) + config_snippet.write_text( + "\n".join( + [ + "[registries.oliphaunt-local]", + f'index = "file://{index_dir}"', + "", + ] + ), + encoding="utf-8", + ) + result.staged.extend([rel(index_dir), rel(config_snippet)]) + return result + + +def copy_tree_contents(source: Path, destination: Path) -> int: + copied = 0 + for path in source.rglob("*"): + if not path.is_file(): + continue + target = destination / path.relative_to(source) + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(path, target) + copied += 1 + return copied + + +def publish_maven(roots: list[Path], registry_root: Path, dry_run: bool, strict: bool) -> SurfaceResult: + result = SurfaceResult("maven") + candidates = sorted( + path + for root in roots + for path in (root.rglob("maven") if root.is_dir() else []) + if path.is_dir() + ) + if not candidates: + result.add_skip("no staged Maven repository directories named maven found") + if strict: + raise RuntimeError(result.skipped[-1]) + return result + maven_root = registry_root / "maven" + if dry_run: + result.published.extend(f"dry-run maven copy {rel(path)}" for path in candidates) + return result + shutil.rmtree(maven_root, ignore_errors=True) + maven_root.mkdir(parents=True, exist_ok=True) + for candidate in candidates: + count = copy_tree_contents(candidate, maven_root) + result.published.append(f"{rel(candidate)} ({count} files)") + result.staged.append(rel(maven_root)) + return result + + +def publish_swift(roots: list[Path], registry_root: Path, dry_run: bool, strict: bool) -> SurfaceResult: + result = SurfaceResult("swift") + swift_files = discover_files(roots, (".swift", ".zip")) + swift_files = [ + path + for path in swift_files + if path.name == "Package.swift.release" or path.name.endswith("-source.zip") or "swift" in str(path) + ] + if not swift_files: + result.add_skip("no SwiftPM package artifacts found") + if strict: + raise RuntimeError(result.skipped[-1]) + return result + if not shutil.which("swift"): + result.add_skip("swift is not installed; staged artifacts are copyable, registry publish skipped on this Linux host") + swift_root = registry_root / "swift" + if dry_run: + result.published.extend(f"dry-run swift stage {rel(path)}" for path in swift_files) + return result + shutil.rmtree(swift_root, ignore_errors=True) + swift_root.mkdir(parents=True, exist_ok=True) + for path in swift_files: + target = swift_root / path.name + shutil.copy2(path, target) + result.staged.append(rel(target)) + return result + + +def publish(args: argparse.Namespace) -> None: + roots = discover_roots(args.artifact_root) + args.registry_root.mkdir(parents=True, exist_ok=True) + surfaces = args.surface or ["npm", "cargo", "maven", "swift"] + results: list[SurfaceResult] = [] + for surface in surfaces: + if surface == "npm": + results.append(publish_npm(roots, args.registry_root, args.dry_run, args.strict, args.verdaccio_port)) + elif surface == "cargo": + results.append(publish_cargo(roots, args.registry_root, args.dry_run, args.strict)) + elif surface == "maven": + results.append(publish_maven(roots, args.registry_root, args.dry_run, args.strict)) + elif surface == "swift": + results.append(publish_swift(roots, args.registry_root, args.dry_run, args.strict)) + else: + raise RuntimeError(f"unsupported surface: {surface}") + + report = { + "registry_root": str(args.registry_root), + "artifact_roots": [str(root) for root in roots], + "dry_run": args.dry_run, + "surfaces": [result.__dict__ for result in results], + } + report_path = args.registry_root / "report.json" + if not args.dry_run: + report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(json.dumps(report, indent=2, sort_keys=True)) + + +def status(args: argparse.Namespace) -> None: + roots = discover_roots(args.artifact_root) + report = { + "default_run_id": DEFAULT_RUN_ID, + "artifact_roots": [str(root) for root in roots], + "tools": { + "cargo": bool(shutil.which("cargo")), + "gh": bool(shutil.which("gh")), + "java": bool(shutil.which("java")), + "npm": bool(shutil.which("npm")), + "pnpm": bool(shutil.which("pnpm")), + "swift": bool(shutil.which("swift")), + }, + "artifacts": { + "npm": [rel(path) for path in discover_files(roots, (".tgz",))], + "cargo": [rel(path) for path in discover_files(roots, (".crate",))], + "maven_roots": [ + rel(path) + for root in roots + for path in (root.rglob("maven") if root.is_dir() else []) + if path.is_dir() + ], + "swift": [ + rel(path) + for path in discover_files(roots, (".swift", ".zip")) + if path.name == "Package.swift.release" or "swift" in str(path) + ], + }, + } + print(json.dumps(report, indent=2, sort_keys=True)) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + subparsers = parser.add_subparsers(dest="command", required=True) + + download = subparsers.add_parser("download", help="download GitHub Actions artifacts with gh") + download.add_argument("--repo", default=DEFAULT_REPO) + download.add_argument("--run-id", default=DEFAULT_RUN_ID) + download.add_argument("--destination", type=Path, default=DEFAULT_ARTIFACT_ROOT) + download.add_argument("--artifact", action="append", default=[]) + download.add_argument("--preset", choices=["local-publish"], default=None) + download.add_argument("--force", action="store_true") + download.add_argument("--dry-run", action="store_true") + download.set_defaults(func=download_artifacts) + + publish_parser = subparsers.add_parser("publish", help="publish staged artifacts to local registries") + publish_parser.add_argument("--artifact-root", type=Path, action="append", default=[]) + publish_parser.add_argument("--registry-root", type=Path, default=DEFAULT_REGISTRY_ROOT) + publish_parser.add_argument( + "--surface", + action="append", + choices=["npm", "cargo", "maven", "swift"], + help="publish only this surface; may be repeated", + ) + publish_parser.add_argument("--verdaccio-port", type=int, default=4873) + publish_parser.add_argument("--dry-run", action="store_true") + publish_parser.add_argument("--strict", action="store_true") + publish_parser.set_defaults(func=publish) + + status_parser = subparsers.add_parser("status", help="show locally available staged artifacts") + status_parser.add_argument("--artifact-root", type=Path, action="append", default=[]) + status_parser.set_defaults(func=status) + return parser + + +def main(argv: list[str] | None = None) -> None: + parser = build_parser() + args = parser.parse_args(argv) + try: + args.func(args) + except RuntimeError as error: + print(f"local_registry_publish.py: {error}", file=sys.stderr) + raise SystemExit(1) from error + + +if __name__ == "__main__": + main() diff --git a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py index d2b472f6..f3fcd60e 100644 --- a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py +++ b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py @@ -24,6 +24,7 @@ CRATES_IO_MAX_BYTES = 10 * 1024 * 1024 RUNTIME_PACKAGE = "oliphaunt-wasix-assets" ICU_PACKAGE = "oliphaunt-icu" +ICU_PAYLOAD_ARCHIVE = "icu-data.tar.zst" AOT_PACKAGES = { "macos-arm64": "oliphaunt-wasix-aot-aarch64-apple-darwin", "linux-arm64-gnu": "oliphaunt-wasix-aot-aarch64-unknown-linux-gnu", @@ -196,6 +197,51 @@ def validate_icu_payload(root: Path) -> None: fail(f"ICU Cargo payload is missing icudt data under {rel(root)}") +def write_icu_payload_archive(root: Path, payload_root: Path) -> Path: + stage = payload_root.parent / "icu-payload-stage" + shutil.rmtree(stage, ignore_errors=True) + shutil.rmtree(payload_root, ignore_errors=True) + (stage / "share").mkdir(parents=True, exist_ok=True) + payload_root.mkdir(parents=True, exist_ok=True) + shutil.copytree(root, stage / "share/icu") + archive = payload_root / ICU_PAYLOAD_ARCHIVE + run( + [ + "tar", + "--sort=name", + "--owner=0", + "--group=0", + "--numeric-owner", + "--mtime=@0", + "--use-compress-program=zstd -19", + "-cf", + str(archive), + "-C", + str(stage), + "share/icu", + ] + ) + members = tar_zstd_members(archive) + unexpected = [] + has_icu_data = False + for member in members: + path = PurePosixPath(member) + if path == PurePosixPath("share/icu"): + continue + try: + relative = path.relative_to("share/icu") + except ValueError: + unexpected.append(member) + continue + if len(relative.parts) >= 2 and relative.parts[0].startswith("icudt"): + has_icu_data = True + if not has_icu_data: + fail(f"{rel(archive)} is missing share/icu/icudt* data") + if unexpected: + fail(f"{rel(archive)} must contain only share/icu data, found {unexpected[0]}") + return payload_root + + def validate_aot_payload(root: Path) -> None: manifest = json.loads((root / "manifest.json").read_text(encoding="utf-8")) artifacts = manifest.get("artifacts") @@ -352,14 +398,15 @@ def package_specs(asset_dir: Path, extract_root: Path, version: str) -> list[Pac extract_tar_zstd(icu_archive, icu_extract) icu_root = canonical_icu_root(target_icu_root(icu_extract)) validate_icu_payload(icu_root) + icu_payload_root = write_icu_payload_archive(icu_root, extract_root / "icu-payload") specs.append( PackageSpec( name=ICU_PACKAGE, target="portable", kind="icu-data", template_dir=ROOT / "src/runtimes/liboliphaunt/icu", - payload_root=icu_root, - payload_dir_name="payload/share/icu", + payload_root=icu_payload_root, + payload_dir_name="payload", ) ) From c9f76cd8289bc268629c266b674d68510328d190 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Wed, 24 Jun 2026 12:58:11 +0000 Subject: [PATCH 002/137] feat: install native extensions from registry packages --- examples/electron-wasix/package.json | 1 + examples/electron-wasix/src/todos.ts | 196 +++--- examples/electron/package.json | 3 +- examples/electron/src/oliphaunt-kysely.ts | 135 ++++ examples/electron/src/todos.ts | 218 ++++--- pnpm-lock.yaml | 12 + src/sdks/js/README.md | 14 + src/sdks/js/src/native/assets-node.ts | 342 +++++++++- src/sdks/js/src/native/bun.ts | 21 +- src/sdks/js/src/native/common.ts | 13 +- src/sdks/js/src/native/node.ts | 17 +- src/sdks/js/src/native/types.ts | 1 + src/sdks/js/src/runtime/broker.ts | 30 +- src/sdks/js/src/runtime/direct.ts | 1 + src/sdks/rust/src/liboliphaunt/ffi.rs | 1 + .../src/liboliphaunt/root/runtime/locate.rs | 33 +- tools/release/local_registry_publish.py | 600 +++++++++++++++++- 17 files changed, 1445 insertions(+), 193 deletions(-) create mode 100644 examples/electron/src/oliphaunt-kysely.ts diff --git a/examples/electron-wasix/package.json b/examples/electron-wasix/package.json index 99e3905c..35c3a854 100644 --- a/examples/electron-wasix/package.json +++ b/examples/electron-wasix/package.json @@ -9,6 +9,7 @@ "dev:renderer": "vite" }, "dependencies": { + "kysely": "^0.29.2", "pg": "^8.16.3" }, "devDependencies": { diff --git a/examples/electron-wasix/src/todos.ts b/examples/electron-wasix/src/todos.ts index 181c4170..40ce9e83 100644 --- a/examples/electron-wasix/src/todos.ts +++ b/examples/electron-wasix/src/todos.ts @@ -1,11 +1,40 @@ import { join } from "node:path"; +import { Kysely, PostgresDialect, sql, type Generated } from "kysely"; import pg from "pg"; -import type { CreateTodoInput, StatusFilter, Todo } from "./types.js"; + import { startWasixSidecar, type WasixSidecar } from "./sidecar.js"; +import type { CreateTodoInput, StatusFilter, Todo } from "./types.js"; const { Pool } = pg; +type TodoTable = { + id: Generated; + title: string; + notes: string; + tags: string; + done: Generated; + priority: number; + created_at: Generated; + updated_at: Generated; +}; + +type TodoDatabase = { + todos: TodoTable; +}; + +type TodoRecord = { + id: string; + title: string; + notes: string; + area: string; + context: string; + done: string; + priority: string; + created_at: string; + updated_at: string; +}; + const schemaStatements = [ "CREATE EXTENSION IF NOT EXISTS hstore", "CREATE EXTENSION IF NOT EXISTS pg_trgm", @@ -23,49 +52,8 @@ const schemaStatements = [ "CREATE INDEX IF NOT EXISTS todos_title_trgm ON todos USING gin (title gin_trgm_ops)", ]; -const selectTodos = ` -SELECT - id, - title, - notes, - COALESCE(tags -> 'area', '') AS area, - COALESCE(tags -> 'context', '') AS context, - done, - priority, - to_char(created_at, 'YYYY-MM-DD HH24:MI') AS created_at, - to_char(updated_at, 'YYYY-MM-DD HH24:MI') AS updated_at -FROM todos -WHERE - ( - $1::text = '' - OR unaccent(title || ' ' || notes) ILIKE '%' || unaccent($1::text) || '%' - OR COALESCE(tags -> 'area', '') ILIKE '%' || $1::text || '%' - OR COALESCE(tags -> 'context', '') ILIKE '%' || $1::text || '%' - OR tags ? $1::text - ) - AND ( - $2::text = 'all' - OR ($2::text = 'open' AND NOT done) - OR ($2::text = 'done' AND done) - ) -ORDER BY done ASC, priority ASC, updated_at DESC, id DESC -`; - -const returningTodo = ` -RETURNING - id, - title, - notes, - COALESCE(tags -> 'area', '') AS area, - COALESCE(tags -> 'context', '') AS context, - done, - priority, - to_char(created_at, 'YYYY-MM-DD HH24:MI') AS created_at, - to_char(updated_at, 'YYYY-MM-DD HH24:MI') AS updated_at -`; - type Store = { - pool: pg.Pool; + db: Kysely; sidecar: WasixSidecar; }; @@ -78,73 +66,123 @@ async function getStore(userData: string) { async function openStore(userData: string): Promise { const sidecar = await startWasixSidecar(join(userData, "oliphaunt-wasix-todos")); - const pool = new Pool({ - connectionString: sidecar.databaseUrl, - max: 1, + const db = new Kysely({ + dialect: new PostgresDialect({ + pool: new Pool({ + connectionString: sidecar.databaseUrl, + max: 1, + }), + }), }); for (const statement of schemaStatements) { - await pool.query(statement); + await sql.raw(statement).execute(db); } - return { pool, sidecar }; + return { db, sidecar }; } export async function listTodos( userData: string, filter: { search: string; status: StatusFilter }, ) { - const { pool } = await getStore(userData); - const result = await pool.query(selectTodos, [filter.search, filter.status]); - return result.rows.map(todoFromRow); + const { db } = await getStore(userData); + const rows = await db + .selectFrom("todos") + .select(todoColumns) + .where(searchPredicate(filter.search)) + .where(statusPredicate(filter.status)) + .orderBy("done", "asc") + .orderBy("priority", "asc") + .orderBy("updated_at", "desc") + .orderBy("id", "desc") + .execute(); + return rows.map(todoFromRow); } export async function createTodo(userData: string, input: CreateTodoInput) { - const { pool } = await getStore(userData); - const result = await pool.query( - `INSERT INTO todos (title, notes, tags, priority) - VALUES ($1, $2, hstore(ARRAY['area', $3, 'context', $4]), $5) - ${returningTodo}`, - [input.title, input.notes, input.area, input.context, clampPriority(input.priority)], - ); - return oneTodo(result.rows); + const { db } = await getStore(userData); + const row = await db + .insertInto("todos") + .values({ + title: input.title, + notes: input.notes, + tags: sql`hstore(ARRAY['area', ${input.area}, 'context', ${input.context}])`, + priority: clampPriority(input.priority), + }) + .returning(todoColumns) + .executeTakeFirstOrThrow(); + return todoFromRow(row); } export async function toggleTodo(userData: string, id: number) { - const { pool } = await getStore(userData); - const result = await pool.query( - `UPDATE todos SET done = NOT done, updated_at = now() WHERE id = $1 ${returningTodo}`, - [id], - ); - return oneTodo(result.rows); + const { db } = await getStore(userData); + const row = await db + .updateTable("todos") + .set({ + done: sql`NOT done`, + updated_at: sql`now()`, + }) + .where("id", "=", String(id)) + .returning(todoColumns) + .executeTakeFirstOrThrow(); + return todoFromRow(row); } export async function deleteTodo(userData: string, id: number) { - const { pool } = await getStore(userData); - await pool.query("DELETE FROM todos WHERE id = $1", [id]); + const { db } = await getStore(userData); + await db.deleteFrom("todos").where("id", "=", String(id)).execute(); } export async function closeStore() { if (!storePromise) return; const store = await storePromise; - await store.pool.end(); + await store.db.destroy(); store.sidecar.process.kill(); + storePromise = undefined; +} + +function todoColumns() { + return [ + sql`id::text`.as("id"), + "title", + "notes", + sql`COALESCE(tags -> 'area', '')`.as("area"), + sql`COALESCE(tags -> 'context', '')`.as("context"), + sql`done::text`.as("done"), + sql`priority::text`.as("priority"), + sql`to_char(created_at, 'YYYY-MM-DD HH24:MI')`.as("created_at"), + sql`to_char(updated_at, 'YYYY-MM-DD HH24:MI')`.as("updated_at"), + ] as const; +} + +function searchPredicate(search: string) { + return sql`( + ${search}::text = '' + OR unaccent(title || ' ' || notes) ILIKE '%' || unaccent(${search}::text) || '%' + OR COALESCE(tags -> 'area', '') ILIKE '%' || ${search}::text || '%' + OR COALESCE(tags -> 'context', '') ILIKE '%' || ${search}::text || '%' + OR tags ? ${search}::text + )`; } -function oneTodo(rows: unknown[]) { - if (rows.length === 0) throw new Error("todo was not returned"); - return todoFromRow(rows[0] as pg.QueryResultRow); +function statusPredicate(status: StatusFilter) { + return sql`( + ${status}::text = 'all' + OR (${status}::text = 'open' AND NOT done) + OR (${status}::text = 'done' AND done) + )`; } -function todoFromRow(row: pg.QueryResultRow): Todo { +function todoFromRow(row: TodoRecord): Todo { return { id: Number(row.id), - title: String(row.title), - notes: String(row.notes), - area: String(row.area), - context: String(row.context), + title: row.title, + notes: row.notes, + area: row.area, + context: row.context, priority: Number(row.priority), - done: Boolean(row.done), - createdAt: String(row.created_at), - updatedAt: String(row.updated_at), + done: row.done === "true", + createdAt: row.created_at, + updatedAt: row.updated_at, }; } diff --git a/examples/electron/package.json b/examples/electron/package.json index 8aee4d13..631140c7 100644 --- a/examples/electron/package.json +++ b/examples/electron/package.json @@ -10,7 +10,8 @@ "dev:renderer": "vite" }, "dependencies": { - "@oliphaunt/ts": "workspace:*" + "@oliphaunt/ts": "workspace:*", + "kysely": "^0.29.2" }, "devDependencies": { "@types/node": "^24.10.1", diff --git a/examples/electron/src/oliphaunt-kysely.ts b/examples/electron/src/oliphaunt-kysely.ts new file mode 100644 index 00000000..071ca89d --- /dev/null +++ b/examples/electron/src/oliphaunt-kysely.ts @@ -0,0 +1,135 @@ +import { + CompiledQuery, + PostgresAdapter, + PostgresIntrospector, + PostgresQueryCompiler, + type AbortableOperationOptions, + type DatabaseConnection, + type DatabaseIntrospector, + type Dialect, + type DialectAdapter, + type Driver, + type Kysely, + type QueryCompiler, + type QueryResult as KyselyQueryResult, + type TransactionSettings, +} from "kysely"; + +import type { OliphauntDatabase, QueryParam } from "@oliphaunt/ts"; + +export class OliphauntDialect implements Dialect { + constructor(private readonly db: OliphauntDatabase) {} + + createDriver(): Driver { + return new OliphauntDriver(this.db); + } + + createQueryCompiler(): QueryCompiler { + return new PostgresQueryCompiler(); + } + + createAdapter(): DialectAdapter { + return new PostgresAdapter(); + } + + createIntrospector(db: Kysely): DatabaseIntrospector { + return new PostgresIntrospector(db); + } +} + +class OliphauntDriver implements Driver { + private readonly connection: OliphauntConnection; + + constructor(db: OliphauntDatabase) { + this.connection = new OliphauntConnection(db); + } + + async init(_options?: AbortableOperationOptions): Promise {} + + async acquireConnection(_options?: AbortableOperationOptions): Promise { + return this.connection; + } + + async beginTransaction( + connection: DatabaseConnection, + settings: TransactionSettings, + ): Promise { + let statement = "begin"; + if (settings.isolationLevel || settings.accessMode) { + statement = "start transaction"; + if (settings.isolationLevel) statement += ` isolation level ${settings.isolationLevel}`; + if (settings.accessMode) statement += ` ${settings.accessMode}`; + } + await connection.executeQuery(CompiledQuery.raw(statement)); + } + + async commitTransaction(connection: DatabaseConnection): Promise { + await connection.executeQuery(CompiledQuery.raw("commit")); + } + + async rollbackTransaction(connection: DatabaseConnection): Promise { + await connection.executeQuery(CompiledQuery.raw("rollback")); + } + + async releaseConnection( + _connection: DatabaseConnection, + _options?: AbortableOperationOptions, + ): Promise {} + + async destroy(_options?: AbortableOperationOptions): Promise {} +} + +class OliphauntConnection implements DatabaseConnection { + constructor(private readonly db: OliphauntDatabase) {} + + async executeQuery(compiledQuery: CompiledQuery): Promise> { + const result = await this.db.query( + compiledQuery.sql, + compiledQuery.parameters.map(toQueryParam), + ); + const rows = result.rows.map((_, rowIndex) => { + const row: Record = {}; + for (const field of result.fields) { + row[field.name] = result.getText(rowIndex, field.name); + } + return row as R; + }); + return { + numAffectedRows: affectedRows(result.commandTag), + rows, + }; + } + + async *streamQuery( + _compiledQuery: CompiledQuery, + _chunkSize: number, + _options?: AbortableOperationOptions, + ): AsyncIterableIterator> { + throw new Error("Streaming is not supported by the Oliphaunt Kysely example dialect."); + } +} + +function toQueryParam(value: unknown): QueryParam { + if ( + value === null || + typeof value === "string" || + typeof value === "number" || + typeof value === "boolean" + ) { + return value; + } + if (value instanceof Uint8Array || value instanceof ArrayBuffer || ArrayBuffer.isView(value)) { + return value; + } + throw new Error(`unsupported Oliphaunt query parameter: ${typeof value}`); +} + +function affectedRows(commandTag: string | undefined): bigint | undefined { + if (!commandTag) return undefined; + const command = commandTag.split(/\s+/, 1)[0]; + if (command !== "INSERT" && command !== "UPDATE" && command !== "DELETE" && command !== "MERGE") { + return undefined; + } + const count = Number(commandTag.trim().split(/\s+/).at(-1)); + return Number.isFinite(count) ? BigInt(count) : undefined; +} diff --git a/examples/electron/src/todos.ts b/examples/electron/src/todos.ts index 462dbbd3..adaa5e2f 100644 --- a/examples/electron/src/todos.ts +++ b/examples/electron/src/todos.ts @@ -1,8 +1,43 @@ import { join } from "node:path"; -import { Oliphaunt, type OliphauntDatabase, type QueryResult } from "@oliphaunt/ts"; +import { Oliphaunt, type OliphauntDatabase } from "@oliphaunt/ts"; +import { Kysely, sql, type Generated } from "kysely"; + +import { OliphauntDialect } from "./oliphaunt-kysely.js"; import type { CreateTodoInput, StatusFilter, Todo } from "./types.js"; +type TodoTable = { + id: Generated; + title: string; + notes: string; + tags: string; + done: Generated; + priority: number; + created_at: Generated; + updated_at: Generated; +}; + +type TodoDatabase = { + todos: TodoTable; +}; + +type TodoRecord = { + id: string; + title: string; + notes: string; + area: string; + context: string; + done: string; + priority: string; + created_at: string; + updated_at: string; +}; + +type Store = { + native: OliphauntDatabase; + db: Kysely; +}; + const schemaStatements = [ "CREATE EXTENSION IF NOT EXISTS hstore", "CREATE EXTENSION IF NOT EXISTS pg_trgm", @@ -20,133 +55,132 @@ const schemaStatements = [ "CREATE INDEX IF NOT EXISTS todos_title_trgm ON todos USING gin (title gin_trgm_ops)", ]; -const selectTodos = ` -SELECT - id::text AS id, - title, - notes, - COALESCE(tags -> 'area', '') AS area, - COALESCE(tags -> 'context', '') AS context, - done::text AS done, - priority::text AS priority, - to_char(created_at, 'YYYY-MM-DD HH24:MI') AS created_at, - to_char(updated_at, 'YYYY-MM-DD HH24:MI') AS updated_at -FROM todos -WHERE - ( - $1::text = '' - OR unaccent(title || ' ' || notes) ILIKE '%' || unaccent($1::text) || '%' - OR COALESCE(tags -> 'area', '') ILIKE '%' || $1::text || '%' - OR COALESCE(tags -> 'context', '') ILIKE '%' || $1::text || '%' - OR tags ? $1::text - ) - AND ( - $2::text = 'all' - OR ($2::text = 'open' AND NOT done) - OR ($2::text = 'done' AND done) - ) -ORDER BY done ASC, priority ASC, updated_at DESC, id DESC -`; - -const returningTodo = ` -RETURNING - id::text AS id, - title, - notes, - COALESCE(tags -> 'area', '') AS area, - COALESCE(tags -> 'context', '') AS context, - done::text AS done, - priority::text AS priority, - to_char(created_at, 'YYYY-MM-DD HH24:MI') AS created_at, - to_char(updated_at, 'YYYY-MM-DD HH24:MI') AS updated_at -`; - -let dbPromise: Promise | undefined; +let storePromise: Promise | undefined; export function getDatabase(userData: string) { - dbPromise ??= openDatabase(userData); - return dbPromise; + storePromise ??= openDatabase(userData); + return storePromise; } -async function openDatabase(userData: string) { - const db = await Oliphaunt.open({ +async function openDatabase(userData: string): Promise { + const native = await Oliphaunt.open({ engine: "nativeBroker", root: join(userData, "oliphaunt-native-todos"), extensions: ["hstore", "pg_trgm", "unaccent"], }); + const db = new Kysely({ + dialect: new OliphauntDialect(native), + }); for (const statement of schemaStatements) { - await db.execute(statement); + await sql.raw(statement).execute(db); } - return db; + return { native, db }; } export async function listTodos( userData: string, filter: { search: string; status: StatusFilter }, ) { - const db = await getDatabase(userData); - const result = await db.query(selectTodos, [filter.search, filter.status]); - return todosFromResult(result); + const { db } = await getDatabase(userData); + const rows = await db + .selectFrom("todos") + .select(todoColumns) + .where(searchPredicate(filter.search)) + .where(statusPredicate(filter.status)) + .orderBy("done", "asc") + .orderBy("priority", "asc") + .orderBy("updated_at", "desc") + .orderBy("id", "desc") + .execute(); + return rows.map(todoFromRow); } export async function createTodo(userData: string, input: CreateTodoInput) { - const db = await getDatabase(userData); - const result = await db.query( - `INSERT INTO todos (title, notes, tags, priority) - VALUES ($1, $2, hstore(ARRAY['area', $3, 'context', $4]), $5) - ${returningTodo}`, - [input.title, input.notes, input.area, input.context, clampPriority(input.priority)], - ); - return oneTodo(result); + const { db } = await getDatabase(userData); + const row = await db + .insertInto("todos") + .values({ + title: input.title, + notes: input.notes, + tags: sql`hstore(ARRAY['area', ${input.area}, 'context', ${input.context}])`, + priority: clampPriority(input.priority), + }) + .returning(todoColumns) + .executeTakeFirstOrThrow(); + return todoFromRow(row); } export async function toggleTodo(userData: string, id: number) { - const db = await getDatabase(userData); - const result = await db.query( - `UPDATE todos SET done = NOT done, updated_at = now() WHERE id = $1 ${returningTodo}`, - [id], - ); - return oneTodo(result); + const { db } = await getDatabase(userData); + const row = await db + .updateTable("todos") + .set({ + done: sql`NOT done`, + updated_at: sql`now()`, + }) + .where("id", "=", String(id)) + .returning(todoColumns) + .executeTakeFirstOrThrow(); + return todoFromRow(row); } export async function deleteTodo(userData: string, id: number) { - const db = await getDatabase(userData); - await db.query("DELETE FROM todos WHERE id = $1", [id]); + const { db } = await getDatabase(userData); + await db.deleteFrom("todos").where("id", "=", String(id)).execute(); } export async function closeDatabase() { - if (!dbPromise) return; - const db = await dbPromise; - await db.close(); + if (!storePromise) return; + const store = await storePromise; + await store.db.destroy(); + await store.native.close(); + storePromise = undefined; } -function todosFromResult(result: QueryResult) { - return Array.from({ length: result.rowCount }, (_, index) => todoFromResult(result, index)); +function todoColumns() { + return [ + sql`id::text`.as("id"), + "title", + "notes", + sql`COALESCE(tags -> 'area', '')`.as("area"), + sql`COALESCE(tags -> 'context', '')`.as("context"), + sql`done::text`.as("done"), + sql`priority::text`.as("priority"), + sql`to_char(created_at, 'YYYY-MM-DD HH24:MI')`.as("created_at"), + sql`to_char(updated_at, 'YYYY-MM-DD HH24:MI')`.as("updated_at"), + ] as const; } -function oneTodo(result: QueryResult) { - if (result.rowCount === 0) throw new Error("todo was not returned"); - return todoFromResult(result, 0); +function searchPredicate(search: string) { + return sql`( + ${search}::text = '' + OR unaccent(title || ' ' || notes) ILIKE '%' || unaccent(${search}::text) || '%' + OR COALESCE(tags -> 'area', '') ILIKE '%' || ${search}::text || '%' + OR COALESCE(tags -> 'context', '') ILIKE '%' || ${search}::text || '%' + OR tags ? ${search}::text + )`; } -function todoFromResult(result: QueryResult, row: number): Todo { - return { - id: Number(required(result, row, "id")), - title: required(result, row, "title"), - notes: required(result, row, "notes"), - area: required(result, row, "area"), - context: required(result, row, "context"), - priority: Number(required(result, row, "priority")), - done: required(result, row, "done") === "true", - createdAt: required(result, row, "created_at"), - updatedAt: required(result, row, "updated_at"), - }; +function statusPredicate(status: StatusFilter) { + return sql`( + ${status}::text = 'all' + OR (${status}::text = 'open' AND NOT done) + OR (${status}::text = 'done' AND done) + )`; } -function required(result: QueryResult, row: number, column: string) { - const value = result.getText(row, column); - if (value === null) throw new Error(`missing ${column}`); - return value; +function todoFromRow(row: TodoRecord): Todo { + return { + id: Number(row.id), + title: row.title, + notes: row.notes, + area: row.area, + context: row.context, + priority: Number(row.priority), + done: row.done === "true", + createdAt: row.created_at, + updatedAt: row.updated_at, + }; } function clampPriority(value: number) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0be54297..b2c3bc4f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,6 +31,9 @@ importers: '@oliphaunt/ts': specifier: workspace:* version: link:../../src/sdks/js + kysely: + specifier: ^0.29.2 + version: 0.29.2 devDependencies: '@types/node': specifier: ^24.10.1 @@ -47,6 +50,9 @@ importers: examples/electron-wasix: dependencies: + kysely: + specifier: ^0.29.2 + version: 0.29.2 pg: specifier: ^8.16.3 version: 8.22.0 @@ -4743,6 +4749,10 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} + kysely@0.29.2: + resolution: {integrity: sha512-s6WVJyEZrbm6jhBpiKHsGHyePMrVQKJ85wZCFCr9W4QHv6WTjWIrdvTmO9hDEA3bNK0xkrE2DqrHsXMLWuZpQg==} + engines: {node: '>=22.0.0'} + lan-network@0.2.1: resolution: {integrity: sha512-ONPnazC96VKDntab9j9JKwIWhZ4ZUceB4A9Epu4Ssg0hYFmtHZSeQ+n15nIwTFmcBUKtExOer8WTJ4GF9MO64A==} hasBin: true @@ -11721,6 +11731,8 @@ snapshots: kleur@3.0.3: {} + kysely@0.29.2: {} + lan-network@0.2.1: {} leven@3.1.0: {} diff --git a/src/sdks/js/README.md b/src/sdks/js/README.md index c1f76539..6fb7bcd6 100644 --- a/src/sdks/js/README.md +++ b/src/sdks/js/README.md @@ -62,6 +62,20 @@ and set the runtime ICU data environment before opening liboliphaunt. Do not add `@oliphaunt/icu` for applications that do not use ICU collations. JSR remains protocol/query-only and does not expose native runtime or ICU packages. +PostgreSQL extensions follow the same registry-driven model. Applications add +the extension meta package for every extension they pass to +`Oliphaunt.open({ extensions })`; that package installs the matching target +payload as an optional dependency. + +```sh +pnpm add @oliphaunt/extension-hstore @oliphaunt/extension-pg-trgm +``` + +At startup the SDK resolves the current platform package, validates that it was +built for the same liboliphaunt version as `@oliphaunt/ts`, and materializes a +runtime tree containing the selected extension SQL files and native modules. +Do not copy extension release assets into the application bundle by hand. + ## Compatibility | Package | Compatible release | diff --git a/src/sdks/js/src/native/assets-node.ts b/src/sdks/js/src/native/assets-node.ts index 744c35b2..a4c77232 100644 --- a/src/sdks/js/src/native/assets-node.ts +++ b/src/sdks/js/src/native/assets-node.ts @@ -1,7 +1,8 @@ +import { createHash } from 'node:crypto'; +import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; -import { arch, platform } from 'node:os'; +import { arch, platform, tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; -import { readdir, readFile, stat } from 'node:fs/promises'; import { liboliphauntPackageTarget, @@ -9,11 +10,13 @@ import { resolveExplicitLibraryPath, resolveExplicitRuntimeDirectory, } from './common.js'; +import { generatedExtensionBySqlName } from '../generated/extensions.js'; export type ResolvedNativeInstall = { libraryPath: string; runtimeDirectory?: string; icuDataDirectory?: string; + moduleDirectory?: string; }; type PackageMetadata = { @@ -46,6 +49,22 @@ type IcuPackageMetadata = { }; }; +type ExtensionPackageMetadata = { + name?: string; + version?: string; + oliphaunt?: { + product?: string; + kind?: string; + sqlName?: string; + target?: string; + runtimeRelativePath?: string; + moduleRelativePath?: string; + liboliphauntVersion?: string; + targetPackageNames?: Record; + payloadPackageNames?: string[]; + }; +}; + const require = createRequire(import.meta.url); export async function resolveNodeNativeInstall( @@ -66,6 +85,81 @@ export async function resolveNodeNativeInstall( return resolvePackageNativeInstall(target, versions.liboliphauntVersion, icuDataDirectory); } +export async function materializeNodeExtensionInstall( + install: ResolvedNativeInstall, + extensions: ReadonlyArray, +): Promise { + const selected = selectedExtensionClosure(extensions); + if (selected.length === 0) { + return install; + } + if (install.runtimeDirectory === undefined) { + throw new Error( + `native extension packages require a package-managed runtime directory; selected extensions: ${selected.join(', ')}`, + ); + } + + const versions = await packageVersions(); + const target = liboliphauntPackageTarget(platform(), arch()); + const packages = await Promise.all( + selected.map((sqlName) => resolveExtensionPackage(sqlName, target.id, versions.liboliphauntVersion)), + ); + const cacheKey = runtimeCacheKey({ + libraryPath: install.libraryPath, + runtimeDirectory: install.runtimeDirectory, + target: target.id, + packages: packages.map((entry) => ({ + name: entry.name, + version: entry.version, + runtimeDirectories: entry.runtimeDirectories, + moduleDirectories: entry.moduleDirectories, + })), + }); + const root = join(tmpdir(), 'oliphaunt-js-runtime-cache', cacheKey); + const runtimeDirectory = join(root, 'runtime'); + const moduleDirectory = join(root, 'modules'); + const marker = join(root, 'manifest.json'); + const manifest = JSON.stringify( + { + runtimeDirectory: install.runtimeDirectory, + libraryPath: install.libraryPath, + target: target.id, + packages: packages.map((entry) => ({ + name: entry.name, + version: entry.version, + sqlName: entry.sqlName, + })), + }, + null, + 2, + ); + if ((await optionalRead(marker)) === manifest) { + return { ...install, runtimeDirectory, moduleDirectory }; + } + + await rm(root, { force: true, recursive: true }); + await mkdir(root, { recursive: true }); + await cp(install.runtimeDirectory, runtimeDirectory, { recursive: true }); + await mkdir(moduleDirectory, { recursive: true }); + for (const source of nativeModuleDirectoryCandidates(install.libraryPath)) { + if (await isDirectory(source)) { + await cp(source, moduleDirectory, { force: true, recursive: true }); + } + } + for (const entry of packages) { + for (const source of entry.runtimeDirectories) { + await cp(source, runtimeDirectory, { force: true, recursive: true }); + } + for (const source of entry.moduleDirectories) { + if (await isDirectory(source)) { + await cp(source, moduleDirectory, { force: true, recursive: true }); + } + } + } + await writeFile(marker, manifest, 'utf8'); + return { ...install, runtimeDirectory, moduleDirectory }; +} + export async function resolveNodeIcuDataDirectory( expectedVersion?: string, packageName?: string, @@ -126,6 +220,150 @@ async function packageVersions(): Promise<{ return { liboliphauntVersion, icuPackage, icuVersion }; } +type ResolvedExtensionPackage = { + name: string; + version: string; + sqlName: string; + runtimeDirectories: string[]; + moduleDirectories: string[]; +}; + +async function resolveExtensionPackage( + sqlName: string, + target: string, + liboliphauntVersion: string, +): Promise { + const packageName = extensionPackageName(sqlName); + const targetPackageName = extensionTargetPackageName(sqlName, target); + const packageJsonPath = await resolveExtensionTargetPackageJson( + packageName, + targetPackageName, + sqlName, + target, + ); + const packageRoot = dirname(packageJsonPath); + const packageJson = JSON.parse( + await readFile(packageJsonPath, 'utf8'), + ) as ExtensionPackageMetadata; + const expectedProduct = `oliphaunt-extension-${sqlName.replaceAll('_', '-')}`; + if (packageJson.name !== targetPackageName) { + throw new Error( + `${targetPackageName} package metadata has name ${packageJson.name ?? ''}`, + ); + } + if (packageJson.oliphaunt?.kind !== 'exact-extension-target') { + throw new Error( + `${targetPackageName} package metadata does not declare an exact Oliphaunt extension target`, + ); + } + if (packageJson.oliphaunt?.product !== expectedProduct) { + throw new Error(`${targetPackageName} package metadata does not declare ${expectedProduct}`); + } + if (packageJson.oliphaunt?.sqlName !== sqlName) { + throw new Error(`${targetPackageName} package metadata does not declare SQL extension ${sqlName}`); + } + if (packageJson.oliphaunt?.target !== target) { + throw new Error(`${targetPackageName} package metadata does not target ${target}`); + } + if (packageJson.oliphaunt?.liboliphauntVersion !== liboliphauntVersion) { + throw new Error( + `${targetPackageName} liboliphauntVersion ${packageJson.oliphaunt?.liboliphauntVersion ?? ''} does not match @oliphaunt/ts liboliphauntVersion ${liboliphauntVersion}`, + ); + } + if (packageJson.version === undefined || packageJson.version.length === 0) { + throw new Error(`${targetPackageName} package metadata is missing version`); + } + const runtimeDirectories: string[] = []; + const moduleDirectories: string[] = []; + const payloadPackageNames = packageJson.oliphaunt.payloadPackageNames ?? []; + if (payloadPackageNames.length > 0) { + for (const payloadPackageName of payloadPackageNames) { + const payload = await resolveExtensionPayloadPackage( + payloadPackageName, + packageJsonPath, + expectedProduct, + sqlName, + target, + liboliphauntVersion, + ); + runtimeDirectories.push(payload.runtimeDirectory); + if (payload.moduleDirectory !== undefined) { + moduleDirectories.push(payload.moduleDirectory); + } + } + } else { + const runtimeDirectory = join(packageRoot, packageJson.oliphaunt.runtimeRelativePath ?? 'runtime'); + await requireDirectory(runtimeDirectory, `${targetPackageName} extension runtime directory`); + runtimeDirectories.push(runtimeDirectory); + const moduleRelativePath = packageJson.oliphaunt.moduleRelativePath; + const moduleDirectory = + moduleRelativePath === undefined ? undefined : join(packageRoot, moduleRelativePath); + if (moduleDirectory !== undefined) { + await requireDirectory(moduleDirectory, `${targetPackageName} extension module directory`); + moduleDirectories.push(moduleDirectory); + } + } + return { + name: targetPackageName, + version: packageJson.version, + sqlName, + runtimeDirectories, + moduleDirectories, + }; +} + +async function resolveExtensionPayloadPackage( + packageName: string, + targetPackageJsonPath: string, + expectedProduct: string, + sqlName: string, + target: string, + liboliphauntVersion: string, +): Promise<{ runtimeDirectory: string; moduleDirectory?: string }> { + let packageJsonPath: string; + try { + packageJsonPath = createRequire(targetPackageJsonPath).resolve(`${packageName}/package.json`); + } catch (error) { + throw new Error( + `${packageName} is not installed; reinstall ${extensionPackageName(sqlName)} with optional dependencies enabled`, + { cause: error }, + ); + } + const packageRoot = dirname(packageJsonPath); + const packageJson = JSON.parse( + await readFile(packageJsonPath, 'utf8'), + ) as ExtensionPackageMetadata; + if (packageJson.name !== packageName) { + throw new Error(`${packageName} package metadata has name ${packageJson.name ?? ''}`); + } + if (packageJson.oliphaunt?.kind !== 'exact-extension-payload') { + throw new Error(`${packageName} package metadata does not declare an exact extension payload`); + } + if (packageJson.oliphaunt?.product !== expectedProduct) { + throw new Error(`${packageName} package metadata does not declare ${expectedProduct}`); + } + if (packageJson.oliphaunt?.sqlName !== sqlName) { + throw new Error(`${packageName} package metadata does not declare SQL extension ${sqlName}`); + } + if (packageJson.oliphaunt?.target !== target) { + throw new Error(`${packageName} package metadata does not target ${target}`); + } + if (packageJson.oliphaunt?.liboliphauntVersion !== liboliphauntVersion) { + throw new Error( + `${packageName} liboliphauntVersion ${packageJson.oliphaunt?.liboliphauntVersion ?? ''} does not match @oliphaunt/ts liboliphauntVersion ${liboliphauntVersion}`, + ); + } + const runtimeDirectory = join(packageRoot, packageJson.oliphaunt.runtimeRelativePath ?? 'runtime'); + await requireDirectory(runtimeDirectory, `${packageName} extension runtime directory`); + const moduleRelativePath = packageJson.oliphaunt.moduleRelativePath; + const moduleDirectory = + moduleRelativePath === undefined ? undefined : join(packageRoot, moduleRelativePath); + if (moduleDirectory !== undefined) { + await requireDirectory(moduleDirectory, `${packageName} extension module directory`); + } + return { runtimeDirectory, moduleDirectory }; +} + async function resolvePackageNativeInstall( target: NativePackageTarget, expectedVersion: string, @@ -173,6 +411,56 @@ function resolvePackageJson(packageName: string): string { } } +async function resolveExtensionTargetPackageJson( + packageName: string, + targetPackageName: string, + sqlName: string, + target: string, +): Promise { + const packageJsonPath = optionalResolvePackageJson(packageName); + if (packageJsonPath === undefined) { + return resolveExtensionPackageJson(targetPackageName, packageName); + } + + const packageJson = JSON.parse( + await readFile(packageJsonPath, 'utf8'), + ) as ExtensionPackageMetadata; + const expectedProduct = `oliphaunt-extension-${sqlName.replaceAll('_', '-')}`; + if (packageJson.name !== packageName) { + throw new Error(`${packageName} package metadata has name ${packageJson.name ?? ''}`); + } + if (packageJson.oliphaunt?.kind !== 'exact-extension') { + throw new Error(`${packageName} package metadata does not declare an exact Oliphaunt extension`); + } + if (packageJson.oliphaunt?.product !== expectedProduct) { + throw new Error(`${packageName} package metadata does not declare ${expectedProduct}`); + } + if (packageJson.oliphaunt?.sqlName !== sqlName) { + throw new Error(`${packageName} package metadata does not declare SQL extension ${sqlName}`); + } + const resolvedTargetPackageName = + packageJson.oliphaunt.targetPackageNames?.[target] ?? targetPackageName; + try { + return createRequire(packageJsonPath).resolve(`${resolvedTargetPackageName}/package.json`); + } catch (error) { + throw new Error( + `${resolvedTargetPackageName} is not installed; reinstall ${packageName} with optional dependencies enabled`, + { cause: error }, + ); + } +} + +function resolveExtensionPackageJson(packageName: string, installPackageName: string): string { + try { + return require.resolve(`${packageName}/package.json`); + } catch (error) { + throw new Error( + `${installPackageName} is not installed; add it to the application dependencies for CREATE EXTENSION support`, + { cause: error }, + ); + } +} + function optionalResolvePackageJson(packageName: string): string | undefined { try { return require.resolve(`${packageName}/package.json`); @@ -199,6 +487,14 @@ async function requireDirectory(path: string, source: string): Promise { throw new Error(`${source} does not point to an existing directory: ${path}`); } +async function isDirectory(path: string): Promise { + try { + return (await stat(path)).isDirectory(); + } catch { + return false; + } +} + async function requireIcuDataDirectory(path: string, source: string): Promise { await requireDirectory(path, source); for (const entry of await readdir(path, { withFileTypes: true })) { @@ -211,3 +507,45 @@ async function requireIcuDataDirectory(path: string, source: string): Promise { + try { + return await readFile(path, 'utf8'); + } catch { + return undefined; + } +} + +function extensionPackageName(sqlName: string): string { + return `@oliphaunt/extension-${sqlName.replaceAll('_', '-')}`; +} + +function extensionTargetPackageName(sqlName: string, target: string): string { + return `${extensionPackageName(sqlName)}-${target}`; +} + +function selectedExtensionClosure(extensions: ReadonlyArray): string[] { + const seen = new Set(); + const queue = [...extensions]; + while (queue.length > 0) { + const sqlName = queue.shift(); + if (sqlName === undefined || seen.has(sqlName)) { + continue; + } + seen.add(sqlName); + const metadata = generatedExtensionBySqlName(sqlName); + for (const dependency of metadata?.selectedExtensionDependencies ?? metadata?.dependencies ?? []) { + queue.push(dependency); + } + } + return [...seen].sort(); +} + +function nativeModuleDirectoryCandidates(libraryPath: string): string[] { + const libraryDir = dirname(libraryPath); + return [join(libraryDir, 'modules'), join(dirname(libraryDir), 'lib', 'modules')]; +} + +function runtimeCacheKey(value: unknown): string { + return createHash('sha256').update(JSON.stringify(value)).digest('hex').slice(0, 32); +} diff --git a/src/sdks/js/src/native/bun.ts b/src/sdks/js/src/native/bun.ts index 67e19205..411d7f54 100644 --- a/src/sdks/js/src/native/bun.ts +++ b/src/sdks/js/src/native/bun.ts @@ -1,10 +1,11 @@ import { applyNativeIcuDataEnvironment, + applyNativeModuleEnvironment, assertSupportedDirectBackupFormat, errorMessage, nativeBackupFormat, } from './common.js'; -import { resolveNodeNativeInstall } from './assets-node.js'; +import { materializeNodeExtensionInstall, resolveNodeNativeInstall } from './assets-node.js'; import type { BackupFormat } from '../types.js'; import { packConfigPointers, @@ -54,8 +55,22 @@ export async function createBunNativeBinding( capabilities(): bigint { return BigInt(symbols.oliphaunt_capabilities() as number | bigint); }, - open(config: NativeOpenConfig): NativeHandle { - const packed = packConfigPointers(config, (value) => pointerOf(ffi, value)); + async open(config: NativeOpenConfig): Promise { + const extensionInstall = await materializeNodeExtensionInstall( + { + ...install, + runtimeDirectory: config.runtimeDirectory ?? install.runtimeDirectory, + }, + config.extensions, + ); + applyNativeModuleEnvironment(extensionInstall.moduleDirectory); + const packed = packConfigPointers( + { + ...config, + runtimeDirectory: extensionInstall.runtimeDirectory, + }, + (value) => pointerOf(ffi, value), + ); const out = new Uint8Array(8); const rc = symbols.oliphaunt_init(packed.config, out) as number; keepAlive(packed.keepAlive); diff --git a/src/sdks/js/src/native/common.ts b/src/sdks/js/src/native/common.ts index c07782d4..f995b657 100644 --- a/src/sdks/js/src/native/common.ts +++ b/src/sdks/js/src/native/common.ts @@ -5,6 +5,7 @@ export const RESTORE_REPLACE_EXISTING = 1n; export const LIBOLIPHAUNT_RUNTIME_DIR_ENV = 'OLIPHAUNT_RUNTIME_DIR'; export const OLIPHAUNT_ICU_DATA_DIR_ENV = 'OLIPHAUNT_ICU_DATA_DIR'; export const ICU_DATA_ENV = 'ICU_DATA'; +export const OLIPHAUNT_EMBEDDED_MODULE_DIR_ENV = 'OLIPHAUNT_EMBEDDED_MODULE_DIR'; export const CAP_PROTOCOL_RAW = 1n << 0n; export const CAP_PROTOCOL_STREAM = 1n << 1n; @@ -66,6 +67,16 @@ export function applyNativeIcuDataEnvironment(icuDataDirectory?: string): void { setRuntimeEnvironment(ICU_DATA_ENV, icuDataDirectory); } +export function applyNativeModuleEnvironment(moduleDirectory?: string): void { + if (moduleDirectory === undefined || moduleDirectory.trim().length === 0) { + return; + } + if (moduleDirectory.includes('\0')) { + throw new Error(`${OLIPHAUNT_EMBEDDED_MODULE_DIR_ENV} must not contain NUL bytes`); + } + setRuntimeEnvironment(OLIPHAUNT_EMBEDDED_MODULE_DIR_ENV, moduleDirectory); +} + export function liboliphauntPackageTarget( platform: string, architecture: string, @@ -158,7 +169,7 @@ function setRuntimeEnvironment(name: string, value: string): void { try { deno.env.set(name, value); } catch (error) { - throw new Error(`cannot set ${name}; grant environment-write permission for native ICU data`, { + throw new Error(`cannot set ${name}; grant environment-write permission for native runtime data`, { cause: error, }); } diff --git a/src/sdks/js/src/native/node.ts b/src/sdks/js/src/native/node.ts index f77e642e..4cfc13f8 100644 --- a/src/sdks/js/src/native/node.ts +++ b/src/sdks/js/src/native/node.ts @@ -1,10 +1,11 @@ import { applyNativeIcuDataEnvironment, + applyNativeModuleEnvironment, assertSupportedDirectBackupFormat, nativeBackupFormat, } from './common.js'; import { loadNodeDirectAddon } from './node-addon.js'; -import { resolveNodeNativeInstall } from './assets-node.js'; +import { materializeNodeExtensionInstall, resolveNodeNativeInstall } from './assets-node.js'; import type { BackupFormat } from '../types.js'; import type { NativeBinding, @@ -32,11 +33,19 @@ export async function createNodeNativeBinding( capabilities(): bigint { return BigInt(addon.capabilities(install.libraryPath)); }, - open(config: NativeOpenConfig): NativeHandle { + async open(config: NativeOpenConfig): Promise { + const extensionInstall = await materializeNodeExtensionInstall( + { + ...install, + runtimeDirectory: config.runtimeDirectory ?? install.runtimeDirectory, + }, + config.extensions, + ); + applyNativeModuleEnvironment(extensionInstall.moduleDirectory); return addon.open({ ...config, - libraryPath: install.libraryPath, - runtimeDirectory: config.runtimeDirectory ?? install.runtimeDirectory, + libraryPath: extensionInstall.libraryPath, + runtimeDirectory: extensionInstall.runtimeDirectory, }); }, execProtocolRaw(handle: NativeHandle, request: Uint8Array): Uint8Array { diff --git a/src/sdks/js/src/native/types.ts b/src/sdks/js/src/native/types.ts index 76236c12..b04152a5 100644 --- a/src/sdks/js/src/native/types.ts +++ b/src/sdks/js/src/native/types.ts @@ -10,6 +10,7 @@ export type NativeOpenConfig = { runtimeDirectory?: string; username: string; database: string; + extensions: string[]; startupArgs: string[]; }; diff --git a/src/sdks/js/src/runtime/broker.ts b/src/sdks/js/src/runtime/broker.ts index a4414bde..2f26d31d 100644 --- a/src/sdks/js/src/runtime/broker.ts +++ b/src/sdks/js/src/runtime/broker.ts @@ -11,6 +11,7 @@ import { ICU_DATA_ENV, envVar, LIBOLIPHAUNT_RUNTIME_DIR_ENV, + OLIPHAUNT_EMBEDDED_MODULE_DIR_ENV, OLIPHAUNT_ICU_DATA_DIR_ENV, } from '../native/common.js'; import { @@ -386,25 +387,33 @@ type BrokerNativeInstall = { libraryPath: string; runtimeDirectory?: string; icuDataDirectory?: string; + moduleDirectory?: string; }; async function resolveBrokerNativeInstall(config: { libraryPath?: string; runtimeDirectory?: string; + extensions?: readonly string[]; }): Promise { - const install = - runtimeName() === 'deno' - ? await import('../native/assets-deno.js').then((module) => - module.resolveDenoNativeInstall(config.libraryPath), - ) - : await import('../native/assets-node.js').then((module) => - module.resolveNodeNativeInstall(config.libraryPath), - ); - return { + if (runtimeName() === 'deno') { + const install = await import('../native/assets-deno.js').then((module) => + module.resolveDenoNativeInstall(config.libraryPath), + ); + return { + libraryPath: install.libraryPath, + runtimeDirectory: config.runtimeDirectory ?? install.runtimeDirectory, + icuDataDirectory: install.icuDataDirectory, + }; + } + + const assets = await import('../native/assets-node.js'); + const install = await assets.resolveNodeNativeInstall(config.libraryPath); + const resolved = { libraryPath: install.libraryPath, runtimeDirectory: config.runtimeDirectory ?? install.runtimeDirectory, icuDataDirectory: install.icuDataDirectory, }; + return assets.materializeNodeExtensionInstall(resolved, config.extensions ?? []); } function brokerSpawnEnv( @@ -423,6 +432,9 @@ function brokerSpawnEnv( env[OLIPHAUNT_ICU_DATA_DIR_ENV] = nativeInstall.icuDataDirectory; env[ICU_DATA_ENV] = nativeInstall.icuDataDirectory; } + if (nativeInstall.moduleDirectory !== undefined) { + env[OLIPHAUNT_EMBEDDED_MODULE_DIR_ENV] = nativeInstall.moduleDirectory; + } return env; } diff --git a/src/sdks/js/src/runtime/direct.ts b/src/sdks/js/src/runtime/direct.ts index d0c2e85f..511a9678 100644 --- a/src/sdks/js/src/runtime/direct.ts +++ b/src/sdks/js/src/runtime/direct.ts @@ -30,6 +30,7 @@ export function directRuntimeBinding(binding: NativeBinding): RuntimeBinding { runtimeDirectory: config.runtimeDirectory ?? binding.defaultRuntimeDirectory, username: config.username, database: config.database, + extensions: config.extensions, startupArgs: config.startupArgs, }), ); diff --git a/src/sdks/rust/src/liboliphaunt/ffi.rs b/src/sdks/rust/src/liboliphaunt/ffi.rs index 1a9f055c..7b66676a 100644 --- a/src/sdks/rust/src/liboliphaunt/ffi.rs +++ b/src/sdks/rust/src/liboliphaunt/ffi.rs @@ -26,6 +26,7 @@ pub(super) const BACKUP_FORMAT_OLIPHAUNT_ARCHIVE: u32 = 3; pub(super) const ENV_OLIPHAUNT: &str = "LIBOLIPHAUNT_PATH"; pub(super) const ENV_INSTALL_DIR: &str = "OLIPHAUNT_INSTALL_DIR"; +pub(super) const ENV_EMBEDDED_MODULE_DIR: &str = "OLIPHAUNT_EMBEDDED_MODULE_DIR"; pub(super) const ENV_POSTGRES: &str = "OLIPHAUNT_POSTGRES"; pub(super) const ENV_INITDB: &str = "OLIPHAUNT_INITDB"; diff --git a/src/sdks/rust/src/liboliphaunt/root/runtime/locate.rs b/src/sdks/rust/src/liboliphaunt/root/runtime/locate.rs index 7f35da13..9f388bac 100644 --- a/src/sdks/rust/src/liboliphaunt/root/runtime/locate.rs +++ b/src/sdks/rust/src/liboliphaunt/root/runtime/locate.rs @@ -1,7 +1,8 @@ use std::path::{Path, PathBuf}; use super::super::super::ffi::{ - ENV_INITDB, ENV_INSTALL_DIR, ENV_POSTGRES, env_path_candidates, resolve_library_path_candidates, + ENV_EMBEDDED_MODULE_DIR, ENV_INITDB, ENV_INSTALL_DIR, ENV_POSTGRES, env_path_candidates, + resolve_library_path_candidates, }; use crate::error::{Error, Result}; @@ -51,6 +52,7 @@ fn locate_native_embedded_modules_dir_from_libraries( library_paths: impl IntoIterator, ) -> Result { let mut candidates = Vec::new(); + candidates.extend(env_path_candidates([ENV_EMBEDDED_MODULE_DIR])); for path in library_paths { if let Some(out_dir) = path.parent() { candidates.push(out_dir.join("modules")); @@ -131,6 +133,35 @@ mod tests { assert_eq!(located, modules_dir); } + #[test] + fn embedded_modules_locator_prefers_explicit_environment_dir() { + let temp = TempTree::new("explicit-env-modules"); + let install_dir = temp.path().join("runtime"); + let modules_dir = temp.path().join("registry/modules"); + fs::create_dir_all(&install_dir).expect("create runtime"); + fs::create_dir_all(&modules_dir).expect("create modules"); + let previous = std::env::var_os(ENV_EMBEDDED_MODULE_DIR); + unsafe { + std::env::set_var(ENV_EMBEDDED_MODULE_DIR, &modules_dir); + } + + let located = locate_native_embedded_modules_dir_from_libraries( + &install_dir, + [temp.path().join("lib/liboliphaunt.so")], + ) + .expect("locate env modules"); + + match previous { + Some(value) => unsafe { + std::env::set_var(ENV_EMBEDDED_MODULE_DIR, value); + }, + None => unsafe { + std::env::remove_var(ENV_EMBEDDED_MODULE_DIR); + }, + } + assert_eq!(located, modules_dir); + } + struct TempTree { path: PathBuf, } diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index b7906a83..0a394d7b 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -18,6 +18,7 @@ import hashlib import json import os +import platform as host_platform import shutil import subprocess import sys @@ -37,6 +38,7 @@ DEFAULT_REPO = "f0rr0/oliphaunt" DEFAULT_REGISTRY_ROOT = ROOT / "target" / "local-registries" DEFAULT_ARTIFACT_ROOT = ROOT / "target" / "local-registry-artifacts" +NPM_PACKAGE_SIZE_LIMIT_BYTES = 10 * 1024 * 1024 LOCAL_PUBLISH_ARTIFACTS = [ "liboliphaunt-native-release-assets", @@ -222,6 +224,545 @@ def discover_files(roots: list[Path], suffixes: tuple[str, ...]) -> list[Path]: return sorted(set(files)) +def host_npm_target() -> str | None: + machine = host_platform.machine().lower() + if sys.platform == "linux" and machine in {"x86_64", "amd64"}: + return "linux-x64-gnu" + if sys.platform == "linux" and machine in {"aarch64", "arm64"}: + return "linux-arm64-gnu" + if sys.platform == "darwin" and machine == "arm64": + return "macos-arm64" + if sys.platform == "win32" and machine in {"amd64", "x86_64"}: + return "windows-x64-msvc" + return None + + +def npm_platform_constraints(target: str) -> dict[str, list[str]]: + if target == "linux-x64-gnu": + return {"os": ["linux"], "cpu": ["x64"], "libc": ["glibc"]} + if target == "linux-arm64-gnu": + return {"os": ["linux"], "cpu": ["arm64"], "libc": ["glibc"]} + if target == "macos-arm64": + return {"os": ["darwin"], "cpu": ["arm64"]} + if target == "windows-x64-msvc": + return {"os": ["win32"], "cpu": ["x64"]} + return {} + + +def extension_npm_package(sql_name: str) -> str: + return f"@oliphaunt/extension-{sql_name.replace('_', '-')}" + + +def extension_npm_target_package(sql_name: str, target: str) -> str: + return f"{extension_npm_package(sql_name)}-{target}" + + +def extension_npm_payload_package(sql_name: str, target: str, index: int) -> str: + return f"{extension_npm_target_package(sql_name, target)}-payload-{index}" + + +def discover_extension_manifests(roots: list[Path]) -> list[Path]: + manifests: list[Path] = [] + for root in roots: + if root.is_file() and root.name == "extension-artifacts.json": + manifests.append(root) + continue + if root.is_dir(): + manifests.extend(path for path in root.rglob("extension-artifacts.json") if path.is_file()) + return sorted(set(manifests)) + + +def safe_package_path(package_name: str) -> str: + return package_name.replace("@", "").replace("/", "__") + + +def extension_release_manifest(extension_dir: Path, product: str, version: str) -> dict[str, Any]: + manifest_path = extension_dir / "release-assets" / f"{product}-{version}-manifest.json" + if not manifest_path.is_file(): + return {} + return json.loads(manifest_path.read_text(encoding="utf-8")) + + +def extension_runtime_asset( + extension_dir: Path, + manifest: dict[str, Any], + target: str, +) -> Path | None: + for asset in manifest.get("assets", []): + if ( + asset.get("family") == "native" + and asset.get("kind") == "runtime" + and asset.get("target") == target + and isinstance(asset.get("name"), str) + ): + path = extension_dir / "release-assets" / asset["name"] + if path.is_file(): + return path + return None + + +def extract_extension_runtime(asset: Path, runtime_dir: Path) -> None: + runtime_dir.mkdir(parents=True, exist_ok=True) + with tarfile.open(asset, "r:gz") as archive: + for member in archive.getmembers(): + if not member.isfile() or not member.name.startswith("files/"): + continue + relative = Path(member.name.removeprefix("files/")) + if relative.is_absolute() or ".." in relative.parts: + raise RuntimeError(f"{rel(asset)} contains unsafe path {member.name!r}") + target = runtime_dir / relative + target.parent.mkdir(parents=True, exist_ok=True) + source = archive.extractfile(member) + if source is None: + continue + with source, target.open("wb") as output: + shutil.copyfileobj(source, output) + + +def extension_module_directory(runtime_dir: Path) -> Path | None: + postgres_lib = runtime_dir / "lib" / "postgresql" + if not postgres_lib.is_dir(): + return None + for path in sorted(postgres_lib.iterdir()): + if path.is_file() and path.suffix.lower() in {".so", ".dylib", ".dll"}: + return postgres_lib + return None + + +def strip_extension_modules(runtime_dir: Path, target: str) -> None: + module_dir = extension_module_directory(runtime_dir) + if module_dir is None or not target.startswith("linux-"): + return + strip = shutil.which("strip") + if strip is None: + return + for path in sorted(module_dir.iterdir()): + if path.is_file() and path.suffix == ".so": + run([strip, "--strip-unneeded", str(path)], check=False) + + +def write_extension_readme(package_dir: Path, package_name: str, sql_name: str, target: str | None) -> None: + target_text = f" for `{target}`" if target else "" + package_dir.joinpath("README.md").write_text( + "\n".join( + [ + f"# {package_name}", + "", + f"Oliphaunt registry package for the `{sql_name}` PostgreSQL extension{target_text}.", + "", + "This package is consumed by `@oliphaunt/ts` when an application opens a database with", + f"`extensions: ['{sql_name}']`.", + "", + ] + ), + encoding="utf-8", + ) + + +def write_extension_meta_package( + package_dir: Path, + *, + product: str, + version: str, + sql_name: str, + target: str, +) -> None: + package_name = extension_npm_package(sql_name) + target_package = extension_npm_target_package(sql_name, target) + package_dir.mkdir(parents=True, exist_ok=True) + write_extension_readme(package_dir, package_name, sql_name, None) + package_dir.joinpath("package.json").write_text( + json.dumps( + { + "name": package_name, + "version": version, + "description": f"Oliphaunt extension package for PostgreSQL {sql_name}.", + "license": "MIT AND Apache-2.0 AND PostgreSQL", + "type": "module", + "optionalDependencies": {target_package: version}, + "oliphaunt": { + "product": product, + "kind": "exact-extension", + "sqlName": sql_name, + "targetPackageNames": {target: target_package}, + }, + "publishConfig": {"access": "public", "provenance": False}, + "files": ["README.md"], + "exports": {"./package.json": "./package.json"}, + }, + indent=2, + ) + + "\n", + encoding="utf-8", + ) + + +def write_extension_target_package( + package_dir: Path, + *, + product: str, + version: str, + sql_name: str, + target: str, + liboliphaunt_version: str, + payload_package_names: list[str], +) -> None: + package_name = extension_npm_target_package(sql_name, target) + package_dir.mkdir(parents=True, exist_ok=True) + write_extension_readme(package_dir, package_name, sql_name, target) + + package_json = { + "name": package_name, + "version": version, + "description": f"{target} Oliphaunt extension package selector for PostgreSQL {sql_name}.", + "license": "MIT AND Apache-2.0 AND PostgreSQL", + "type": "module", + **npm_platform_constraints(target), + "optional": True, + "optionalDependencies": {name: version for name in payload_package_names}, + "oliphaunt": { + "product": product, + "kind": "exact-extension-target", + "sqlName": sql_name, + "target": target, + "liboliphauntVersion": liboliphaunt_version, + "payloadPackageNames": payload_package_names, + }, + "publishConfig": {"access": "public", "provenance": False}, + "files": ["README.md"], + "exports": {"./package.json": "./package.json"}, + } + package_dir.joinpath("package.json").write_text( + json.dumps(package_json, indent=2) + "\n", + encoding="utf-8", + ) + + +def copy_runtime_entries(runtime_dir: Path, payload_runtime_dir: Path, entries: list[Path]) -> None: + for entry in entries: + relative = entry.relative_to(runtime_dir) + target = payload_runtime_dir / relative + if entry.is_dir(): + shutil.copytree(entry, target, dirs_exist_ok=True) + elif entry.is_file(): + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(entry, target) + + +def write_extension_payload_package( + package_dir: Path, + *, + package_name: str, + product: str, + version: str, + sql_name: str, + target: str, + liboliphaunt_version: str, +) -> None: + runtime_dir = package_dir / "runtime" + module_dir = extension_module_directory(runtime_dir) + write_extension_readme(package_dir, package_name, sql_name, target) + oliphaunt: dict[str, Any] = { + "product": product, + "kind": "exact-extension-payload", + "sqlName": sql_name, + "target": target, + "runtimeRelativePath": "runtime", + "liboliphauntVersion": liboliphaunt_version, + } + if module_dir is not None: + oliphaunt["moduleRelativePath"] = module_dir.relative_to(package_dir).as_posix() + package_json = { + "name": package_name, + "version": version, + "description": f"{target} Oliphaunt extension runtime payload for PostgreSQL {sql_name}.", + "license": "MIT AND Apache-2.0 AND PostgreSQL", + "type": "module", + **npm_platform_constraints(target), + "optional": True, + "oliphaunt": oliphaunt, + "publishConfig": {"access": "public", "provenance": False}, + "files": ["runtime", "README.md"], + "exports": {"./package.json": "./package.json"}, + } + package_dir.joinpath("package.json").write_text( + json.dumps(package_json, indent=2) + "\n", + encoding="utf-8", + ) + + +def pack_extension_package(package_dir: Path, tarball_dir: Path) -> Path: + tarball_dir.mkdir(parents=True, exist_ok=True) + completed = run( + [ + "npm", + "pack", + str(package_dir), + "--pack-destination", + str(tarball_dir), + "--loglevel=error", + ], + capture=True, + ) + filename = completed.stdout.strip().splitlines()[-1] + return tarball_dir / filename + + +def npm_package_size_ok(tarball: Path, result: SurfaceResult) -> bool: + size = tarball.stat().st_size + if size <= NPM_PACKAGE_SIZE_LIMIT_BYTES: + return True + result.add_skip( + f"{rel(tarball)} is {size} bytes, exceeding the 10 MiB npm package limit", + ) + tarball.unlink(missing_ok=True) + return False + + +def stage_extension_payload_group( + *, + runtime_dir: Path, + entries: list[Path], + package_root: Path, + tarball_root: Path, + product: str, + version: str, + sql_name: str, + target: str, + liboliphaunt_version: str, + payload_index: int, + result: SurfaceResult, +) -> tuple[list[str], list[Path]]: + package_name = extension_npm_payload_package(sql_name, target, payload_index) + package_dir = package_root / safe_package_path(package_name) + shutil.rmtree(package_dir, ignore_errors=True) + payload_runtime_dir = package_dir / "runtime" + payload_runtime_dir.mkdir(parents=True, exist_ok=True) + copy_runtime_entries(runtime_dir, payload_runtime_dir, entries) + write_extension_payload_package( + package_dir, + package_name=package_name, + product=product, + version=version, + sql_name=sql_name, + target=target, + liboliphaunt_version=liboliphaunt_version, + ) + tarball = pack_extension_package(package_dir, tarball_root) + if tarball.stat().st_size <= NPM_PACKAGE_SIZE_LIMIT_BYTES: + return [package_name], [tarball] + + tarball.unlink(missing_ok=True) + shutil.rmtree(package_dir, ignore_errors=True) + if len(entries) == 1 and entries[0].is_dir(): + child_entries = sorted(entries[0].iterdir()) + if child_entries: + return stage_extension_payload_groups( + runtime_dir=runtime_dir, + groups=[[entry] for entry in child_entries], + package_root=package_root, + tarball_root=tarball_root, + product=product, + version=version, + sql_name=sql_name, + target=target, + liboliphaunt_version=liboliphaunt_version, + start_index=payload_index, + result=result, + ) + if len(entries) > 1: + return stage_extension_payload_groups( + runtime_dir=runtime_dir, + groups=[[entry] for entry in entries], + package_root=package_root, + tarball_root=tarball_root, + product=product, + version=version, + sql_name=sql_name, + target=target, + liboliphaunt_version=liboliphaunt_version, + start_index=payload_index, + result=result, + ) + + result.add_skip( + f"{package_name} cannot be split below the 10 MiB npm package limit; largest entry is {entries[0]}", + ) + return [], [] + + +def stage_extension_payload_groups( + *, + runtime_dir: Path, + groups: list[list[Path]], + package_root: Path, + tarball_root: Path, + product: str, + version: str, + sql_name: str, + target: str, + liboliphaunt_version: str, + start_index: int, + result: SurfaceResult, +) -> tuple[list[str], list[Path]]: + package_names: list[str] = [] + tarballs: list[Path] = [] + payload_index = start_index + for entries in groups: + names, paths = stage_extension_payload_group( + runtime_dir=runtime_dir, + entries=entries, + package_root=package_root, + tarball_root=tarball_root, + product=product, + version=version, + sql_name=sql_name, + target=target, + liboliphaunt_version=liboliphaunt_version, + payload_index=payload_index, + result=result, + ) + if not names: + continue + package_names.extend(names) + tarballs.extend(paths) + payload_index += len(names) + return package_names, tarballs + + +def stage_extension_payload_packages( + *, + runtime_dir: Path, + package_root: Path, + tarball_root: Path, + product: str, + version: str, + sql_name: str, + target: str, + liboliphaunt_version: str, + result: SurfaceResult, +) -> tuple[list[str], list[Path]]: + entries = sorted(runtime_dir.iterdir()) + return stage_extension_payload_groups( + runtime_dir=runtime_dir, + groups=[[entry] for entry in entries], + package_root=package_root, + tarball_root=tarball_root, + product=product, + version=version, + sql_name=sql_name, + target=target, + liboliphaunt_version=liboliphaunt_version, + start_index=0, + result=result, + ) + + +def stage_extension_npm_packages( + roots: list[Path], + staging_root: Path, + target: str | None, + dry_run: bool, + result: SurfaceResult, +) -> Path | None: + manifests = discover_extension_manifests(roots) + if not manifests: + result.add_skip("no extension-artifacts.json manifests found for npm extension packages") + return None + if target is None: + result.add_skip("current host does not map to a supported npm extension target") + return None + + if dry_run: + for manifest_path in manifests: + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + sql_name = manifest.get("sqlName") + version = manifest.get("version") + if isinstance(sql_name, str) and isinstance(version, str): + result.staged.append( + f"dry-run npm extension packages {extension_npm_package(sql_name)}@{version} ({target})", + ) + return None + + shutil.rmtree(staging_root, ignore_errors=True) + package_root = staging_root / "packages" + tarball_root = staging_root / "tarballs" + work_root = staging_root / "work" + staged_any = False + for manifest_path in manifests: + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + extension_dir = manifest_path.parent + product = manifest.get("product") + version = manifest.get("version") + sql_name = manifest.get("sqlName") + if not all(isinstance(value, str) and value for value in [product, version, sql_name]): + result.add_skip(f"{rel(manifest_path)} is missing product, version, or sqlName") + continue + release_manifest = extension_release_manifest(extension_dir, product, version) + asset = extension_runtime_asset(extension_dir, release_manifest or manifest, target) + if asset is None: + result.add_skip(f"{product}@{version} has no {target} native runtime asset") + continue + compatibility = release_manifest.get("compatibility", {}) + liboliphaunt_version = compatibility.get("nativeRuntimeVersion", version) + if not isinstance(liboliphaunt_version, str) or not liboliphaunt_version: + result.add_skip(f"{product}@{version} is missing native runtime compatibility") + continue + + meta_dir = package_root / safe_package_path(extension_npm_package(sql_name)) + target_dir = package_root / safe_package_path(extension_npm_target_package(sql_name, target)) + runtime_work_dir = work_root / safe_package_path(extension_npm_target_package(sql_name, target)) / "runtime" + extract_extension_runtime(asset, runtime_work_dir) + strip_extension_modules(runtime_work_dir, target) + payload_package_names, payload_tarballs = stage_extension_payload_packages( + runtime_dir=runtime_work_dir, + package_root=package_root, + tarball_root=tarball_root, + product=product, + version=version, + sql_name=sql_name, + target=target, + liboliphaunt_version=liboliphaunt_version, + result=result, + ) + if not payload_package_names: + continue + write_extension_meta_package( + meta_dir, + product=product, + version=version, + sql_name=sql_name, + target=target, + ) + write_extension_target_package( + target_dir, + product=product, + version=version, + sql_name=sql_name, + target=target, + liboliphaunt_version=liboliphaunt_version, + payload_package_names=payload_package_names, + ) + target_tarball = pack_extension_package(target_dir, tarball_root) + if not npm_package_size_ok(target_tarball, result): + for tarball in payload_tarballs: + tarball.unlink(missing_ok=True) + continue + meta_tarball = pack_extension_package(meta_dir, tarball_root) + if not npm_package_size_ok(meta_tarball, result): + target_tarball.unlink(missing_ok=True) + for tarball in payload_tarballs: + tarball.unlink(missing_ok=True) + continue + for tarball in payload_tarballs: + result.staged.append(rel(tarball)) + result.staged.append(rel(target_tarball)) + result.staged.append(rel(meta_tarball)) + staged_any = True + + return tarball_root if staged_any else None + + def write_verdaccio_config(root: Path, port: int) -> tuple[Path, bool]: config = root / "config.yaml" storage = root / "storage" @@ -404,8 +945,59 @@ def ensure_verdaccio_npmrc(root: Path, registry_url: str, dry_run: bool) -> Path return npmrc +def npm_package_identity(tarball: Path) -> tuple[str, str] | None: + try: + with tarfile.open(tarball, "r:gz") as archive: + for member in archive.getmembers(): + if member.isfile() and member.name.endswith("/package.json"): + source = archive.extractfile(member) + if source is None: + continue + with source: + package_json = json.loads(source.read().decode("utf-8")) + name = package_json.get("name") + version = package_json.get("version") + if isinstance(name, str) and isinstance(version, str): + return name, version + except (tarfile.TarError, json.JSONDecodeError): + return None + return None + + +def npm_package_exists( + registry_url: str, + npmrc: Path | None, + name: str, + version: str, +) -> bool: + command = [ + "npm", + "view", + f"{name}@{version}", + "version", + "--registry", + registry_url, + "--fetch-retries=0", + "--loglevel=error", + ] + if npmrc is not None: + command.extend(["--userconfig", str(npmrc)]) + completed = run(command, check=False, capture=True, timeout=10) + return completed.returncode == 0 and completed.stdout.strip() == version + + def publish_npm(roots: list[Path], registry_root: Path, dry_run: bool, strict: bool, port: int) -> SurfaceResult: result = SurfaceResult("npm") + extension_target = host_npm_target() + extension_tarball_root = stage_extension_npm_packages( + roots, + registry_root / "npm-extension-packages", + extension_target, + dry_run, + result, + ) + if extension_tarball_root is not None: + roots = [*roots, extension_tarball_root] tarballs = discover_files(roots, (".tgz",)) if not tarballs: result.add_skip("no npm .tgz artifacts found") @@ -418,8 +1010,13 @@ def publish_npm(roots: list[Path], registry_root: Path, dry_run: bool, strict: b npmrc = ensure_verdaccio_npmrc(verdaccio_root, registry_url, dry_run) result.staged.append(f"verdaccio={registry_url}") for tarball in tarballs: + identity = npm_package_identity(tarball) if dry_run: - result.published.append(f"dry-run npm publish {rel(tarball)}") + label = rel(tarball) if identity is None else f"{identity[0]}@{identity[1]}" + result.published.append(f"dry-run npm publish {label}") + continue + if identity is not None and npm_package_exists(registry_url, npmrc, identity[0], identity[1]): + result.add_skip(f"already published {identity[0]}@{identity[1]}") continue command = [ "npm", @@ -431,6 +1028,7 @@ def publish_npm(roots: list[Path], registry_root: Path, dry_run: bool, strict: b "--ignore-scripts", "--access", "public", + "--loglevel=error", ] if npmrc is not None: command.extend(["--userconfig", str(npmrc)]) From 2d5ab9e6064ea5e31bea9e30784635c07aa3d69b Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Thu, 25 Jun 2026 12:19:01 +0000 Subject: [PATCH 003/137] fix(wasix): package extension assets from registry --- .github/workflows/ci.yml | 15 + examples/electron-wasix/src-wasix/Cargo.toml | 6 +- examples/tauri-wasix/src-tauri/Cargo.toml | 6 +- .../crates/oliphaunt-wasix/Cargo.toml | 39 ++ .../oliphaunt-wasix/src/oliphaunt/aot.rs | 67 +- .../oliphaunt-wasix/src/oliphaunt/assets.rs | 18 +- .../wasix/crates/assets/Cargo.toml | 41 ++ .../liboliphaunt/wasix/crates/assets/build.rs | 610 +++++++++++++++++- .../wasix/crates/assets/src/lib.rs | 8 +- .../wasix/tools/build-aot-target.sh | 1 + tools/graph/ci_plan.py | 1 + tools/release/build-extension-ci-artifacts.py | 25 + tools/release/local_registry_publish.py | 4 + ...kage_liboliphaunt_wasix_cargo_artifacts.py | 494 +++++++++++++- tools/release/release.py | 22 +- tools/xtask/src/asset_pipeline.rs | 98 ++- tools/xtask/src/main.rs | 7 + 17 files changed, 1422 insertions(+), 40 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e690fc3..b5f0bf76 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -484,6 +484,7 @@ jobs: - affected - extension-artifacts-native - extension-artifacts-wasix + - liboliphaunt-wasix-aot if: ${{ contains(fromJson(needs.affected.outputs.jobs), 'extension-packages') }} runs-on: ubuntu-latest timeout-minutes: 30 @@ -517,6 +518,13 @@ jobs: path: target/extensions/wasix/release-assets merge-multiple: true + - name: Download WASIX exact-extension AOT artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + pattern: liboliphaunt-wasix-extension-aot-* + path: target/extensions/wasix/aot-artifacts + merge-multiple: true + - name: Build exact-extension product packages env: OLIPHAUNT_EXTENSION_PACKAGE_PRODUCTS: ${{ needs.affected.outputs.extension_package_products_csv }} @@ -1441,6 +1449,13 @@ jobs: target/oliphaunt-wasix/aot-upload/** if-no-files-found: error + - name: Upload target extension AOT artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: liboliphaunt-wasix-extension-aot-${{ matrix.target_id }} + path: target/extensions/wasix/aot-artifacts + if-no-files-found: error + liboliphaunt-wasix-release-assets: name: Builds / liboliphaunt-wasix-release-assets needs: diff --git a/examples/electron-wasix/src-wasix/Cargo.toml b/examples/electron-wasix/src-wasix/Cargo.toml index 73d291bd..806c9466 100644 --- a/examples/electron-wasix/src-wasix/Cargo.toml +++ b/examples/electron-wasix/src-wasix/Cargo.toml @@ -8,5 +8,9 @@ publish = false [dependencies] anyhow = "1" -oliphaunt-wasix = { path = "../../../src/bindings/wasix-rust/crates/oliphaunt-wasix", features = ["extensions"] } +oliphaunt-wasix = { path = "../../../src/bindings/wasix-rust/crates/oliphaunt-wasix", features = [ + "extension-hstore", + "extension-pg-trgm", + "extension-unaccent", +] } serde_json = "1" diff --git a/examples/tauri-wasix/src-tauri/Cargo.toml b/examples/tauri-wasix/src-tauri/Cargo.toml index 5ea40bd1..b92957cb 100644 --- a/examples/tauri-wasix/src-tauri/Cargo.toml +++ b/examples/tauri-wasix/src-tauri/Cargo.toml @@ -16,7 +16,11 @@ tauri-build = { version = "2", features = [] } [dependencies] anyhow = "1" -oliphaunt-wasix = { path = "../../../src/bindings/wasix-rust/crates/oliphaunt-wasix", features = ["extensions"] } +oliphaunt-wasix = { path = "../../../src/bindings/wasix-rust/crates/oliphaunt-wasix", features = [ + "extension-hstore", + "extension-pg-trgm", + "extension-unaccent", +] } serde = { version = "1", features = ["derive"] } sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio-rustls", "postgres"] } tauri = { version = "2", features = [] } diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml b/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml index 9e140fd7..68c33f8c 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml @@ -20,6 +20,45 @@ exclude = [ [features] default = [] extensions = [] +extension-amcheck = ["extensions", "oliphaunt-wasix-assets/extension-amcheck"] +extension-auto-explain = ["extensions", "oliphaunt-wasix-assets/extension-auto-explain"] +extension-bloom = ["extensions", "oliphaunt-wasix-assets/extension-bloom"] +extension-btree-gin = ["extensions", "oliphaunt-wasix-assets/extension-btree-gin"] +extension-btree-gist = ["extensions", "oliphaunt-wasix-assets/extension-btree-gist"] +extension-citext = ["extensions", "oliphaunt-wasix-assets/extension-citext"] +extension-cube = ["extensions", "oliphaunt-wasix-assets/extension-cube"] +extension-dict-int = ["extensions", "oliphaunt-wasix-assets/extension-dict-int"] +extension-dict-xsyn = ["extensions", "oliphaunt-wasix-assets/extension-dict-xsyn"] +extension-earthdistance = ["extensions", "oliphaunt-wasix-assets/extension-earthdistance"] +extension-file-fdw = ["extensions", "oliphaunt-wasix-assets/extension-file-fdw"] +extension-fuzzystrmatch = ["extensions", "oliphaunt-wasix-assets/extension-fuzzystrmatch"] +extension-hstore = ["extensions", "oliphaunt-wasix-assets/extension-hstore"] +extension-intarray = ["extensions", "oliphaunt-wasix-assets/extension-intarray"] +extension-isn = ["extensions", "oliphaunt-wasix-assets/extension-isn"] +extension-lo = ["extensions", "oliphaunt-wasix-assets/extension-lo"] +extension-ltree = ["extensions", "oliphaunt-wasix-assets/extension-ltree"] +extension-pageinspect = ["extensions", "oliphaunt-wasix-assets/extension-pageinspect"] +extension-pg-buffercache = ["extensions", "oliphaunt-wasix-assets/extension-pg-buffercache"] +extension-pg-freespacemap = ["extensions", "oliphaunt-wasix-assets/extension-pg-freespacemap"] +extension-pg-hashids = ["extensions", "oliphaunt-wasix-assets/extension-pg-hashids"] +extension-pg-ivm = ["extensions", "oliphaunt-wasix-assets/extension-pg-ivm"] +extension-pg-surgery = ["extensions", "oliphaunt-wasix-assets/extension-pg-surgery"] +extension-pg-textsearch = ["extensions", "oliphaunt-wasix-assets/extension-pg-textsearch"] +extension-pg-trgm = ["extensions", "oliphaunt-wasix-assets/extension-pg-trgm"] +extension-pg-uuidv7 = ["extensions", "oliphaunt-wasix-assets/extension-pg-uuidv7"] +extension-pg-visibility = ["extensions", "oliphaunt-wasix-assets/extension-pg-visibility"] +extension-pg-walinspect = ["extensions", "oliphaunt-wasix-assets/extension-pg-walinspect"] +extension-pgcrypto = ["extensions", "oliphaunt-wasix-assets/extension-pgcrypto"] +extension-pgtap = ["extensions", "oliphaunt-wasix-assets/extension-pgtap"] +extension-postgis = ["extensions", "oliphaunt-wasix-assets/extension-postgis"] +extension-seg = ["extensions", "oliphaunt-wasix-assets/extension-seg"] +extension-tablefunc = ["extensions", "oliphaunt-wasix-assets/extension-tablefunc"] +extension-tcn = ["extensions", "oliphaunt-wasix-assets/extension-tcn"] +extension-tsm-system-rows = ["extensions", "oliphaunt-wasix-assets/extension-tsm-system-rows"] +extension-tsm-system-time = ["extensions", "oliphaunt-wasix-assets/extension-tsm-system-time"] +extension-unaccent = ["extensions", "oliphaunt-wasix-assets/extension-unaccent"] +extension-uuid-ossp = ["extensions", "oliphaunt-wasix-assets/extension-uuid-ossp"] +extension-vector = ["extensions", "oliphaunt-wasix-assets/extension-vector"] icu = ["dep:oliphaunt-icu"] [package.metadata.oliphaunt-wasix.assets] diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs index 52712520..88d310d7 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs @@ -451,7 +451,10 @@ fn validate_compressed_artifact_manifest( fn target_aot_manifest() -> Result { if let Some(json) = target_aot_manifest_json() { - return serde_json::from_str(json).context("parse package-manager-resolved AOT manifest"); + let mut manifest: AotManifest = + serde_json::from_str(json).context("parse package-manager-resolved AOT manifest")?; + merge_extension_aot_manifests(&mut manifest)?; + return Ok(manifest); } bail!( "no package-manager-resolved Wasmer LLVM AOT manifest is available for target {}; publish and stage the matching liboliphaunt-wasix AOT artifact crate with the application", @@ -459,6 +462,57 @@ fn target_aot_manifest() -> Result { ) } +fn merge_extension_aot_manifests(manifest: &mut AotManifest) -> Result<()> { + #[cfg(feature = "extensions")] + { + for sql_name in oliphaunt_wasix_assets::SELECTED_EXTENSION_SQL_NAMES { + let Some(json) = assets::extension_aot_manifest_json(target_triple(), sql_name) else { + continue; + }; + let extension_manifest: AotManifest = + serde_json::from_str(json).with_context(|| { + format!( + "parse package-manager-resolved AOT manifest for extension '{sql_name}'" + ) + })?; + ensure!( + extension_manifest.target_triple == manifest.target_triple, + "extension AOT manifest target mismatch for '{sql_name}': manifest={} core={}", + extension_manifest.target_triple, + manifest.target_triple + ); + ensure!( + extension_manifest.engine == manifest.engine, + "extension AOT manifest engine mismatch for '{sql_name}': manifest={} core={}", + extension_manifest.engine, + manifest.engine + ); + ensure!( + extension_manifest.wasmer_version == manifest.wasmer_version, + "extension AOT manifest Wasmer version mismatch for '{sql_name}': manifest={} core={}", + extension_manifest.wasmer_version, + manifest.wasmer_version + ); + ensure!( + extension_manifest.wasmer_wasix_version == manifest.wasmer_wasix_version, + "extension AOT manifest wasmer-wasix version mismatch for '{sql_name}': manifest={} core={}", + extension_manifest.wasmer_wasix_version, + manifest.wasmer_wasix_version + ); + ensure!( + extension_manifest.source_fingerprint == manifest.source_fingerprint, + "extension AOT manifest source fingerprint mismatch for '{sql_name}'" + ); + ensure!( + extension_manifest.postgres_version == manifest.postgres_version, + "extension AOT manifest postgres version mismatch for '{sql_name}'" + ); + manifest.artifacts.extend(extension_manifest.artifacts); + } + } + Ok(()) +} + fn cache_path(name: &str, hash: &str) -> Result { let safe_name = name.replace([':', '/', '\\'], "-"); let dirs = ProjectDirs::from("dev", "oliphaunt-wasix", "oliphaunt-wasix") @@ -632,13 +686,22 @@ fn target_triple() -> &'static str { } fn target_artifact_bytes(name: &str) -> Option<&'static [u8]> { - target_aot_artifact_bytes(name) + target_aot_artifact_bytes(name).or_else(|| extension_aot_artifact_bytes(name)) } fn target_aot_manifest_json() -> Option<&'static str> { target_aot_manifest_json_for_crate() } +fn extension_aot_artifact_bytes(name: &str) -> Option<&'static [u8]> { + #[cfg(feature = "extensions")] + { + return assets::extension_aot_artifact_bytes(target_triple(), name); + } + #[allow(unreachable_code)] + None +} + #[cfg(all(target_os = "macos", target_arch = "aarch64"))] fn target_aot_artifact_bytes(name: &str) -> Option<&'static [u8]> { if !oliphaunt_wasix_aot_aarch64_apple_darwin::HAS_EMBEDDED_AOT { diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/assets.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/assets.rs index ffe89bfd..89eeb432 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/assets.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/assets.rs @@ -80,7 +80,19 @@ pub(crate) fn extension_archive(sql_name: &str) -> Option<&'static [u8]> { #[cfg(feature = "extensions")] pub(crate) fn expected_extension_archive_sha256(sql_name: &str) -> Result { - Err(anyhow!( - "extension asset '{sql_name}' is not embedded in this oliphaunt-wasix build" - )) + oliphaunt_wasix_assets::expected_extension_archive_sha256(sql_name) + .map(str::to_owned) + .ok_or_else(|| { + anyhow!("extension asset '{sql_name}' is not embedded in this oliphaunt-wasix build") + }) +} + +#[cfg(feature = "extensions")] +pub(crate) fn extension_aot_manifest_json(target: &str, sql_name: &str) -> Option<&'static str> { + oliphaunt_wasix_assets::extension_aot_manifest_json(target, sql_name) +} + +#[cfg(feature = "extensions")] +pub(crate) fn extension_aot_artifact_bytes(target: &str, name: &str) -> Option<&'static [u8]> { + oliphaunt_wasix_assets::extension_aot_artifact_bytes(target, name) } diff --git a/src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml b/src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml index f60a72ff..0bca7958 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml +++ b/src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml @@ -18,6 +18,47 @@ include = [ "payload/**", ] +[features] +extension-amcheck = [] +extension-auto-explain = [] +extension-bloom = [] +extension-btree-gin = [] +extension-btree-gist = [] +extension-citext = [] +extension-cube = [] +extension-dict-int = [] +extension-dict-xsyn = [] +extension-earthdistance = [] +extension-file-fdw = [] +extension-fuzzystrmatch = [] +extension-hstore = [] +extension-intarray = [] +extension-isn = [] +extension-lo = [] +extension-ltree = [] +extension-pageinspect = [] +extension-pg-buffercache = [] +extension-pg-freespacemap = [] +extension-pg-hashids = [] +extension-pg-ivm = [] +extension-pg-surgery = [] +extension-pg-textsearch = [] +extension-pg-trgm = [] +extension-pg-uuidv7 = [] +extension-pg-visibility = [] +extension-pg-walinspect = [] +extension-pgcrypto = [] +extension-pgtap = [] +extension-postgis = [] +extension-seg = [] +extension-tablefunc = [] +extension-tcn = [] +extension-tsm-system-rows = [] +extension-tsm-system-time = [] +extension-unaccent = [] +extension-uuid-ossp = [] +extension-vector = [] + [lib] path = "src/lib.rs" diff --git a/src/runtimes/liboliphaunt/wasix/crates/assets/build.rs b/src/runtimes/liboliphaunt/wasix/crates/assets/build.rs index d1b4e543..6cbb9f1d 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/assets/build.rs +++ b/src/runtimes/liboliphaunt/wasix/crates/assets/build.rs @@ -10,20 +10,365 @@ const ARTIFACT_PRODUCT: &str = "liboliphaunt-wasix"; const ARTIFACT_KIND: &str = "wasix-runtime"; const ARTIFACT_TARGET: &str = "portable"; +#[derive(Debug, Clone, Copy)] +struct ExtensionPackage { + #[allow(dead_code)] + feature: &'static str, + env: &'static str, + product: &'static str, + sql_name: &'static str, + crate_ident: &'static str, +} + +#[derive(Debug)] +struct SelectedExtension { + package: ExtensionPackage, + archive: ExtensionArchiveSource, + aot_packages: Vec, +} + +#[derive(Debug)] +enum ExtensionArchiveSource { + Crate, + Local { + path: PathBuf, + sha256: String, + size: u64, + }, + Missing, +} + +#[derive(Debug, Clone, Copy)] +struct ExtensionAotTarget { + target: &'static str, + cfg: &'static str, +} + +#[derive(Debug)] +struct SelectedExtensionAotPackage { + target: ExtensionAotTarget, + crate_ident: String, +} + +const EXTENSION_AOT_TARGETS: &[ExtensionAotTarget] = &[ + ExtensionAotTarget { + target: "aarch64-apple-darwin", + cfg: r#"all(target_os = "macos", target_arch = "aarch64")"#, + }, + ExtensionAotTarget { + target: "aarch64-unknown-linux-gnu", + cfg: r#"all(target_os = "linux", target_arch = "aarch64", target_env = "gnu")"#, + }, + ExtensionAotTarget { + target: "x86_64-unknown-linux-gnu", + cfg: r#"all(target_os = "linux", target_arch = "x86_64", target_env = "gnu")"#, + }, + ExtensionAotTarget { + target: "x86_64-pc-windows-msvc", + cfg: r#"all(target_os = "windows", target_arch = "x86_64", target_env = "msvc")"#, + }, +]; + +const EXTENSION_PACKAGES: &[ExtensionPackage] = &[ + ExtensionPackage { + feature: "extension-amcheck", + env: "CARGO_FEATURE_EXTENSION_AMCHECK", + product: "oliphaunt-extension-amcheck", + sql_name: "amcheck", + crate_ident: "oliphaunt_extension_amcheck", + }, + ExtensionPackage { + feature: "extension-auto-explain", + env: "CARGO_FEATURE_EXTENSION_AUTO_EXPLAIN", + product: "oliphaunt-extension-auto-explain", + sql_name: "auto_explain", + crate_ident: "oliphaunt_extension_auto_explain", + }, + ExtensionPackage { + feature: "extension-bloom", + env: "CARGO_FEATURE_EXTENSION_BLOOM", + product: "oliphaunt-extension-bloom", + sql_name: "bloom", + crate_ident: "oliphaunt_extension_bloom", + }, + ExtensionPackage { + feature: "extension-btree-gin", + env: "CARGO_FEATURE_EXTENSION_BTREE_GIN", + product: "oliphaunt-extension-btree-gin", + sql_name: "btree_gin", + crate_ident: "oliphaunt_extension_btree_gin", + }, + ExtensionPackage { + feature: "extension-btree-gist", + env: "CARGO_FEATURE_EXTENSION_BTREE_GIST", + product: "oliphaunt-extension-btree-gist", + sql_name: "btree_gist", + crate_ident: "oliphaunt_extension_btree_gist", + }, + ExtensionPackage { + feature: "extension-citext", + env: "CARGO_FEATURE_EXTENSION_CITEXT", + product: "oliphaunt-extension-citext", + sql_name: "citext", + crate_ident: "oliphaunt_extension_citext", + }, + ExtensionPackage { + feature: "extension-cube", + env: "CARGO_FEATURE_EXTENSION_CUBE", + product: "oliphaunt-extension-cube", + sql_name: "cube", + crate_ident: "oliphaunt_extension_cube", + }, + ExtensionPackage { + feature: "extension-dict-int", + env: "CARGO_FEATURE_EXTENSION_DICT_INT", + product: "oliphaunt-extension-dict-int", + sql_name: "dict_int", + crate_ident: "oliphaunt_extension_dict_int", + }, + ExtensionPackage { + feature: "extension-dict-xsyn", + env: "CARGO_FEATURE_EXTENSION_DICT_XSYN", + product: "oliphaunt-extension-dict-xsyn", + sql_name: "dict_xsyn", + crate_ident: "oliphaunt_extension_dict_xsyn", + }, + ExtensionPackage { + feature: "extension-earthdistance", + env: "CARGO_FEATURE_EXTENSION_EARTHDISTANCE", + product: "oliphaunt-extension-earthdistance", + sql_name: "earthdistance", + crate_ident: "oliphaunt_extension_earthdistance", + }, + ExtensionPackage { + feature: "extension-file-fdw", + env: "CARGO_FEATURE_EXTENSION_FILE_FDW", + product: "oliphaunt-extension-file-fdw", + sql_name: "file_fdw", + crate_ident: "oliphaunt_extension_file_fdw", + }, + ExtensionPackage { + feature: "extension-fuzzystrmatch", + env: "CARGO_FEATURE_EXTENSION_FUZZYSTRMATCH", + product: "oliphaunt-extension-fuzzystrmatch", + sql_name: "fuzzystrmatch", + crate_ident: "oliphaunt_extension_fuzzystrmatch", + }, + ExtensionPackage { + feature: "extension-hstore", + env: "CARGO_FEATURE_EXTENSION_HSTORE", + product: "oliphaunt-extension-hstore", + sql_name: "hstore", + crate_ident: "oliphaunt_extension_hstore", + }, + ExtensionPackage { + feature: "extension-intarray", + env: "CARGO_FEATURE_EXTENSION_INTARRAY", + product: "oliphaunt-extension-intarray", + sql_name: "intarray", + crate_ident: "oliphaunt_extension_intarray", + }, + ExtensionPackage { + feature: "extension-isn", + env: "CARGO_FEATURE_EXTENSION_ISN", + product: "oliphaunt-extension-isn", + sql_name: "isn", + crate_ident: "oliphaunt_extension_isn", + }, + ExtensionPackage { + feature: "extension-lo", + env: "CARGO_FEATURE_EXTENSION_LO", + product: "oliphaunt-extension-lo", + sql_name: "lo", + crate_ident: "oliphaunt_extension_lo", + }, + ExtensionPackage { + feature: "extension-ltree", + env: "CARGO_FEATURE_EXTENSION_LTREE", + product: "oliphaunt-extension-ltree", + sql_name: "ltree", + crate_ident: "oliphaunt_extension_ltree", + }, + ExtensionPackage { + feature: "extension-pageinspect", + env: "CARGO_FEATURE_EXTENSION_PAGEINSPECT", + product: "oliphaunt-extension-pageinspect", + sql_name: "pageinspect", + crate_ident: "oliphaunt_extension_pageinspect", + }, + ExtensionPackage { + feature: "extension-pg-buffercache", + env: "CARGO_FEATURE_EXTENSION_PG_BUFFERCACHE", + product: "oliphaunt-extension-pg-buffercache", + sql_name: "pg_buffercache", + crate_ident: "oliphaunt_extension_pg_buffercache", + }, + ExtensionPackage { + feature: "extension-pg-freespacemap", + env: "CARGO_FEATURE_EXTENSION_PG_FREESPACEMAP", + product: "oliphaunt-extension-pg-freespacemap", + sql_name: "pg_freespacemap", + crate_ident: "oliphaunt_extension_pg_freespacemap", + }, + ExtensionPackage { + feature: "extension-pg-surgery", + env: "CARGO_FEATURE_EXTENSION_PG_SURGERY", + product: "oliphaunt-extension-pg-surgery", + sql_name: "pg_surgery", + crate_ident: "oliphaunt_extension_pg_surgery", + }, + ExtensionPackage { + feature: "extension-pg-trgm", + env: "CARGO_FEATURE_EXTENSION_PG_TRGM", + product: "oliphaunt-extension-pg-trgm", + sql_name: "pg_trgm", + crate_ident: "oliphaunt_extension_pg_trgm", + }, + ExtensionPackage { + feature: "extension-pg-visibility", + env: "CARGO_FEATURE_EXTENSION_PG_VISIBILITY", + product: "oliphaunt-extension-pg-visibility", + sql_name: "pg_visibility", + crate_ident: "oliphaunt_extension_pg_visibility", + }, + ExtensionPackage { + feature: "extension-pg-walinspect", + env: "CARGO_FEATURE_EXTENSION_PG_WALINSPECT", + product: "oliphaunt-extension-pg-walinspect", + sql_name: "pg_walinspect", + crate_ident: "oliphaunt_extension_pg_walinspect", + }, + ExtensionPackage { + feature: "extension-pgcrypto", + env: "CARGO_FEATURE_EXTENSION_PGCRYPTO", + product: "oliphaunt-extension-pgcrypto", + sql_name: "pgcrypto", + crate_ident: "oliphaunt_extension_pgcrypto", + }, + ExtensionPackage { + feature: "extension-seg", + env: "CARGO_FEATURE_EXTENSION_SEG", + product: "oliphaunt-extension-seg", + sql_name: "seg", + crate_ident: "oliphaunt_extension_seg", + }, + ExtensionPackage { + feature: "extension-tablefunc", + env: "CARGO_FEATURE_EXTENSION_TABLEFUNC", + product: "oliphaunt-extension-tablefunc", + sql_name: "tablefunc", + crate_ident: "oliphaunt_extension_tablefunc", + }, + ExtensionPackage { + feature: "extension-tcn", + env: "CARGO_FEATURE_EXTENSION_TCN", + product: "oliphaunt-extension-tcn", + sql_name: "tcn", + crate_ident: "oliphaunt_extension_tcn", + }, + ExtensionPackage { + feature: "extension-tsm-system-rows", + env: "CARGO_FEATURE_EXTENSION_TSM_SYSTEM_ROWS", + product: "oliphaunt-extension-tsm-system-rows", + sql_name: "tsm_system_rows", + crate_ident: "oliphaunt_extension_tsm_system_rows", + }, + ExtensionPackage { + feature: "extension-tsm-system-time", + env: "CARGO_FEATURE_EXTENSION_TSM_SYSTEM_TIME", + product: "oliphaunt-extension-tsm-system-time", + sql_name: "tsm_system_time", + crate_ident: "oliphaunt_extension_tsm_system_time", + }, + ExtensionPackage { + feature: "extension-unaccent", + env: "CARGO_FEATURE_EXTENSION_UNACCENT", + product: "oliphaunt-extension-unaccent", + sql_name: "unaccent", + crate_ident: "oliphaunt_extension_unaccent", + }, + ExtensionPackage { + feature: "extension-uuid-ossp", + env: "CARGO_FEATURE_EXTENSION_UUID_OSSP", + product: "oliphaunt-extension-uuid-ossp", + sql_name: "uuid-ossp", + crate_ident: "oliphaunt_extension_uuid_ossp", + }, + ExtensionPackage { + feature: "extension-pg-hashids", + env: "CARGO_FEATURE_EXTENSION_PG_HASHIDS", + product: "oliphaunt-extension-pg-hashids", + sql_name: "pg_hashids", + crate_ident: "oliphaunt_extension_pg_hashids", + }, + ExtensionPackage { + feature: "extension-pg-ivm", + env: "CARGO_FEATURE_EXTENSION_PG_IVM", + product: "oliphaunt-extension-pg-ivm", + sql_name: "pg_ivm", + crate_ident: "oliphaunt_extension_pg_ivm", + }, + ExtensionPackage { + feature: "extension-pg-textsearch", + env: "CARGO_FEATURE_EXTENSION_PG_TEXTSEARCH", + product: "oliphaunt-extension-pg-textsearch", + sql_name: "pg_textsearch", + crate_ident: "oliphaunt_extension_pg_textsearch", + }, + ExtensionPackage { + feature: "extension-pg-uuidv7", + env: "CARGO_FEATURE_EXTENSION_PG_UUIDV7", + product: "oliphaunt-extension-pg-uuidv7", + sql_name: "pg_uuidv7", + crate_ident: "oliphaunt_extension_pg_uuidv7", + }, + ExtensionPackage { + feature: "extension-pgtap", + env: "CARGO_FEATURE_EXTENSION_PGTAP", + product: "oliphaunt-extension-pgtap", + sql_name: "pgtap", + crate_ident: "oliphaunt_extension_pgtap", + }, + ExtensionPackage { + feature: "extension-postgis", + env: "CARGO_FEATURE_EXTENSION_POSTGIS", + product: "oliphaunt-extension-postgis", + sql_name: "postgis", + crate_ident: "oliphaunt_extension_postgis", + }, + ExtensionPackage { + feature: "extension-vector", + env: "CARGO_FEATURE_EXTENSION_VECTOR", + product: "oliphaunt-extension-vector", + sql_name: "vector", + crate_ident: "oliphaunt_extension_vector", + }, +]; + fn main() { println!("cargo:rerun-if-env-changed=OLIPHAUNT_WASM_GENERATED_ASSETS_DIR"); + println!("cargo:rerun-if-env-changed=OLIPHAUNT_WASIX_EXTENSION_ARTIFACT_ROOT"); + for package in EXTENSION_PACKAGES { + println!("cargo:rerun-if-env-changed={}", package.env); + } emit_expected_asset_inputs(); + let manifest_dir = PathBuf::from( + env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Cargo"), + ); let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set by Cargo")); let out = out_dir.join("generated_assets.rs"); + let manifest_text = + fs::read_to_string(manifest_dir.join("Cargo.toml")).expect("read Cargo.toml"); + let selected_extensions = selected_extensions(&manifest_dir, &manifest_text); if let Some(asset_dir) = find_asset_dir() { emit_rerun_directives(&asset_dir); - write_generated_assets(&out, &asset_dir); + write_generated_assets(&out, &asset_dir, &selected_extensions); } else if env::var_os("OLIPHAUNT_ARTIFACT_CRATE_REQUIRE_PAYLOAD").is_some() { panic!("release packaging requires package-local WASIX runtime payload"); } else { - write_source_only_assets(&out); + write_source_only_assets(&out, &selected_extensions); } } @@ -105,13 +450,13 @@ fn visit_files(path: &Path, f: &mut impl FnMut(&Path)) { } } -fn write_generated_assets(out: &Path, asset_dir: &Path) { +fn write_generated_assets(out: &Path, asset_dir: &Path, selected_extensions: &[SelectedExtension]) { let manifest = asset_dir.join("manifest.json"); let generated_manifest = out .parent() .expect("generated asset output has parent") .join("manifest.json"); - write_core_manifest(&manifest, &generated_manifest); + write_core_manifest(&manifest, &generated_manifest, selected_extensions); let runtime = asset_dir.join("oliphaunt.wasix.tar.zst"); let pgdata_archive = asset_dir.join("prepopulated/pgdata-template.tar.zst"); let pgdata_manifest = asset_dir.join("prepopulated/pgdata-template.json"); @@ -137,22 +482,36 @@ fn write_generated_assets(out: &Path, asset_dir: &Path) { let pgdata_archive_body = optional_include_bytes_body(&pgdata_archive); let pgdata_manifest_body = optional_include_bytes_body(&pgdata_manifest); let pg_dump_body = optional_include_bytes_body(&pg_dump); + let extension_sql_names = selected_extension_sql_names_body(selected_extensions); + let extension_archive_body = extension_archive_body(selected_extensions); + let extension_sha256_body = expected_extension_archive_sha256_body(selected_extensions); + let extension_aot_manifest_body = extension_aot_manifest_json_body(selected_extensions); + let extension_aot_bytes_body = extension_aot_artifact_bytes_body(selected_extensions); let text = format!( "pub const HAS_EMBEDDED_ASSETS: bool = true;\n\ + pub const SELECTED_EXTENSION_SQL_NAMES: &[&str] = {extension_sql_names};\n\ pub const MANIFEST_JSON: &str = include_str!({manifest});\n\ pub fn runtime_archive() -> Option<&'static [u8]> {{ Some(include_bytes!({runtime})) }}\n\ pub fn pgdata_template_archive() -> Option<&'static [u8]> {{ {pgdata_archive_body} }}\n\ pub fn pgdata_template_manifest() -> Option<&'static [u8]> {{ {pgdata_manifest_body} }}\n\ pub fn pg_dump_wasm() -> Option<&'static [u8]> {{ {pg_dump_body} }}\n\ pub fn initdb_wasm() -> Option<&'static [u8]> {{ Some(include_bytes!({initdb})) }}\n\ - pub fn extension_archive(_name: &str) -> Option<&'static [u8]> {{ None }}\n", + pub fn extension_archive(name: &str) -> Option<&'static [u8]> {{\n{extension_archive_body} }}\n\ + pub fn expected_extension_archive_sha256(name: &str) -> Option<&'static str> {{\n{extension_sha256_body} }}\n\ + pub fn extension_aot_manifest_json(target: &str, sql_name: &str) -> Option<&'static str> {{\n{extension_aot_manifest_body} }}\n\ + pub fn extension_aot_artifact_bytes(target: &str, name: &str) -> Option<&'static [u8]> {{\n{extension_aot_bytes_body} }}\n", manifest = rust_string_literal(&generated_manifest), runtime = rust_string_literal(&runtime), pgdata_archive_body = pgdata_archive_body, pgdata_manifest_body = pgdata_manifest_body, pg_dump_body = pg_dump_body, initdb = rust_string_literal(&initdb), + extension_sql_names = extension_sql_names, + extension_archive_body = extension_archive_body, + extension_sha256_body = extension_sha256_body, + extension_aot_manifest_body = extension_aot_manifest_body, + extension_aot_bytes_body = extension_aot_bytes_body, ); fs::write(out, text).expect("write generated asset include module"); emit_artifact_manifest( @@ -169,16 +528,35 @@ fn write_generated_assets(out: &Path, asset_dir: &Path) { ); } -fn write_source_only_assets(out: &Path) { - let text = r##"pub const HAS_EMBEDDED_ASSETS: bool = false; -pub const MANIFEST_JSON: &str = r#"{"format-version":1,"runtime":{"archive":"","sha256":"","module-sha256":"","postgres-version":"","runtime-kind":"source-only-template"},"runtime-support":[],"pg-dump":null,"extensions":[],"sources":[]}"#; +fn write_source_only_assets(out: &Path, selected_extensions: &[SelectedExtension]) { + let extension_sql_names = selected_extension_sql_names_body(selected_extensions); + let extension_archive_body = extension_archive_body(selected_extensions); + let extension_sha256_body = expected_extension_archive_sha256_body(selected_extensions); + let extension_aot_manifest_body = extension_aot_manifest_json_body(selected_extensions); + let extension_aot_bytes_body = extension_aot_artifact_bytes_body(selected_extensions); + let mut text = format!( + "pub const HAS_EMBEDDED_ASSETS: bool = false;\n\ + pub const SELECTED_EXTENSION_SQL_NAMES: &[&str] = {extension_sql_names};\n" + ); + text.push_str( + r##"pub const MANIFEST_JSON: &str = r#"{"format-version":1,"runtime":{"archive":"","sha256":"","module-sha256":"","postgres-version":"","runtime-kind":"source-only-template"},"runtime-support":[],"pg-dump":null,"extensions":[],"sources":[]}"#; pub fn runtime_archive() -> Option<&'static [u8]> { None } pub fn pgdata_template_archive() -> Option<&'static [u8]> { None } pub fn pgdata_template_manifest() -> Option<&'static [u8]> { None } pub fn pg_dump_wasm() -> Option<&'static [u8]> { None } pub fn initdb_wasm() -> Option<&'static [u8]> { None } -pub fn extension_archive(_name: &str) -> Option<&'static [u8]> { None } -"##; +"##, + ); + text.push_str(&format!( + "pub fn extension_archive(name: &str) -> Option<&'static [u8]> {{\n\ +{extension_archive_body}}}\n\ + pub fn expected_extension_archive_sha256(name: &str) -> Option<&'static str> {{\n\ +{extension_sha256_body}}}\n\ + pub fn extension_aot_manifest_json(target: &str, sql_name: &str) -> Option<&'static str> {{\n\ +{extension_aot_manifest_body}}}\n\ + pub fn extension_aot_artifact_bytes(target: &str, name: &str) -> Option<&'static [u8]> {{\n\ +{extension_aot_bytes_body}}}\n" + )); fs::write(out, text).expect("write source-only asset include module"); } @@ -194,16 +572,224 @@ fn optional_include_bytes_body(path: &Path) -> String { } } -fn write_core_manifest(source: &Path, destination: &Path) { +fn write_core_manifest( + source: &Path, + destination: &Path, + selected_extensions: &[SelectedExtension], +) { let text = fs::read_to_string(source).expect("read generated WASIX asset manifest"); let mut manifest: serde_json::Value = serde_json::from_str(&text).expect("parse generated WASIX asset manifest"); - manifest["extensions"] = serde_json::Value::Array(Vec::new()); + manifest["extensions"] = serde_json::Value::Array( + selected_extensions + .iter() + .filter_map(extension_manifest_entry) + .collect(), + ); let rendered = serde_json::to_string_pretty(&manifest).expect("serialize core WASIX asset manifest"); fs::write(destination, format!("{rendered}\n")).expect("write core WASIX asset manifest"); } +fn selected_extensions(manifest_dir: &Path, manifest_text: &str) -> Vec { + let repo_root = repo_root_from_manifest_dir(manifest_dir).map(Path::to_path_buf); + EXTENSION_PACKAGES + .iter() + .copied() + .filter_map(|package| { + if env::var_os(package.env).is_none() { + return None; + } + let archive = if manifest_declares_dependency(manifest_text, package.product) { + ExtensionArchiveSource::Crate + } else if let Some(path) = + find_local_extension_archive(manifest_dir, repo_root.as_deref(), package) + { + println!("cargo:rerun-if-changed={}", path.display()); + let sha256 = + sha256_file(&path).expect("hash selected local WASIX extension archive"); + let size = path + .metadata() + .expect("stat selected local WASIX extension archive") + .len(); + ExtensionArchiveSource::Local { path, sha256, size } + } else { + ExtensionArchiveSource::Missing + }; + let aot_packages = selected_extension_aot_packages(manifest_text, package); + Some(SelectedExtension { + package, + archive, + aot_packages, + }) + }) + .collect() +} + +fn selected_extension_aot_packages( + manifest_text: &str, + package: ExtensionPackage, +) -> Vec { + EXTENSION_AOT_TARGETS + .iter() + .copied() + .filter_map(|target| { + let package_name = extension_aot_package_name(package, target); + manifest_declares_dependency(manifest_text, &package_name).then(|| { + SelectedExtensionAotPackage { + target, + crate_ident: crate_ident(&package_name), + } + }) + }) + .collect() +} + +fn extension_aot_package_name(package: ExtensionPackage, target: ExtensionAotTarget) -> String { + format!("{}-aot-{}", package.product, target.target) +} + +fn crate_ident(package_name: &str) -> String { + package_name.replace('-', "_") +} + +fn manifest_declares_dependency(manifest_text: &str, package_name: &str) -> bool { + manifest_text + .lines() + .any(|line| line.trim_start().starts_with(&format!("{package_name} ="))) +} + +fn find_local_extension_archive( + manifest_dir: &Path, + repo_root: Option<&Path>, + package: ExtensionPackage, +) -> Option { + let version = env::var("CARGO_PKG_VERSION").expect("CARGO_PKG_VERSION is set by Cargo"); + let archive_name = format!("{}-{version}-wasix-portable.tar.zst", package.product); + let mut roots = Vec::new(); + if let Some(path) = env::var_os("OLIPHAUNT_WASIX_EXTENSION_ARTIFACT_ROOT") { + roots.push(PathBuf::from(path)); + } + if let Some(repo_root) = repo_root { + roots.push(repo_root.join("target/extension-artifacts")); + roots.push( + repo_root.join("target/local-registry-artifacts/oliphaunt-extension-package-artifacts"), + ); + } + roots.push(manifest_dir.join("extension-artifacts")); + + for root in roots { + for candidate in [ + root.join(package.product) + .join("release-assets") + .join(&archive_name), + root.join("oliphaunt-extension-package-artifacts") + .join(package.product) + .join("release-assets") + .join(&archive_name), + ] { + if candidate.is_file() { + return Some(candidate); + } + } + } + None +} + +fn selected_extension_sql_names_body(selected_extensions: &[SelectedExtension]) -> String { + let sql_names = selected_extensions + .iter() + .map(|extension| format!("{:?}", extension.package.sql_name)) + .collect::>() + .join(", "); + format!("&[{sql_names}]") +} + +fn extension_archive_body(selected_extensions: &[SelectedExtension]) -> String { + let mut body = String::from(" match name {\n"); + for extension in selected_extensions { + let sql_name = extension.package.sql_name; + let expression = match &extension.archive { + ExtensionArchiveSource::Crate => { + format!("{}::archive()", extension.package.crate_ident) + } + ExtensionArchiveSource::Local { path, .. } => { + format!("Some(include_bytes!({}))", rust_string_literal(path)) + } + ExtensionArchiveSource::Missing => "None".to_owned(), + }; + body.push_str(&format!(" {sql_name:?} => {expression},\n")); + } + body.push_str(" _ => None,\n }\n"); + body +} + +fn expected_extension_archive_sha256_body(selected_extensions: &[SelectedExtension]) -> String { + let mut body = String::from(" match name {\n"); + for extension in selected_extensions { + let sql_name = extension.package.sql_name; + let expression = match &extension.archive { + ExtensionArchiveSource::Crate => { + format!("Some({}::ARCHIVE_SHA256)", extension.package.crate_ident) + } + ExtensionArchiveSource::Local { sha256, .. } => { + format!("Some({sha256:?})") + } + ExtensionArchiveSource::Missing => "None".to_owned(), + }; + body.push_str(&format!(" {sql_name:?} => {expression},\n")); + } + body.push_str(" _ => None,\n }\n"); + body +} + +fn extension_aot_manifest_json_body(selected_extensions: &[SelectedExtension]) -> String { + let mut body = String::from(" match (target, sql_name) {\n"); + for extension in selected_extensions { + let sql_name = extension.package.sql_name; + for aot in &extension.aot_packages { + body.push_str(&format!( + " #[cfg({})]\n ({:?}, {:?}) => {}::aot_manifest_json(),\n", + aot.target.cfg, + aot.target.target, + sql_name, + aot.crate_ident, + )); + } + } + body.push_str(" _ => None,\n }\n"); + body +} + +fn extension_aot_artifact_bytes_body(selected_extensions: &[SelectedExtension]) -> String { + let mut body = String::from(" let _ = (target, name);\n"); + for extension in selected_extensions { + for aot in &extension.aot_packages { + body.push_str(&format!( + " #[cfg({})]\n if target == {:?} {{\n if let Some(bytes) = {}::aot_artifact_bytes(name) {{\n return Some(bytes);\n }}\n }}\n", + aot.target.cfg, + aot.target.target, + aot.crate_ident, + )); + } + } + body.push_str(" None\n"); + body +} + +fn extension_manifest_entry(extension: &SelectedExtension) -> Option { + match &extension.archive { + ExtensionArchiveSource::Local { sha256, size, .. } => Some(serde_json::json!({ + "name": extension.package.sql_name, + "sql-name": extension.package.sql_name, + "archive": format!("extensions/{}.tar.zst", extension.package.sql_name), + "sha256": sha256, + "size": size, + })), + ExtensionArchiveSource::Crate | ExtensionArchiveSource::Missing => None, + } +} + fn emit_artifact_manifest(out_dir: &Path, asset_dir: &Path, files: &[&Path]) { let version = env::var("CARGO_PKG_VERSION").expect("CARGO_PKG_VERSION is set by Cargo"); let manifest_path = out_dir.join("oliphaunt-artifact.toml"); diff --git a/src/runtimes/liboliphaunt/wasix/crates/assets/src/lib.rs b/src/runtimes/liboliphaunt/wasix/crates/assets/src/lib.rs index 98641fad..067f9a5a 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/assets/src/lib.rs +++ b/src/runtimes/liboliphaunt/wasix/crates/assets/src/lib.rs @@ -231,12 +231,16 @@ mod tests { let manifest = manifest().expect("asset manifest should parse"); if !HAS_EMBEDDED_ASSETS { assert_eq!(manifest.runtime.runtime_kind, "source-only-template"); - assert!(manifest.extensions.is_empty()); + if SELECTED_EXTENSION_SQL_NAMES.is_empty() { + assert!(manifest.extensions.is_empty()); + } return; } assert_eq!(manifest.runtime.postgres_version, "18.4"); assert_eq!(manifest.runtime.runtime_kind, "wasix-dynamic-main"); - assert!(manifest.extensions.is_empty()); + if SELECTED_EXTENSION_SQL_NAMES.is_empty() { + assert!(manifest.extensions.is_empty()); + } } #[test] diff --git a/src/runtimes/liboliphaunt/wasix/tools/build-aot-target.sh b/src/runtimes/liboliphaunt/wasix/tools/build-aot-target.sh index 3f934411..3f36e575 100755 --- a/src/runtimes/liboliphaunt/wasix/tools/build-aot-target.sh +++ b/src/runtimes/liboliphaunt/wasix/tools/build-aot-target.sh @@ -20,6 +20,7 @@ package="${AOT_PACKAGE:-oliphaunt-wasix-aot-${target}}" cargo run -p xtask -- assets aot --target-triple "$target" cargo run -p xtask -- assets package-aot --target-triple "$target" +cargo run -p xtask -- assets package-extension-aot --target-triple "$target" cargo run -p xtask -- assets check-aot --target-triple "$target" cargo check -p "$package" --locked cargo run -p xtask -- assets smoke diff --git a/tools/graph/ci_plan.py b/tools/graph/ci_plan.py index a6b0388b..f28f23b2 100644 --- a/tools/graph/ci_plan.py +++ b/tools/graph/ci_plan.py @@ -197,6 +197,7 @@ def add_implied_jobs(jobs: set[str], tasks: set[str]) -> None: if jobs & WASIX_EXTENSION_ARTIFACT_PORTABLE_CONSUMER_JOBS: jobs.add("extension-artifacts-wasix") jobs.add("liboliphaunt-wasix-runtime") + jobs.add("liboliphaunt-wasix-aot") def plan_jobs_for_affected( diff --git a/tools/release/build-extension-ci-artifacts.py b/tools/release/build-extension-ci-artifacts.py index 88b5c73e..60e6a4d7 100755 --- a/tools/release/build-extension-ci-artifacts.py +++ b/tools/release/build-extension-ci-artifacts.py @@ -103,6 +103,13 @@ def wasix_release_asset_root() -> Path: ) +def wasix_aot_artifact_root() -> Path: + return resolve_repo_path( + os.environ.get("OLIPHAUNT_WASIX_EXTENSION_AOT_ARTIFACT_ROOT", "target/extensions/wasix/aot-artifacts"), + label="WASIX extension AOT artifact root", + ) + + def index_contains_sql_name(index: Path, sql_name: str) -> bool: with index.open("r", encoding="utf-8", newline="") as handle: return any(row.get("sql_name") == sql_name for row in csv.DictReader(handle, delimiter="\t")) @@ -215,6 +222,18 @@ def wasix_archive_for(sql_name: str, *, product: str | None = None, required: bo return None +def wasix_aot_dirs_for(sql_name: str) -> list[tuple[str, Path]]: + root = wasix_aot_artifact_root() + if not root.is_dir(): + return [] + dirs: list[tuple[str, Path]] = [] + for target_root in sorted(child for child in root.iterdir() if child.is_dir()): + candidate = target_root / sql_name + if (candidate / "manifest.json").is_file(): + dirs.append((target_root.name, candidate)) + return dirs + + def copy_asset(source: Path, destination_dir: Path, *, name: str) -> dict[str, object]: destination_dir.mkdir(parents=True, exist_ok=True) destination = destination_dir / name @@ -365,6 +384,12 @@ def stage_product( metadata["target"] = "wasix-portable" assets.append(metadata) + for target_id, source in wasix_aot_dirs_for(sql_name): + destination = product_root / "wasix-aot" / target_id + if destination.exists(): + shutil.rmtree(destination) + shutil.copytree(source, destination) + validate_staged_targets( product, assets, diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index 0a394d7b..1cba30ee 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -50,6 +50,10 @@ "liboliphaunt-native-release-assets-macos-arm64", "liboliphaunt-native-release-assets-windows-x64-msvc", "liboliphaunt-wasix-extension-artifacts-wasix-portable", + "liboliphaunt-wasix-extension-aot-linux-arm64-gnu", + "liboliphaunt-wasix-extension-aot-linux-x64-gnu", + "liboliphaunt-wasix-extension-aot-macos-arm64", + "liboliphaunt-wasix-extension-aot-windows-x64-msvc", "liboliphaunt-wasix-release-assets", "liboliphaunt-wasix-runtime-aot-linux-arm64-gnu", "liboliphaunt-wasix-runtime-aot-linux-x64-gnu", diff --git a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py index f3fcd60e..e2df3bef 100644 --- a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py +++ b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py @@ -11,6 +11,7 @@ import shutil import subprocess import sys +import tarfile from dataclasses import dataclass from pathlib import Path, PurePosixPath from typing import NoReturn @@ -37,6 +38,12 @@ "linux-x64-gnu": "x86_64-unknown-linux-gnu", "windows-x64-msvc": "x86_64-pc-windows-msvc", } +AOT_TARGET_CFGS = { + "aarch64-apple-darwin": 'cfg(all(target_os = "macos", target_arch = "aarch64"))', + "aarch64-unknown-linux-gnu": 'cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))', + "x86_64-unknown-linux-gnu": 'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))', + "x86_64-pc-windows-msvc": 'cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))', +} @dataclass(frozen=True) @@ -60,6 +67,38 @@ class GeneratedPackage: sha256: str +@dataclass(frozen=True) +class ExtensionCargoSpec: + name: str + version: str + sql_name: str + archive: Path + sha256: str + size: int + aot_targets: tuple["ExtensionAotCargoSpec", ...] + + +@dataclass(frozen=True) +class ExtensionAotCargoSpec: + name: str + version: str + sql_name: str + target: str + source_dir: Path + + +@dataclass(frozen=True) +class ExtensionCargoSource: + spec: ExtensionCargoSpec + source_dir: Path + + +@dataclass(frozen=True) +class ExtensionAotCargoSource: + spec: ExtensionAotCargoSpec + source_dir: Path + + def fail(message: str) -> NoReturn: print(f"package_liboliphaunt_wasix_cargo_artifacts.py: {message}", file=sys.stderr) raise SystemExit(1) @@ -268,10 +307,19 @@ def validate_aot_payload(root: Path) -> None: fail(f"WASIX AOT Cargo payload file set mismatch for {rel(root)}: expected {sorted(expected)}, got {sorted(actual)}") -def rewrite_cargo_manifest(manifest: Path, *, package_name: str, version: str) -> None: +def rewrite_cargo_manifest( + manifest: Path, + *, + package_name: str, + version: str, + extension_sources: list[ExtensionCargoSource], + extension_aot_sources: list[ExtensionAotCargoSource], +) -> None: text = manifest.read_text(encoding="utf-8") text = re.sub(r'(?m)^version = "[^"]+"$', f'version = "{version}"', text, count=1) text = re.sub(r'(?m)^publish = false\n?', "", text) + if package_name == RUNTIME_PACKAGE and extension_sources: + text = inject_runtime_extension_dependencies(text, extension_sources, extension_aot_sources) if "\n[workspace]" not in text: text = text.rstrip() + "\n\n[workspace]\n" manifest.write_text(text, encoding="utf-8") @@ -283,7 +331,55 @@ def rewrite_cargo_manifest(manifest: Path, *, package_name: str, version: str) - ) -def copy_package_source(spec: PackageSpec, source_root: Path, version: str) -> Path: +def inject_runtime_extension_dependencies( + text: str, + extension_sources: list[ExtensionCargoSource], + extension_aot_sources: list[ExtensionAotCargoSource], +) -> str: + dependency_lines = [] + target_dependency_lines: dict[str, list[str]] = {} + aot_by_extension: dict[str, list[ExtensionAotCargoSource]] = {} + for source in extension_aot_sources: + aot_by_extension.setdefault(source.spec.sql_name, []).append(source) + for source in extension_sources: + package = source.spec.name + dependency_lines.append( + f'{package} = {{ version = "={source.spec.version}", path = "../{package}", optional = true }}' + ) + feature = extension_feature_name(package) + feature_deps = [f"dep:{package}"] + for aot_source in sorted(aot_by_extension.get(source.spec.sql_name, []), key=lambda item: item.spec.name): + feature_deps.append(f"dep:{aot_source.spec.name}") + replacement = f'{feature} = [{", ".join(json.dumps(dep) for dep in feature_deps)}]' + pattern = rf"(?m)^{re.escape(feature)} = \[[^\n]*\]$" + text, count = re.subn(pattern, replacement, text, count=1) + if count == 0: + text = text.replace("[features]\n", f"[features]\n{replacement}\n", 1) + for source in extension_aot_sources: + cfg = AOT_TARGET_CFGS.get(source.spec.target) + if cfg is None: + fail(f"unsupported extension AOT target {source.spec.target}") + target_dependency_lines.setdefault(cfg, []).append( + f'{source.spec.name} = {{ version = "={source.spec.version}", path = "../{source.spec.name}", optional = true }}' + ) + if dependency_lines: + block = "\n".join(dependency_lines) + text = text.replace("\n[build-dependencies]", f"\n{block}\n\n[build-dependencies]", 1) + if target_dependency_lines: + blocks = [] + for cfg, lines in sorted(target_dependency_lines.items()): + blocks.append(f"[target.'{cfg}'.dependencies]\n" + "\n".join(sorted(lines))) + text = text.replace("\n[build-dependencies]", "\n" + "\n\n".join(blocks) + "\n\n[build-dependencies]", 1) + return text + + +def copy_package_source( + spec: PackageSpec, + source_root: Path, + version: str, + extension_sources: list[ExtensionCargoSource], + extension_aot_sources: list[ExtensionAotCargoSource], +) -> Path: crate_dir = source_root / spec.name if crate_dir.exists(): fail(f"duplicate generated WASIX Cargo package source: {rel(crate_dir)}") @@ -293,7 +389,13 @@ def copy_package_source(spec: PackageSpec, source_root: Path, version: str) -> P ignore=shutil.ignore_patterns("target", "payload", "artifacts"), ) shutil.copytree(spec.payload_root, crate_dir / spec.payload_dir_name) - rewrite_cargo_manifest(crate_dir / "Cargo.toml", package_name=spec.name, version=version) + rewrite_cargo_manifest( + crate_dir / "Cargo.toml", + package_name=spec.name, + version=version, + extension_sources=extension_sources, + extension_aot_sources=extension_aot_sources, + ) return crate_dir @@ -317,7 +419,7 @@ def cargo_metadata_package(manifest: Path) -> dict[str, object]: return package -def cargo_package(crate_dir: Path, target_dir: Path) -> Path: +def cargo_package(crate_dir: Path, target_dir: Path, *, no_verify: bool = False) -> Path: manifest = crate_dir / "Cargo.toml" package = cargo_metadata_package(manifest) name = package["name"] @@ -331,6 +433,8 @@ def cargo_package(crate_dir: Path, target_dir: Path) -> Path: str(target_dir), "--allow-dirty", ] + if no_verify: + command.append("--no-verify") env = {**os.environ, "OLIPHAUNT_ARTIFACT_CRATE_REQUIRE_PAYLOAD": "1"} run(command, env=env) crate_path = target_dir / "package" / f"{name}-{version}.crate" @@ -339,6 +443,50 @@ def cargo_package(crate_dir: Path, target_dir: Path) -> Path: return crate_path +def packaged_manifest_text(text: str) -> str: + return re.sub(r', path = "\.\./[^"]+"', "", text) + + +def cargo_package_without_dependency_resolution(crate_dir: Path, target_dir: Path) -> Path: + manifest = crate_dir / "Cargo.toml" + package = cargo_metadata_package(manifest) + name = str(package["name"]) + version = str(package["version"]) + package_root = f"{name}-{version}" + stage_root = target_dir / "manual-package-stage" + stage_dir = stage_root / package_root + crate_path = target_dir / "package" / f"{package_root}.crate" + shutil.rmtree(stage_dir, ignore_errors=True) + crate_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copytree( + crate_dir, + stage_dir, + ignore=shutil.ignore_patterns("target", ".git"), + ) + staged_manifest = stage_dir / "Cargo.toml" + staged_manifest.write_text( + packaged_manifest_text(staged_manifest.read_text(encoding="utf-8")), + encoding="utf-8", + ) + cargo_metadata_package(staged_manifest) + if crate_path.exists(): + crate_path.unlink() + with tarfile.open(crate_path, "w:gz") as archive: + for path in sorted(item for item in stage_dir.rglob("*") if item.is_file()): + arcname = f"{package_root}/{path.relative_to(stage_dir).as_posix()}" + info = archive.gettarinfo(path, arcname) + info.uid = 0 + info.gid = 0 + info.uname = "" + info.gname = "" + info.mtime = 0 + with path.open("rb") as handle: + archive.addfile(info, handle) + if not crate_path.is_file(): + fail(f"manual package did not create {rel(crate_path)}") + return crate_path + + def validate_crate_size(crate_path: Path) -> None: size = crate_path.stat().st_size if size > CRATES_IO_MAX_BYTES: @@ -355,9 +503,14 @@ def package_spec( source_root: Path, output_dir: Path, cargo_target_dir: Path, + extension_sources: list[ExtensionCargoSource], + extension_aot_sources: list[ExtensionAotCargoSource], ) -> GeneratedPackage: - crate_dir = copy_package_source(spec, source_root, version) - crate_path = cargo_package(crate_dir, cargo_target_dir) + crate_dir = copy_package_source(spec, source_root, version, extension_sources, extension_aot_sources) + if spec.name == RUNTIME_PACKAGE and extension_sources: + crate_path = cargo_package_without_dependency_resolution(crate_dir, cargo_target_dir) + else: + crate_path = cargo_package(crate_dir, cargo_target_dir) validate_crate_size(crate_path) output = output_dir / crate_path.name shutil.copy2(crate_path, output) @@ -372,6 +525,293 @@ def package_spec( ) +def extension_feature_name(package_name: str) -> str: + if not package_name.startswith("oliphaunt-extension-"): + fail(f"invalid extension package name {package_name}") + return "extension-" + package_name.removeprefix("oliphaunt-extension-") + + +def discover_extension_manifests(roots: list[Path]) -> list[Path]: + manifests: list[Path] = [] + for root in roots: + if root.is_file() and root.name == "extension-artifacts.json": + manifests.append(root) + continue + if root.is_dir(): + manifests.extend(path for path in root.rglob("extension-artifacts.json") if path.is_file()) + return sorted(set(manifests)) + + +def extension_wasix_asset(extension_dir: Path, manifest: dict[str, object]) -> Path | None: + for asset in manifest.get("assets", []): + if not isinstance(asset, dict): + continue + if ( + asset.get("family") == "wasix" + and asset.get("kind") == "wasix-runtime" + and asset.get("target") == "wasix-portable" + and isinstance(asset.get("name"), str) + ): + path = extension_dir / "release-assets" / str(asset["name"]) + if path.is_file(): + return path + return None + + +def extension_aot_specs(extension_dir: Path, *, product: str, version: str, sql_name: str) -> tuple[ExtensionAotCargoSpec, ...]: + aot_root = extension_dir / "wasix-aot" + if not aot_root.is_dir(): + return () + specs: list[ExtensionAotCargoSpec] = [] + seen_targets: set[str] = set() + for manifest_path in sorted(aot_root.glob("*/manifest.json")): + data = json.loads(manifest_path.read_text(encoding="utf-8")) + target = data.get("target-triple") + artifacts = data.get("artifacts") + if not isinstance(target, str) or not target: + fail(f"{rel(manifest_path)} is missing target-triple") + if target in seen_targets: + fail(f"{rel(aot_root)} has duplicate extension AOT target {target}") + if not isinstance(artifacts, list) or not artifacts: + fail(f"{rel(manifest_path)} must contain extension AOT artifacts") + expected_prefix = f"extension:{sql_name}" + for artifact in artifacts: + if not isinstance(artifact, dict): + fail(f"{rel(manifest_path)} contains a non-object AOT artifact") + name = artifact.get("name") + path = artifact.get("path") + if not isinstance(name, str) or not ( + name == expected_prefix or name.startswith(f"{expected_prefix}:") + ): + fail(f"{rel(manifest_path)} contains AOT artifact {name!r} for {sql_name}") + if not isinstance(path, str) or not path: + fail(f"{rel(manifest_path)} artifact {name!r} is missing path") + checked = PurePosixPath(path) + if checked.is_absolute() or any(part in {"", ".", ".."} for part in checked.parts): + fail(f"{rel(manifest_path)} artifact {name!r} path must be simple relative path, got {path!r}") + if not (manifest_path.parent / path).is_file(): + fail(f"{rel(manifest_path)} references missing AOT artifact {path}") + seen_targets.add(target) + specs.append( + ExtensionAotCargoSpec( + name=f"{product}-aot-{target}", + version=version, + sql_name=sql_name, + target=target, + source_dir=manifest_path.parent, + ) + ) + return tuple(sorted(specs, key=lambda spec: spec.target)) + + +def extension_cargo_specs(extension_roots: list[Path]) -> list[ExtensionCargoSpec]: + specs: list[ExtensionCargoSpec] = [] + for manifest_path in discover_extension_manifests(extension_roots): + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + product = manifest.get("product") + version = manifest.get("version") + sql_name = manifest.get("sqlName") + if not all(isinstance(value, str) and value for value in [product, version, sql_name]): + fail(f"{rel(manifest_path)} is missing product, version, or sqlName") + archive = extension_wasix_asset(manifest_path.parent, manifest) + if archive is None: + continue + specs.append( + ExtensionCargoSpec( + name=str(product), + version=str(version), + sql_name=str(sql_name), + archive=archive, + sha256=sha256_file(archive), + size=archive.stat().st_size, + aot_targets=extension_aot_specs( + manifest_path.parent, + product=str(product), + version=str(version), + sql_name=str(sql_name), + ), + ) + ) + return sorted(specs, key=lambda spec: spec.name) + + +def write_extension_cargo_source(spec: ExtensionCargoSpec, source_root: Path) -> ExtensionCargoSource: + crate_dir = source_root / spec.name + if crate_dir.exists(): + fail(f"duplicate generated WASIX extension Cargo package source: {rel(crate_dir)}") + (crate_dir / "src").mkdir(parents=True, exist_ok=True) + (crate_dir / "payload").mkdir(parents=True, exist_ok=True) + shutil.copy2(spec.archive, crate_dir / "payload/extension.tar.zst") + crate_dir.joinpath("README.md").write_text( + "\n".join( + [ + f"# {spec.name}", + "", + f"Cargo artifact package for the `{spec.sql_name}` Oliphaunt WASIX extension.", + "", + ] + ), + encoding="utf-8", + ) + crate_dir.joinpath("Cargo.toml").write_text( + "\n".join( + [ + "[package]", + f'name = "{spec.name}"', + f'version = "{spec.version}"', + 'edition = "2024"', + 'rust-version = "1.93"', + f'description = "Oliphaunt WASIX artifact package for the {spec.sql_name} PostgreSQL extension"', + 'repository = "https://github.com/f0rr0/oliphaunt"', + 'homepage = "https://oliphaunt.dev"', + 'license = "MIT AND Apache-2.0 AND PostgreSQL"', + 'include = ["Cargo.toml", "README.md", "src/**", "payload/**"]', + "", + "[lib]", + 'path = "src/lib.rs"', + "", + "[workspace]", + "", + ] + ), + encoding="utf-8", + ) + crate_dir.joinpath("src/lib.rs").write_text( + "\n".join( + [ + "#![deny(unsafe_code)]", + "", + f'pub const SQL_NAME: &str = "{spec.sql_name}";', + f'pub const ARCHIVE_SHA256: &str = "{spec.sha256}";', + f"pub const ARCHIVE_SIZE: u64 = {spec.size};", + "", + "pub fn archive() -> Option<&'static [u8]> {", + ' Some(include_bytes!("../payload/extension.tar.zst"))', + "}", + "", + ] + ), + encoding="utf-8", + ) + return ExtensionCargoSource(spec=spec, source_dir=crate_dir) + + +def write_extension_aot_cargo_source( + spec: ExtensionAotCargoSpec, + source_root: Path, +) -> ExtensionAotCargoSource: + crate_dir = source_root / spec.name + if crate_dir.exists(): + fail(f"duplicate generated WASIX extension AOT Cargo package source: {rel(crate_dir)}") + (crate_dir / "src").mkdir(parents=True, exist_ok=True) + shutil.copytree(spec.source_dir, crate_dir / "artifacts") + manifest = json.loads((crate_dir / "artifacts/manifest.json").read_text(encoding="utf-8")) + artifact_cases = [] + for artifact in sorted(manifest.get("artifacts", []), key=lambda item: item.get("name", "")): + name = artifact["name"] + path = artifact["path"] + artifact_cases.append( + f' {json.dumps(name)} => Some(include_bytes!("../artifacts/{path}")),\n' + ) + crate_dir.joinpath("README.md").write_text( + "\n".join( + [ + f"# {spec.name}", + "", + f"Cargo artifact package for `{spec.sql_name}` Oliphaunt WASIX AOT artifacts on `{spec.target}`.", + "", + ] + ), + encoding="utf-8", + ) + crate_dir.joinpath("Cargo.toml").write_text( + "\n".join( + [ + "[package]", + f'name = "{spec.name}"', + f'version = "{spec.version}"', + 'edition = "2024"', + 'rust-version = "1.93"', + f'description = "Oliphaunt WASIX AOT artifact package for the {spec.sql_name} PostgreSQL extension on {spec.target}"', + 'repository = "https://github.com/f0rr0/oliphaunt"', + 'homepage = "https://oliphaunt.dev"', + 'license = "MIT AND Apache-2.0 AND PostgreSQL"', + 'include = ["Cargo.toml", "README.md", "src/**", "artifacts/**"]', + "", + "[lib]", + 'path = "src/lib.rs"', + "", + "[workspace]", + "", + ] + ), + encoding="utf-8", + ) + crate_dir.joinpath("src/lib.rs").write_text( + "".join( + [ + "#![deny(unsafe_code)]\n\n", + f'pub const SQL_NAME: &str = "{spec.sql_name}";\n', + f'pub const TARGET_TRIPLE: &str = "{spec.target}";\n', + 'pub const MANIFEST_JSON: &str = include_str!("../artifacts/manifest.json");\n\n', + "pub fn aot_manifest_json() -> Option<&'static str> {\n", + " Some(MANIFEST_JSON)\n", + "}\n\n", + "pub fn aot_artifact_bytes(name: &str) -> Option<&'static [u8]> {\n", + " match name {\n", + *artifact_cases, + " _ => None,\n", + " }\n", + "}\n", + ] + ), + encoding="utf-8", + ) + return ExtensionAotCargoSource(spec=spec, source_dir=crate_dir) + + +def package_extension_source( + source: ExtensionCargoSource, + *, + output_dir: Path, + cargo_target_dir: Path, +) -> GeneratedPackage: + crate_path = cargo_package(source.source_dir, cargo_target_dir) + validate_crate_size(crate_path) + output = output_dir / crate_path.name + shutil.copy2(crate_path, output) + return GeneratedPackage( + name=source.spec.name, + manifest_path=source.source_dir / "Cargo.toml", + crate_path=output, + target="wasix-portable", + kind="wasix-extension", + size=output.stat().st_size, + sha256=sha256_file(output), + ) + + +def package_extension_aot_source( + source: ExtensionAotCargoSource, + *, + output_dir: Path, + cargo_target_dir: Path, +) -> GeneratedPackage: + crate_path = cargo_package(source.source_dir, cargo_target_dir) + validate_crate_size(crate_path) + output = output_dir / crate_path.name + shutil.copy2(crate_path, output) + return GeneratedPackage( + name=source.spec.name, + manifest_path=source.source_dir / "Cargo.toml", + crate_path=output, + target=source.spec.target, + kind="wasix-extension-aot", + size=output.stat().st_size, + sha256=sha256_file(output), + ) + + def package_specs(asset_dir: Path, extract_root: Path, version: str) -> list[PackageSpec]: specs: list[PackageSpec] = [] runtime_archive = asset_dir / f"liboliphaunt-wasix-{version}-runtime-portable.tar.zst" @@ -466,6 +906,12 @@ def parse_args(argv: list[str]) -> argparse.Namespace: help="directory where generated .crate files are written", ) parser.add_argument("--version", default=product_metadata.read_current_version(PRODUCT)) + parser.add_argument( + "--extension-artifact-root", + action="append", + default=["target/extension-artifacts"], + help="directory containing staged exact-extension artifacts with WASIX archives", + ) return parser.parse_args(argv) @@ -477,6 +923,12 @@ def main(argv: list[str]) -> int: asset_dir = ROOT / asset_dir if not output_dir.is_absolute(): output_dir = ROOT / output_dir + extension_roots = [] + for value in args.extension_artifact_root: + path = Path(value) + if not path.is_absolute(): + path = ROOT / path + extension_roots.append(path) if not asset_dir.is_dir(): fail(f"WASIX release asset directory does not exist: {rel(asset_dir)}") @@ -491,16 +943,46 @@ def main(argv: list[str]) -> int: extract_root.mkdir(parents=True, exist_ok=True) output_dir.mkdir(parents=True, exist_ok=True) + extension_specs = extension_cargo_specs(extension_roots) + extension_sources = [ + write_extension_cargo_source(spec, source_root) + for spec in extension_specs + ] + extension_aot_sources = [ + write_extension_aot_cargo_source(aot_spec, source_root) + for spec in extension_specs + for aot_spec in spec.aot_targets + ] specs = package_specs(asset_dir, extract_root, args.version) packages = [ + *[ + package_extension_source( + source, + output_dir=output_dir, + cargo_target_dir=cargo_target_dir, + ) + for source in extension_sources + ], + *[ + package_extension_aot_source( + source, + output_dir=output_dir, + cargo_target_dir=cargo_target_dir, + ) + for source in extension_aot_sources + ], + *[ package_spec( spec, version=args.version, source_root=source_root, output_dir=output_dir, cargo_target_dir=cargo_target_dir, + extension_sources=extension_sources, + extension_aot_sources=extension_aot_sources, ) for spec in specs + ], ] write_packages_manifest(packages, output_dir) print("generated liboliphaunt-wasix Cargo artifact crates:") diff --git a/tools/release/release.py b/tools/release/release.py index 41a8a7de..b77aa382 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -2476,16 +2476,16 @@ def liboliphaunt_wasix_cargo_artifact_crates(version: str) -> list[tuple[str, Pa if data.get("schema") != package_liboliphaunt_wasix_cargo_artifacts.SCHEMA or not isinstance(packages_data, list): fail(f"{manifest_path.relative_to(ROOT)} has an invalid schema") - expected_crates = { + expected_base_crates = { package_liboliphaunt_wasix_cargo_artifacts.ICU_PACKAGE, package_liboliphaunt_wasix_cargo_artifacts.RUNTIME_PACKAGE, *package_liboliphaunt_wasix_cargo_artifacts.AOT_PACKAGES.values(), } configured_crates = set(check_cratesio_publication.product_crates("liboliphaunt-wasix")) - if configured_crates != expected_crates: + if configured_crates != expected_base_crates: fail( "liboliphaunt-wasix crates.io packages must match WASIX runtime/AOT artifact packages: " - f"expected={sorted(expected_crates)}, configured={sorted(configured_crates)}" + f"expected={sorted(expected_base_crates)}, configured={sorted(configured_crates)}" ) generated_crates: set[str] = set() expected_crate_paths: set[Path] = set() @@ -2502,9 +2502,14 @@ def liboliphaunt_wasix_cargo_artifact_crates(version: str) -> list[tuple[str, Pa fail(f"{manifest_path.relative_to(ROOT)} has an invalid package row: {item!r}") if role != "artifact": fail(f"{manifest_path.relative_to(ROOT)} must contain direct WASIX artifact packages, got role {role!r}") - if name not in expected_crates: + if name not in expected_base_crates and not ( + kind == "wasix-extension" and is_extension_product(name) + ) and not ( + kind == "wasix-extension-aot" + and any(name.startswith(f"{product}-aot-") for product in product_metadata.extension_product_ids()) + ): fail(f"unexpected liboliphaunt-wasix Cargo artifact crate {name}") - if kind not in {"wasix-runtime", "wasix-aot", "icu-data"}: + if kind not in {"wasix-runtime", "wasix-aot", "icu-data", "wasix-extension", "wasix-extension-aot"}: fail(f"{manifest_path.relative_to(ROOT)} has unsupported WASIX Cargo artifact kind {kind!r}") source_manifest = ROOT / raw_manifest if not source_manifest.is_file(): @@ -2517,10 +2522,11 @@ def liboliphaunt_wasix_cargo_artifact_crates(version: str) -> list[tuple[str, Pa generated_crates.add(name) expected_crate_paths.add(crate_path) packages.append((name, crate_path, source_manifest)) - if generated_crates != expected_crates: + missing_base_crates = expected_base_crates - generated_crates + if missing_base_crates: fail( - "generated liboliphaunt-wasix Cargo artifacts do not match configured crates: " - f"expected={sorted(expected_crates)}, generated={sorted(generated_crates)}" + "generated liboliphaunt-wasix Cargo artifacts are missing configured runtime crates: " + f"missing={sorted(missing_base_crates)}, generated={sorted(generated_crates)}" ) unexpected = sorted( path.name diff --git a/tools/xtask/src/asset_pipeline.rs b/tools/xtask/src/asset_pipeline.rs index f7797683..1203046d 100644 --- a/tools/xtask/src/asset_pipeline.rs +++ b/tools/xtask/src/asset_pipeline.rs @@ -1353,11 +1353,7 @@ pub(crate) fn generate_aot_artifacts(target: &str, source_lane: &str) -> Result< fs::create_dir_all(&source_dir).with_context(|| format!("create {}", source_dir.display()))?; let serializer = ensure_aot_serializer_binary()?; - for module in outputs - .modules - .iter() - .filter(|module| module.requires_aot && is_core_aot_module(&module.name)) - { + for module in outputs.modules.iter().filter(|module| module.requires_aot) { let output = source_dir.join(&module.aot_file); generate_one_aot_artifact(&serializer, &module.path, &output)?; } @@ -2198,6 +2194,98 @@ fn package_aot_artifacts( Ok(()) } +pub(crate) fn package_extension_aot_artifacts( + sources: &SourcesManifest, + target: &str, + source_lane: &str, +) -> Result<()> { + let outputs = BuildOutputs::discover_for_aot(source_lane)?; + let source_dir = generated_aot_source_dir_for_source_lane(target, &outputs.source_lane)?; + if !source_dir.exists() { + let source_lane_arg = if outputs.source_lane == DEFAULT_SOURCE_LANE { + String::new() + } else { + format!(" --source-lane {}", outputs.source_lane) + }; + bail!( + "AOT source directory {} is missing; run `cargo run -p xtask -- assets aot --target-triple {target}{source_lane_arg}` before packaging extension AOT artifacts", + source_dir.display() + ); + } + + let target_id = aot_target_id_for_triple(target)?; + let artifacts_root = Path::new("target/extensions/wasix/aot-artifacts").join(target_id); + if artifacts_root.exists() { + fs::remove_dir_all(&artifacts_root) + .with_context(|| format!("remove {}", artifacts_root.display()))?; + } + fs::create_dir_all(&artifacts_root) + .with_context(|| format!("create {}", artifacts_root.display()))?; + + let mut grouped: BTreeMap> = BTreeMap::new(); + for module in outputs + .modules + .iter() + .filter(|module| module.requires_aot && !is_core_aot_module(&module.name)) + { + let Some(sql_name) = extension_module_sql_name(&module.name) else { + bail!("extension AOT module has invalid name {}", module.name); + }; + let source = source_dir.join(&module.aot_file); + if !source.exists() { + bail!( + "missing extension AOT artifact {}; run AOT generation for target {target} before packaging", + source.display() + ); + } + let extension_dir = artifacts_root.join(sql_name); + fs::create_dir_all(&extension_dir) + .with_context(|| format!("create {}", extension_dir.display()))?; + let destination = extension_dir.join(&module.aot_file); + copy_file(&source, &destination)?; + let raw_artifact = decode_zstd_file(&destination) + .with_context(|| format!("decode extension AOT artifact {}", destination.display()))?; + grouped + .entry(sql_name.to_owned()) + .or_default() + .push(AotManifestArtifact { + name: module.name.clone(), + path: module.aot_file.clone(), + sha256: sha256_file(&destination)?, + raw_sha256: sha256_bytes(&raw_artifact), + raw_size: raw_artifact.len() as u64, + module_sha256: sha256_file(&module.path)?, + compressed: true, + }); + } + + ensure!( + !grouped.is_empty(), + "extension AOT packaging produced no artifacts for {target}" + ); + + for (sql_name, mut artifacts) in grouped { + artifacts.sort_by(|left, right| left.name.cmp(&right.name)); + let manifest = AotManifest { + format_version: 1, + source_lane: Some(outputs.source_lane.clone()), + source_fingerprint: outputs.source_fingerprint.clone(), + postgres_version: Some(outputs.postgres_version.clone()), + target_triple: target.to_owned(), + engine: "llvm-opta".to_owned(), + wasmer_version: sources.toolchain.wasmer.clone(), + wasmer_wasix_version: sources.toolchain.wasmer_wasix.clone(), + artifacts, + }; + let manifest_json = + serde_json::to_string_pretty(&manifest).context("serialize extension AOT manifest")?; + let manifest_path = artifacts_root.join(&sql_name).join("manifest.json"); + fs::write(&manifest_path, format!("{manifest_json}\n")) + .with_context(|| format!("write {}", manifest_path.display()))?; + } + Ok(()) +} + pub(crate) fn check_aot_package_manifest(target: &str, source_lane: &str) -> Result<()> { let outputs = BuildOutputs::discover_for_aot(source_lane)?; let artifacts_dir = find_aot_artifact_dir_for_source_lane(target, &outputs.source_lane)?; diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs index 6d2bcc66..5126669a 100644 --- a/tools/xtask/src/main.rs +++ b/tools/xtask/src/main.rs @@ -230,6 +230,12 @@ fn assets(args: Vec) -> Result<()> { let source_lane = value_after(&args, "--source-lane").unwrap_or(DEFAULT_SOURCE_LANE); package_aot_only(&manifest, target, source_lane) } + Some("package-extension-aot") => { + let manifest = check_sources_manifest(false)?; + let target = value_after(&args, "--target-triple").unwrap_or(host_target_triple()); + let source_lane = value_after(&args, "--source-lane").unwrap_or(DEFAULT_SOURCE_LANE); + package_extension_aot_artifacts(&manifest, target, source_lane) + } Some("check-aot") => { let target = value_after(&args, "--target-triple").unwrap_or(host_target_triple()); let source_lane = value_after(&args, "--source-lane").unwrap_or(DEFAULT_SOURCE_LANE); @@ -518,6 +524,7 @@ fn print_usage() { " cargo run -p xtask --features aot-serializer -- assets package [--target-triple ] [--skip-aot]" ); eprintln!(" cargo run -p xtask -- assets package-aot [--target-triple ]"); + eprintln!(" cargo run -p xtask -- assets package-extension-aot [--target-triple ]"); eprintln!(" cargo run -p xtask -- assets check-aot [--target-triple ]"); eprintln!(" cargo run -p xtask -- assets export-list [--write]"); eprintln!(" cargo run -p xtask -- assets smoke"); From c77b3fe3477ab45ef102d9c260cf7f5fd217d395 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Thu, 25 Jun 2026 12:43:16 +0000 Subject: [PATCH 004/137] fix(wasix): suffix extension artifact crates --- .../liboliphaunt/wasix/crates/assets/build.rs | 23 +++++++++++++++---- ...kage_liboliphaunt_wasix_cargo_artifacts.py | 20 +++++++++++++--- tools/release/release.py | 5 ++-- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/runtimes/liboliphaunt/wasix/crates/assets/build.rs b/src/runtimes/liboliphaunt/wasix/crates/assets/build.rs index 6cbb9f1d..ee00f788 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/assets/build.rs +++ b/src/runtimes/liboliphaunt/wasix/crates/assets/build.rs @@ -600,7 +600,8 @@ fn selected_extensions(manifest_dir: &Path, manifest_text: &str) -> Vec String { - format!("{}-aot-{}", package.product, target.target) + format!("{}-wasix-aot-{}", package.product, target.target) +} + +fn extension_wasix_package_name(package: ExtensionPackage) -> String { + format!("{}-wasix", package.product) } fn crate_ident(package_name: &str) -> String { @@ -711,7 +716,10 @@ fn extension_archive_body(selected_extensions: &[SelectedExtension]) -> String { let sql_name = extension.package.sql_name; let expression = match &extension.archive { ExtensionArchiveSource::Crate => { - format!("{}::archive()", extension.package.crate_ident) + format!( + "{}::archive()", + extension_wasix_crate_ident(extension.package) + ) } ExtensionArchiveSource::Local { path, .. } => { format!("Some(include_bytes!({}))", rust_string_literal(path)) @@ -730,7 +738,10 @@ fn expected_extension_archive_sha256_body(selected_extensions: &[SelectedExtensi let sql_name = extension.package.sql_name; let expression = match &extension.archive { ExtensionArchiveSource::Crate => { - format!("Some({}::ARCHIVE_SHA256)", extension.package.crate_ident) + format!( + "Some({}::ARCHIVE_SHA256)", + extension_wasix_crate_ident(extension.package) + ) } ExtensionArchiveSource::Local { sha256, .. } => { format!("Some({sha256:?})") @@ -790,6 +801,10 @@ fn extension_manifest_entry(extension: &SelectedExtension) -> Option String { + format!("{}_wasix", package.crate_ident) +} + fn emit_artifact_manifest(out_dir: &Path, asset_dir: &Path, files: &[&Path]) { let version = env::var("CARGO_PKG_VERSION").expect("CARGO_PKG_VERSION is set by Cargo"); let manifest_path = out_dir.join("oliphaunt-artifact.toml"); diff --git a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py index e2df3bef..3f791a80 100644 --- a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py +++ b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py @@ -70,6 +70,7 @@ class GeneratedPackage: @dataclass(frozen=True) class ExtensionCargoSpec: name: str + product: str version: str sql_name: str archive: Path @@ -346,7 +347,7 @@ def inject_runtime_extension_dependencies( dependency_lines.append( f'{package} = {{ version = "={source.spec.version}", path = "../{package}", optional = true }}' ) - feature = extension_feature_name(package) + feature = extension_feature_name(source.spec.product) feature_deps = [f"dep:{package}"] for aot_source in sorted(aot_by_extension.get(source.spec.sql_name, []), key=lambda item: item.spec.name): feature_deps.append(f"dep:{aot_source.spec.name}") @@ -531,6 +532,18 @@ def extension_feature_name(package_name: str) -> str: return "extension-" + package_name.removeprefix("oliphaunt-extension-") +def wasix_extension_package_name(product: str) -> str: + if not product.startswith("oliphaunt-extension-"): + fail(f"invalid extension product name {product}") + return f"{product}-wasix" + + +def wasix_extension_aot_package_name(product: str, target: str) -> str: + if not product.startswith("oliphaunt-extension-"): + fail(f"invalid extension product name {product}") + return f"{product}-wasix-aot-{target}" + + def discover_extension_manifests(roots: list[Path]) -> list[Path]: manifests: list[Path] = [] for root in roots: @@ -594,7 +607,7 @@ def extension_aot_specs(extension_dir: Path, *, product: str, version: str, sql_ seen_targets.add(target) specs.append( ExtensionAotCargoSpec( - name=f"{product}-aot-{target}", + name=wasix_extension_aot_package_name(product, target), version=version, sql_name=sql_name, target=target, @@ -618,7 +631,8 @@ def extension_cargo_specs(extension_roots: list[Path]) -> list[ExtensionCargoSpe continue specs.append( ExtensionCargoSpec( - name=str(product), + name=wasix_extension_package_name(str(product)), + product=str(product), version=str(version), sql_name=str(sql_name), archive=archive, diff --git a/tools/release/release.py b/tools/release/release.py index b77aa382..e6f6c2be 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -2503,10 +2503,11 @@ def liboliphaunt_wasix_cargo_artifact_crates(version: str) -> list[tuple[str, Pa if role != "artifact": fail(f"{manifest_path.relative_to(ROOT)} must contain direct WASIX artifact packages, got role {role!r}") if name not in expected_base_crates and not ( - kind == "wasix-extension" and is_extension_product(name) + kind == "wasix-extension" + and any(name == f"{product}-wasix" for product in product_metadata.extension_product_ids()) ) and not ( kind == "wasix-extension-aot" - and any(name.startswith(f"{product}-aot-") for product in product_metadata.extension_product_ids()) + and any(name.startswith(f"{product}-wasix-aot-") for product in product_metadata.extension_product_ids()) ): fail(f"unexpected liboliphaunt-wasix Cargo artifact crate {name}") if kind not in {"wasix-runtime", "wasix-aot", "icu-data", "wasix-extension", "wasix-extension-aot"}: From f6b10fcf60020e65393fb9a0f784d4080d1358ea Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Thu, 25 Jun 2026 13:42:49 +0000 Subject: [PATCH 005/137] fix(examples): resolve oliphaunt packages from local registries --- Cargo.lock | 2 +- examples/README.md | 29 +- examples/electron-wasix/.npmrc | 3 + examples/electron-wasix/README.md | 4 +- examples/electron-wasix/src-wasix/Cargo.lock | 75 ++- examples/electron-wasix/src-wasix/Cargo.toml | 2 +- examples/electron/.npmrc | 3 + examples/electron/README.md | 4 +- examples/electron/package.json | 6 +- examples/tauri-wasix/.npmrc | 3 + examples/tauri-wasix/README.md | 4 +- examples/tauri-wasix/src-tauri/Cargo.lock | 83 ++- examples/tauri-wasix/src-tauri/Cargo.toml | 2 +- examples/tauri/.npmrc | 3 + examples/tauri/README.md | 4 +- examples/tauri/src-tauri/Cargo.lock | 218 ++++++- examples/tauri/src-tauri/Cargo.toml | 11 +- examples/tools/check-examples.sh | 28 + examples/tools/with-local-registries.sh | 26 + pnpm-lock.yaml | 133 +++- .../crates/oliphaunt-wasix/Cargo.toml | 2 +- src/runtimes/liboliphaunt/icu/Cargo.toml | 2 +- tools/release/local_registry_publish.py | 596 +++++++++++++++++- .../release/package_broker_cargo_artifacts.py | 12 + 24 files changed, 1148 insertions(+), 107 deletions(-) create mode 100644 examples/electron-wasix/.npmrc create mode 100644 examples/electron/.npmrc create mode 100644 examples/tauri-wasix/.npmrc create mode 100644 examples/tauri/.npmrc create mode 100755 examples/tools/with-local-registries.sh diff --git a/Cargo.lock b/Cargo.lock index 1f21c700..5b8d1e69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2266,7 +2266,7 @@ dependencies = [ [[package]] name = "oliphaunt-icu" -version = "0.0.0" +version = "0.1.0" dependencies = [ "sha2 0.10.9", "tar", diff --git a/examples/README.md b/examples/README.md index fbe96bc8..fbe03eca 100644 --- a/examples/README.md +++ b/examples/README.md @@ -10,11 +10,36 @@ These examples keep the same todo schema across desktop shells: Each app opts into `hstore`, `pg_trgm`, and `unaccent`, then uses `hstore` tags plus trigram/accent-insensitive search for the todo list. -Local registry artifacts from CI run `28049923289` can be staged with: +Local registry artifacts for Linux x64 from CI run `28049923289` can be +staged with: ```sh python3 tools/release/local_registry_publish.py download --run-id 28049923289 --preset local-publish -python3 tools/release/local_registry_publish.py publish +python3 tools/release/package_liboliphaunt_cargo_artifacts.py \ + --asset-dir target/local-registry-artifacts/liboliphaunt-native-release-assets-linux-x64-gnu \ + --output-dir target/local-registry-generated/liboliphaunt-native-cargo \ + --target linux-x64-gnu +python3 tools/release/package_broker_cargo_artifacts.py \ + --asset-dir target/local-registry-artifacts/oliphaunt-broker-release-assets-linux-x64-gnu \ + --output-dir target/local-registry-generated/broker-cargo \ + --target linux-x64-gnu +python3 tools/release/package_liboliphaunt_wasix_cargo_artifacts.py \ + --asset-dir target/local-registry-artifacts/liboliphaunt-wasix-release-assets \ + --output-dir target/local-registry-generated/wasix-cargo \ + --extension-artifact-root target/local-registry-artifacts/oliphaunt-extension-package-artifacts +python3 tools/release/local_registry_publish.py publish \ + --artifact-root target/local-registry-generated/liboliphaunt-native-cargo \ + --artifact-root target/local-registry-generated/broker-cargo \ + --artifact-root target/local-registry-generated/wasix-cargo \ + --artifact-root target/local-registry-artifacts/oliphaunt-extension-package-artifacts +``` + +Run examples through the local registry helper so Cargo resolves +`registry = "oliphaunt-local"` and pnpm reads the local Verdaccio registry: + +```sh +examples/tools/with-local-registries.sh pnpm --dir examples/electron install +examples/tools/with-local-registries.sh pnpm --dir examples/electron start ``` On Linux, SwiftPM artifacts are staged for inspection and skipped for registry diff --git a/examples/electron-wasix/.npmrc b/examples/electron-wasix/.npmrc new file mode 100644 index 00000000..5cd8aaac --- /dev/null +++ b/examples/electron-wasix/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:4873/ +link-workspace-packages=false +prefer-workspace-packages=false diff --git a/examples/electron-wasix/README.md b/examples/electron-wasix/README.md index 46a65d53..361db07b 100644 --- a/examples/electron-wasix/README.md +++ b/examples/electron-wasix/README.md @@ -6,8 +6,8 @@ Electron exits. The Electron main process uses `pg` with a single connection and exposes the same preload API as the native Electron example. ```sh -pnpm --dir examples/electron-wasix install -pnpm --dir examples/electron-wasix start +examples/tools/with-local-registries.sh pnpm --dir examples/electron-wasix install +examples/tools/with-local-registries.sh pnpm --dir examples/electron-wasix start ``` For packaged apps, build the `src-wasix` binary and set diff --git a/examples/electron-wasix/src-wasix/Cargo.lock b/examples/electron-wasix/src-wasix/Cargo.lock index 85e9f3c4..62b6a011 100644 --- a/examples/electron-wasix/src-wasix/Cargo.lock +++ b/examples/electron-wasix/src-wasix/Cargo.lock @@ -224,12 +224,12 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" dependencies = [ "memchr", - "serde", + "serde_core", ] [[package]] @@ -325,9 +325,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures 0.3.0", @@ -1499,9 +1499,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.102" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", @@ -1857,9 +1857,29 @@ dependencies = [ "serde_json", ] +[[package]] +name = "oliphaunt-extension-hstore-wasix" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "1d0b20fd2a03b45880974241e3443d9e324de637fefa4f43859efce70089812b" + +[[package]] +name = "oliphaunt-extension-pg-trgm-wasix" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "6ea075c13c8283d2eb26526c63061b116ffc515899fa59478a8a6c570539a312" + +[[package]] +name = "oliphaunt-extension-unaccent-wasix" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "9ab06b4d61878a87b53afc7b047d09f5f2fd794528acb5e40d359e599b0fc956" + [[package]] name = "oliphaunt-wasix" version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "ce6b8585b7d1314c42b2cb9ae8ccad6e65c2c70c6d037607de2e0894dd115f48" dependencies = [ "anyhow", "async-trait", @@ -1892,6 +1912,8 @@ dependencies = [ [[package]] name = "oliphaunt-wasix-aot-aarch64-apple-darwin" version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "9576d617b17ff231bd9edac4e9a4aec7e20b9e09f5db1fe1791d730e2af2b0ac" dependencies = [ "serde_json", "sha2 0.10.9", @@ -1900,6 +1922,8 @@ dependencies = [ [[package]] name = "oliphaunt-wasix-aot-aarch64-unknown-linux-gnu" version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "43cdd574cd33c901cab077a772364ff82760c0e4d40747c4811fe8cf102ca5c3" dependencies = [ "serde_json", "sha2 0.10.9", @@ -1908,6 +1932,8 @@ dependencies = [ [[package]] name = "oliphaunt-wasix-aot-x86_64-pc-windows-msvc" version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "47dbaab95593814aaa187d44e49bc54c02a14a559d6d30f09c0785282ef7467d" dependencies = [ "serde_json", "sha2 0.10.9", @@ -1916,6 +1942,8 @@ dependencies = [ [[package]] name = "oliphaunt-wasix-aot-x86_64-unknown-linux-gnu" version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "0afe5cb3df0987556274309165ca158c644437421bd93fa2892023b6a4578da4" dependencies = [ "serde_json", "sha2 0.10.9", @@ -1924,7 +1952,12 @@ dependencies = [ [[package]] name = "oliphaunt-wasix-assets" version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "6aafe0b142fc074331ae191f07c3df3b0973b6d95dfcf6c88b66d4969fa0bce4" dependencies = [ + "oliphaunt-extension-hstore-wasix", + "oliphaunt-extension-pg-trgm-wasix", + "oliphaunt-extension-unaccent-wasix", "serde", "serde_json", "sha2 0.10.9", @@ -2722,9 +2755,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "symbolic-common" -version = "13.5.0" +version = "13.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1acef24ab2c9b307824e99ee81544a7fd5eac70b29898013580c2ab68e22104b" +checksum = "b2dd5edfa38a9ff82e3f394bed19a5f953e2b40d3acf51535a45bb3653c3aabd" dependencies = [ "debugid", "memmap2 0.9.11", @@ -2734,9 +2767,9 @@ dependencies = [ [[package]] name = "symbolic-demangle" -version = "13.5.0" +version = "13.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eafb9860981a3611afed2ffadf834dabc8e7921ae9e6fe941ffee8d8d206888f" +checksum = "7bfea8acd6e7a1a51cf030a4ea77472b37af8c33b428f18ac62ceaee3645310d" dependencies = [ "cpp_demangle", "msvc-demangler", @@ -3124,9 +3157,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.3" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ "js-sys", "wasm-bindgen", @@ -3325,9 +3358,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -3338,9 +3371,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3348,9 +3381,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", @@ -3361,9 +3394,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] diff --git a/examples/electron-wasix/src-wasix/Cargo.toml b/examples/electron-wasix/src-wasix/Cargo.toml index 806c9466..96558521 100644 --- a/examples/electron-wasix/src-wasix/Cargo.toml +++ b/examples/electron-wasix/src-wasix/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] anyhow = "1" -oliphaunt-wasix = { path = "../../../src/bindings/wasix-rust/crates/oliphaunt-wasix", features = [ +oliphaunt-wasix = { version = "=0.1.0", registry = "oliphaunt-local", features = [ "extension-hstore", "extension-pg-trgm", "extension-unaccent", diff --git a/examples/electron/.npmrc b/examples/electron/.npmrc new file mode 100644 index 00000000..5cd8aaac --- /dev/null +++ b/examples/electron/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:4873/ +link-workspace-packages=false +prefer-workspace-packages=false diff --git a/examples/electron/README.md b/examples/electron/README.md index def6e7ee..f8acfe37 100644 --- a/examples/electron/README.md +++ b/examples/electron/README.md @@ -5,6 +5,6 @@ small IPC surface to the renderer through preload. The app uses `nativeBroker` mode with a persistent root under Electron's user data directory. ```sh -pnpm --dir examples/electron install -pnpm --dir examples/electron start +examples/tools/with-local-registries.sh pnpm --dir examples/electron install +examples/tools/with-local-registries.sh pnpm --dir examples/electron start ``` diff --git a/examples/electron/package.json b/examples/electron/package.json index 631140c7..c0a18a08 100644 --- a/examples/electron/package.json +++ b/examples/electron/package.json @@ -4,13 +4,15 @@ "version": "0.1.0", "type": "module", "scripts": { - "prebuild": "pnpm --dir ../../src/sdks/js run build", "build": "tsc -p tsconfig.main.json && vite build", "start": "pnpm run build && electron dist/main/main-process.js", "dev:renderer": "vite" }, "dependencies": { - "@oliphaunt/ts": "workspace:*", + "@oliphaunt/extension-hstore": "0.1.0", + "@oliphaunt/extension-pg-trgm": "0.1.0", + "@oliphaunt/extension-unaccent": "0.1.0", + "@oliphaunt/ts": "0.1.0", "kysely": "^0.29.2" }, "devDependencies": { diff --git a/examples/tauri-wasix/.npmrc b/examples/tauri-wasix/.npmrc new file mode 100644 index 00000000..5cd8aaac --- /dev/null +++ b/examples/tauri-wasix/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:4873/ +link-workspace-packages=false +prefer-workspace-packages=false diff --git a/examples/tauri-wasix/README.md b/examples/tauri-wasix/README.md index 066a2d9b..f0bd0d3b 100644 --- a/examples/tauri-wasix/README.md +++ b/examples/tauri-wasix/README.md @@ -5,6 +5,6 @@ Tauri owns a Rust backend that starts `OliphauntServer` from PostgreSQL URL. The webview receives app-specific commands only. ```sh -pnpm --dir examples/tauri-wasix install -pnpm --dir examples/tauri-wasix tauri dev +examples/tools/with-local-registries.sh pnpm --dir examples/tauri-wasix install +examples/tools/with-local-registries.sh pnpm --dir examples/tauri-wasix tauri dev ``` diff --git a/examples/tauri-wasix/src-tauri/Cargo.lock b/examples/tauri-wasix/src-tauri/Cargo.lock index ce0beb90..0f0807c3 100644 --- a/examples/tauri-wasix/src-tauri/Cargo.lock +++ b/examples/tauri-wasix/src-tauri/Cargo.lock @@ -355,12 +355,12 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" dependencies = [ "memchr", - "serde", + "serde_core", ] [[package]] @@ -556,9 +556,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures 0.3.0", @@ -2616,9 +2616,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.102" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", @@ -3331,9 +3331,29 @@ dependencies = [ "tokio", ] +[[package]] +name = "oliphaunt-extension-hstore-wasix" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "1d0b20fd2a03b45880974241e3443d9e324de637fefa4f43859efce70089812b" + +[[package]] +name = "oliphaunt-extension-pg-trgm-wasix" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "6ea075c13c8283d2eb26526c63061b116ffc515899fa59478a8a6c570539a312" + +[[package]] +name = "oliphaunt-extension-unaccent-wasix" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "9ab06b4d61878a87b53afc7b047d09f5f2fd794528acb5e40d359e599b0fc956" + [[package]] name = "oliphaunt-wasix" version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "ce6b8585b7d1314c42b2cb9ae8ccad6e65c2c70c6d037607de2e0894dd115f48" dependencies = [ "anyhow", "async-trait", @@ -3366,6 +3386,8 @@ dependencies = [ [[package]] name = "oliphaunt-wasix-aot-aarch64-apple-darwin" version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "9576d617b17ff231bd9edac4e9a4aec7e20b9e09f5db1fe1791d730e2af2b0ac" dependencies = [ "serde_json", "sha2 0.10.9", @@ -3374,6 +3396,8 @@ dependencies = [ [[package]] name = "oliphaunt-wasix-aot-aarch64-unknown-linux-gnu" version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "43cdd574cd33c901cab077a772364ff82760c0e4d40747c4811fe8cf102ca5c3" dependencies = [ "serde_json", "sha2 0.10.9", @@ -3382,6 +3406,8 @@ dependencies = [ [[package]] name = "oliphaunt-wasix-aot-x86_64-pc-windows-msvc" version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "47dbaab95593814aaa187d44e49bc54c02a14a559d6d30f09c0785282ef7467d" dependencies = [ "serde_json", "sha2 0.10.9", @@ -3390,6 +3416,8 @@ dependencies = [ [[package]] name = "oliphaunt-wasix-aot-x86_64-unknown-linux-gnu" version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "0afe5cb3df0987556274309165ca158c644437421bd93fa2892023b6a4578da4" dependencies = [ "serde_json", "sha2 0.10.9", @@ -3398,7 +3426,12 @@ dependencies = [ [[package]] name = "oliphaunt-wasix-assets" version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "6aafe0b142fc074331ae191f07c3df3b0973b6d95dfcf6c88b66d4969fa0bce4" dependencies = [ + "oliphaunt-extension-hstore-wasix", + "oliphaunt-extension-pg-trgm-wasix", + "oliphaunt-extension-unaccent-wasix", "serde", "serde_json", "sha2 0.10.9", @@ -4887,9 +4920,9 @@ dependencies = [ [[package]] name = "symbolic-common" -version = "13.5.0" +version = "13.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1acef24ab2c9b307824e99ee81544a7fd5eac70b29898013580c2ab68e22104b" +checksum = "b2dd5edfa38a9ff82e3f394bed19a5f953e2b40d3acf51535a45bb3653c3aabd" dependencies = [ "debugid", "memmap2 0.9.11", @@ -4899,9 +4932,9 @@ dependencies = [ [[package]] name = "symbolic-demangle" -version = "13.5.0" +version = "13.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eafb9860981a3611afed2ffadf834dabc8e7921ae9e6fe941ffee8d8d206888f" +checksum = "7bfea8acd6e7a1a51cf030a4ea77472b37af8c33b428f18ac62ceaee3645310d" dependencies = [ "cpp_demangle", "msvc-demangler", @@ -5837,9 +5870,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.3" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -6081,9 +6114,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -6094,9 +6127,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.75" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ "js-sys", "wasm-bindgen", @@ -6104,9 +6137,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6114,9 +6147,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", @@ -6127,9 +6160,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] @@ -6489,9 +6522,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.102" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/examples/tauri-wasix/src-tauri/Cargo.toml b/examples/tauri-wasix/src-tauri/Cargo.toml index b92957cb..a0d3acd7 100644 --- a/examples/tauri-wasix/src-tauri/Cargo.toml +++ b/examples/tauri-wasix/src-tauri/Cargo.toml @@ -16,7 +16,7 @@ tauri-build = { version = "2", features = [] } [dependencies] anyhow = "1" -oliphaunt-wasix = { path = "../../../src/bindings/wasix-rust/crates/oliphaunt-wasix", features = [ +oliphaunt-wasix = { version = "=0.1.0", registry = "oliphaunt-local", features = [ "extension-hstore", "extension-pg-trgm", "extension-unaccent", diff --git a/examples/tauri/.npmrc b/examples/tauri/.npmrc new file mode 100644 index 00000000..5cd8aaac --- /dev/null +++ b/examples/tauri/.npmrc @@ -0,0 +1,3 @@ +registry=http://127.0.0.1:4873/ +link-workspace-packages=false +prefer-workspace-packages=false diff --git a/examples/tauri/README.md b/examples/tauri/README.md index cf9e10ea..0e529721 100644 --- a/examples/tauri/README.md +++ b/examples/tauri/README.md @@ -6,6 +6,6 @@ the persistent root lives under the app data directory, and the exact extension set is declared in `src-tauri/Cargo.toml`. ```sh -pnpm --dir examples/tauri install -pnpm --dir examples/tauri tauri dev +examples/tools/with-local-registries.sh pnpm --dir examples/tauri install +examples/tools/with-local-registries.sh pnpm --dir examples/tauri tauri dev ``` diff --git a/examples/tauri/src-tauri/Cargo.lock b/examples/tauri/src-tauri/Cargo.lock index 0ac8d072..94d782a3 100644 --- a/examples/tauri/src-tauri/Cargo.lock +++ b/examples/tauri/src-tauri/Cargo.lock @@ -1609,9 +1609,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.102" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", @@ -1710,6 +1710,148 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "liboliphaunt-native-linux-x64-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "a20540ee7e2178c23667bf8fef269a6bcadadf5906a899aa413a6c7880d48987" +dependencies = [ + "liboliphaunt-native-linux-x64-gnu-part-000", + "liboliphaunt-native-linux-x64-gnu-part-001", + "liboliphaunt-native-linux-x64-gnu-part-002", + "liboliphaunt-native-linux-x64-gnu-part-003", + "liboliphaunt-native-linux-x64-gnu-part-004", + "liboliphaunt-native-linux-x64-gnu-part-005", + "liboliphaunt-native-linux-x64-gnu-part-006", + "liboliphaunt-native-linux-x64-gnu-part-007", + "liboliphaunt-native-linux-x64-gnu-part-008", + "liboliphaunt-native-linux-x64-gnu-part-009", + "liboliphaunt-native-linux-x64-gnu-part-010", + "liboliphaunt-native-linux-x64-gnu-part-011", + "liboliphaunt-native-linux-x64-gnu-part-012", + "liboliphaunt-native-linux-x64-gnu-part-013", + "liboliphaunt-native-linux-x64-gnu-part-014", + "liboliphaunt-native-linux-x64-gnu-part-015", + "liboliphaunt-native-linux-x64-gnu-part-016", + "liboliphaunt-native-linux-x64-gnu-part-017", + "liboliphaunt-native-linux-x64-gnu-part-018", + "sha2", +] + +[[package]] +name = "liboliphaunt-native-linux-x64-gnu-part-000" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "ce8496e2a86e7f70827318ce04432103d707394ca181b0dcc72f8a4852546ba2" + +[[package]] +name = "liboliphaunt-native-linux-x64-gnu-part-001" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "062b06f96ae1eaf3deacf1f862937c60f5db2443a231d291cc778d225b69d9e8" + +[[package]] +name = "liboliphaunt-native-linux-x64-gnu-part-002" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "00a4200ba9455997a383d791554f4972765967b5f4695e6a5d10eb341d28a62e" + +[[package]] +name = "liboliphaunt-native-linux-x64-gnu-part-003" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "2b13d309c1c023db07edc2da1c8690b48e7950c680b8e5bdbd41749e6ac22e49" + +[[package]] +name = "liboliphaunt-native-linux-x64-gnu-part-004" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "c374711cef7606ea3901a8a27530dbb101dbcb640ff3650ee624462be87bed99" + +[[package]] +name = "liboliphaunt-native-linux-x64-gnu-part-005" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "aff19d3622276f94e1c6e5185cff6d50c1465e6dc7549a8a5b3d4c6d1f6e49b8" + +[[package]] +name = "liboliphaunt-native-linux-x64-gnu-part-006" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "b3868b1cc083adba8bc3e1f4a88b3e00dfb2a41b238faca0c33d19bb8b65085c" + +[[package]] +name = "liboliphaunt-native-linux-x64-gnu-part-007" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "359c929c00e676da0cd10c221736582a9d929a58a7d77ee8dacd348fc0d9fbb4" + +[[package]] +name = "liboliphaunt-native-linux-x64-gnu-part-008" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "1dc32627ee552289518c60d9dd4afa992b2828c2cfc112a3f2f4f6947142974a" + +[[package]] +name = "liboliphaunt-native-linux-x64-gnu-part-009" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "9f72606f36ebcf593d762a0bc3739d2d2ce35f8beb6ddbbd136666de5be9872a" + +[[package]] +name = "liboliphaunt-native-linux-x64-gnu-part-010" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "39d44995ec52d4c297d59c9d7ea3279448996dc499ef1fe9819824c8b625747f" + +[[package]] +name = "liboliphaunt-native-linux-x64-gnu-part-011" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "bdd00410be5ddb58573acdcfef27485f7c9bf2e629f0fb423ed99c24f5e3cef4" + +[[package]] +name = "liboliphaunt-native-linux-x64-gnu-part-012" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "9f00bc29f1ed6220a7b036ea2c94eedc6c4342af83de54936a50e515869ffbd4" + +[[package]] +name = "liboliphaunt-native-linux-x64-gnu-part-013" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "b28ba903db0cbc308c50bb86cbd896b273a39f78d508b8da05f3f23f90414831" + +[[package]] +name = "liboliphaunt-native-linux-x64-gnu-part-014" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "eb5787d47861b6daee4435c67004ca2d9e272230ada3c43a1988f359573199b4" + +[[package]] +name = "liboliphaunt-native-linux-x64-gnu-part-015" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "13f81bec6d95b4d032969687881419dcb49621b153478f7b80509207be10730c" + +[[package]] +name = "liboliphaunt-native-linux-x64-gnu-part-016" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "2a2ffe641f6f1651c908d9996430bcd1ae844bbf4d85773163c227ca53ee0705" + +[[package]] +name = "liboliphaunt-native-linux-x64-gnu-part-017" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "719bb0e2c435631a3b449818b3495689be6b9ab96cbcd402c2f7beb64581ebbf" + +[[package]] +name = "liboliphaunt-native-linux-x64-gnu-part-018" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "09b4016ecd42044254f8707343d6ad9b1fd68bad81861d00a43dde2cdf29cce4" + [[package]] name = "libredox" version = "0.1.17" @@ -2085,12 +2227,16 @@ dependencies = [ [[package]] name = "oliphaunt" version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "60d9438f9208c76d8c5da49e450d76fe8829d3739562c710ec14ed8bfef6a790" dependencies = [ "crossbeam-channel", "flate2", "fs2", "getrandom 0.3.4", "libloading 0.8.9", + "liboliphaunt-native-linux-x64-gnu", + "oliphaunt-broker-linux-x64-gnu", "serde", "sha2", "tar", @@ -2099,9 +2245,17 @@ dependencies = [ "zstd", ] +[[package]] +name = "oliphaunt-broker-linux-x64-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "e8789d11e7ee362e2dce2cdf0487cc5a06a3e58441761c02b8f0ba2e27c95765" + [[package]] name = "oliphaunt-build" version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "6c342a63fd9162f1594885093d164e275cfed43a5b8af49f831a40d498286d9c" dependencies = [ "serde", "sha2", @@ -2113,8 +2267,13 @@ name = "oliphaunt-example-tauri" version = "0.1.0" dependencies = [ "anyhow", + "liboliphaunt-native-linux-x64-gnu", "oliphaunt", + "oliphaunt-broker-linux-x64-gnu", "oliphaunt-build", + "oliphaunt-extension-hstore-linux-x64-gnu", + "oliphaunt-extension-pg-trgm-linux-x64-gnu", + "oliphaunt-extension-unaccent-linux-x64-gnu", "serde", "tauri", "tauri-build", @@ -2122,6 +2281,33 @@ dependencies = [ "tokio", ] +[[package]] +name = "oliphaunt-extension-hstore-linux-x64-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "16ddd8e9bb0a2ead98c126723aebb820f849f9e3c5d47dd97fc2c25c4feb5536" +dependencies = [ + "sha2", +] + +[[package]] +name = "oliphaunt-extension-pg-trgm-linux-x64-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "87cbc3eb3707976f30efd3e928bebc08a9db45e1bf553f33e8c89c8a97107742" +dependencies = [ + "sha2", +] + +[[package]] +name = "oliphaunt-extension-unaccent-linux-x64-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "ab031ebd7d25afc721114420cbb7d26dbec6f8b413590700ab2a7e13d2d07872" +dependencies = [ + "sha2", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -3730,9 +3916,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.23.3" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -3808,9 +3994,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -3821,9 +4007,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.75" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ "js-sys", "wasm-bindgen", @@ -3831,9 +4017,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3841,9 +4027,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", @@ -3854,9 +4040,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.125" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] @@ -3876,9 +4062,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.102" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/examples/tauri/src-tauri/Cargo.toml b/examples/tauri/src-tauri/Cargo.toml index 40bd5b96..af6c3a19 100644 --- a/examples/tauri/src-tauri/Cargo.toml +++ b/examples/tauri/src-tauri/Cargo.toml @@ -17,13 +17,20 @@ runtime-version = "0.1.0" extensions = ["hstore", "pg_trgm", "unaccent"] [build-dependencies] -oliphaunt-build = { path = "../../../src/sdks/rust/crates/oliphaunt-build" } +oliphaunt-build = { version = "=0.1.0", registry = "oliphaunt-local" } tauri-build = { version = "2", features = [] } [dependencies] anyhow = "1" -oliphaunt = { path = "../../../src/sdks/rust" } +oliphaunt = { version = "=0.1.0", registry = "oliphaunt-local" } serde = { version = "1", features = ["derive"] } tauri = { version = "2", features = [] } thiserror = "2" tokio = { version = "1", features = ["sync"] } + +[target.'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))'.dependencies] +liboliphaunt-native-linux-x64-gnu = { version = "=0.1.0", registry = "oliphaunt-local" } +oliphaunt-broker-linux-x64-gnu = { version = "=0.1.0", registry = "oliphaunt-local" } +oliphaunt-extension-hstore-linux-x64-gnu = { version = "=0.1.0", registry = "oliphaunt-local" } +oliphaunt-extension-pg-trgm-linux-x64-gnu = { version = "=0.1.0", registry = "oliphaunt-local" } +oliphaunt-extension-unaccent-linux-x64-gnu = { version = "=0.1.0", registry = "oliphaunt-local" } diff --git a/examples/tools/check-examples.sh b/examples/tools/check-examples.sh index 80e5d2f7..91467234 100755 --- a/examples/tools/check-examples.sh +++ b/examples/tools/check-examples.sh @@ -50,18 +50,46 @@ require_text() { fi } +reject_text() { + local path="$1" + local pattern="$2" + if grep -Eq "$pattern" "$path"; then + echo "forbidden example local dependency pattern in $path: $pattern" >&2 + exit 1 + fi +} + require_file "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/package.json" require_file "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml" require_text "src/bindings/wasix-rust/moon.yml" '^ example-check:$' require_text "src/bindings/wasix-rust/moon.yml" 'tags: \["examples", "quality", "ci-wasm-regression"\]' +require_file "examples/tools/with-local-registries.sh" for example in tauri tauri-wasix electron electron-wasix; do require_file "examples/$example/package.json" require_file "examples/$example/README.md" + require_file "examples/$example/.npmrc" + require_text "examples/$example/.npmrc" '^registry=http://127\.0\.0\.1:4873/$' + require_text "examples/$example/.npmrc" '^link-workspace-packages=false$' + require_text "examples/$example/.npmrc" '^prefer-workspace-packages=false$' done require_file "examples/tauri/src-tauri/Cargo.toml" require_file "examples/tauri-wasix/src-tauri/Cargo.toml" require_file "examples/electron-wasix/src-wasix/Cargo.toml" +require_text "examples/electron/package.json" '"@oliphaunt/ts": "0\.1\.0"' +require_text "examples/electron/package.json" '"@oliphaunt/extension-hstore": "0\.1\.0"' +require_text "examples/electron/package.json" '"@oliphaunt/extension-pg-trgm": "0\.1\.0"' +require_text "examples/electron/package.json" '"@oliphaunt/extension-unaccent": "0\.1\.0"' +require_text "examples/tauri/src-tauri/Cargo.toml" 'registry = "oliphaunt-local"' +require_text "examples/tauri/src-tauri/Cargo.toml" 'oliphaunt-extension-hstore-linux-x64-gnu' +require_text "examples/tauri/src-tauri/Cargo.toml" 'oliphaunt-extension-pg-trgm-linux-x64-gnu' +require_text "examples/tauri/src-tauri/Cargo.toml" 'oliphaunt-extension-unaccent-linux-x64-gnu' +require_text "examples/tauri-wasix/src-tauri/Cargo.toml" 'registry = "oliphaunt-local"' +require_text "examples/electron-wasix/src-wasix/Cargo.toml" 'registry = "oliphaunt-local"' +reject_text "examples/electron/package.json" '"@oliphaunt/ts": "workspace:\*"' +reject_text "examples/tauri/src-tauri/Cargo.toml" 'path = "../../../src/sdks/rust' +reject_text "examples/tauri-wasix/src-tauri/Cargo.toml" 'path = "../../../src/bindings/wasix-rust' +reject_text "examples/electron-wasix/src-wasix/Cargo.toml" 'path = "../../../src/bindings/wasix-rust' require_file "src/sdks/react-native/examples/expo/package.json" require_file "src/sdks/react-native/examples/expo/maestro/installed-smoke.yaml" diff --git a/examples/tools/with-local-registries.sh b/examples/tools/with-local-registries.sh new file mode 100755 index 00000000..0d195ef6 --- /dev/null +++ b/examples/tools/with-local-registries.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +root="$(git rev-parse --show-toplevel 2>/dev/null)" || { + echo "must run inside the Oliphaunt git checkout" >&2 + exit 1 +} + +cargo_index="$root/target/local-registries/cargo/index" +npmrc="$root/target/local-registries/verdaccio/npmrc" + +if [[ ! -d "$cargo_index" ]]; then + echo "missing local Cargo registry index: $cargo_index" >&2 + echo "stage it with tools/release/local_registry_publish.py before running examples" >&2 + exit 1 +fi + +export CARGO_REGISTRIES_OLIPHAUNT_LOCAL_INDEX="file://$cargo_index" +if [[ -f "$npmrc" ]]; then + export NPM_CONFIG_USERCONFIG="$npmrc" +fi +# Local Verdaccio publishes packages during the example setup; allow those +# freshly-published local packages without changing the workspace policy. +export PNPM_CONFIG_MINIMUM_RELEASE_AGE=0 + +exec "$@" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b2c3bc4f..dbea3ee7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,9 +28,18 @@ importers: examples/electron: dependencies: + '@oliphaunt/extension-hstore': + specifier: 0.1.0 + version: 0.1.0 + '@oliphaunt/extension-pg-trgm': + specifier: 0.1.0 + version: 0.1.0 + '@oliphaunt/extension-unaccent': + specifier: 0.1.0 + version: 0.1.0 '@oliphaunt/ts': - specifier: workspace:* - version: link:../../src/sdks/js + specifier: 0.1.0 + version: 0.1.0 kysely: specifier: ^0.29.2 version: 0.29.2 @@ -1772,6 +1781,73 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@oliphaunt/extension-hstore-linux-x64-gnu@0.1.0': + resolution: {integrity: sha512-SFLBAQOITw1cq7ipyAejj7Br5V879vV6eoRsku5eq48N8FMTT5gnFVhHkIcGZ5zGXW2hDF0Se6kl3IiiX96BsQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oliphaunt/extension-hstore-linux-x64-gnu-payload-0@0.1.0': + resolution: {integrity: sha512-L2n/7d3Xt5PgrmFbuZKYdTiG8BbexieiOQgAEhrzEwKqTY9xj1C1rodcARn/Y5uFnZya32oZ4v8UMnt1ePJSAQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oliphaunt/extension-hstore-linux-x64-gnu-payload-1@0.1.0': + resolution: {integrity: sha512-kc+6WXQFgIDLNCKRnazNdviwr9M48i8duMQ+urHK2Xg0nuj8xp3R5klVS5KlB0cw82Vem+0EBg9LnNxWRD08ow==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oliphaunt/extension-hstore@0.1.0': + resolution: {integrity: sha512-Rbj1wtX0XY6oXorBAWwWVx22Tm2mLEegC3JPs0fJ5XmZ7QDIa4eTDoqv3kr4wbX2li1YbcTUkl03aKevKRNdbg==} + + '@oliphaunt/extension-pg-trgm-linux-x64-gnu@0.1.0': + resolution: {integrity: sha512-J6ZiD0aWHBmuT64R2b1zeHz30753Jxojh/yRDKzqg2Iy+9ZIY6N2Fc12pWKw9tUhJfiNTjrq5yZSROYgXoMWcA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oliphaunt/extension-pg-trgm-linux-x64-gnu-payload-0@0.1.0': + resolution: {integrity: sha512-V4h14dpRbkIAS6MpoNzHaHKyVvmDO6HfAsrTLraZ5mPs1zA9xWgBpOKzlWixP5J5kcqRKP/87NTFf90Jyx2e7g==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oliphaunt/extension-pg-trgm-linux-x64-gnu-payload-1@0.1.0': + resolution: {integrity: sha512-fDAsqWZSKab3RaEeTpHOse2oAuNT6BXgDX24rtLOxVMJXu/32u2zjXOaHIXw4vNzf2uOzWhV8qevPyCVhAiIuQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oliphaunt/extension-pg-trgm@0.1.0': + resolution: {integrity: sha512-/OcL2Rxm0jEOyQf5LCx/3NDGq8XBbKZPPwy4lslulL5w8oYcrbiWkaq7/3ydLmbA5BdfGUQnpMP1P3Jz7JoFhQ==} + + '@oliphaunt/extension-unaccent-linux-x64-gnu-payload-0@0.1.0': + resolution: {integrity: sha512-4tD8F+LrjS0XkpBc9FTLDf70U9roHypvksalDvItNXwaZeELcs+LYPMXw7TNhD60hO6XepmBOO07tP8j6JaNuQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oliphaunt/extension-unaccent-linux-x64-gnu-payload-1@0.1.0': + resolution: {integrity: sha512-w390+r99Mo9Umxx3tx4a++glV8D+BW47Fl6Zz3yQ2Mbepc4/ZjZZKL943fHmcc7E38m8SUby2BW6SjmLy8vIeQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oliphaunt/extension-unaccent-linux-x64-gnu@0.1.0': + resolution: {integrity: sha512-HJ51Z2CzHxiynqC6GD6BDWXMp4VsC2Dq2CSN13pMYRPhV1q4eqsw7pUD30nXNn+hbyH43nJ+s9HWB3XOcTJgUw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oliphaunt/extension-unaccent@0.1.0': + resolution: {integrity: sha512-UtfqGnTj6HvTkXqTHFtad17mhwi7bxUFzc8bnYF+Ou5or5gwvsUpdwnFu1fKOh8iRBetWs2U3iIDhEYJ6bBVxw==} + + '@oliphaunt/ts@0.1.0': + resolution: {integrity: sha512-VrhXdLX7bmFWUt0TLUm1uj/Glc387UfsLWA1j0jjmbzoL+dv/IvumjDLnjbJLC9wbAG0p2+9YScPND62xrXqnQ==} + engines: {node: '>=22.13 <25'} + '@orama/orama@3.1.18': resolution: {integrity: sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA==} engines: {node: '>= 20.0.0'} @@ -3128,7 +3204,6 @@ packages: boolean@3.2.0: resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. bplist-creator@0.1.0: resolution: {integrity: sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==} @@ -4295,7 +4370,6 @@ packages: glob@11.1.0: resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} engines: {node: 20 || >=22} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@13.0.6: @@ -6495,7 +6569,6 @@ packages: uuid@7.0.3: resolution: {integrity: sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==} - deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true validate-npm-package-name@5.0.1: @@ -8201,6 +8274,56 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} + '@oliphaunt/extension-hstore-linux-x64-gnu@0.1.0': + optionalDependencies: + '@oliphaunt/extension-hstore-linux-x64-gnu-payload-0': 0.1.0 + '@oliphaunt/extension-hstore-linux-x64-gnu-payload-1': 0.1.0 + optional: true + + '@oliphaunt/extension-hstore-linux-x64-gnu-payload-0@0.1.0': + optional: true + + '@oliphaunt/extension-hstore-linux-x64-gnu-payload-1@0.1.0': + optional: true + + '@oliphaunt/extension-hstore@0.1.0': + optionalDependencies: + '@oliphaunt/extension-hstore-linux-x64-gnu': 0.1.0 + + '@oliphaunt/extension-pg-trgm-linux-x64-gnu@0.1.0': + optionalDependencies: + '@oliphaunt/extension-pg-trgm-linux-x64-gnu-payload-0': 0.1.0 + '@oliphaunt/extension-pg-trgm-linux-x64-gnu-payload-1': 0.1.0 + optional: true + + '@oliphaunt/extension-pg-trgm-linux-x64-gnu-payload-0@0.1.0': + optional: true + + '@oliphaunt/extension-pg-trgm-linux-x64-gnu-payload-1@0.1.0': + optional: true + + '@oliphaunt/extension-pg-trgm@0.1.0': + optionalDependencies: + '@oliphaunt/extension-pg-trgm-linux-x64-gnu': 0.1.0 + + '@oliphaunt/extension-unaccent-linux-x64-gnu-payload-0@0.1.0': + optional: true + + '@oliphaunt/extension-unaccent-linux-x64-gnu-payload-1@0.1.0': + optional: true + + '@oliphaunt/extension-unaccent-linux-x64-gnu@0.1.0': + optionalDependencies: + '@oliphaunt/extension-unaccent-linux-x64-gnu-payload-0': 0.1.0 + '@oliphaunt/extension-unaccent-linux-x64-gnu-payload-1': 0.1.0 + optional: true + + '@oliphaunt/extension-unaccent@0.1.0': + optionalDependencies: + '@oliphaunt/extension-unaccent-linux-x64-gnu': 0.1.0 + + '@oliphaunt/ts@0.1.0': {} + '@orama/orama@3.1.18': {} '@radix-ui/number@1.1.1': {} diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml b/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml index 68c33f8c..d1ec3f62 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml @@ -90,7 +90,7 @@ sha2 = "0.10" dunce = "1" filetime = "0.2" oliphaunt-wasix-assets = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/assets" } -oliphaunt-icu = { version = "=0.0.0", path = "../../../../runtimes/liboliphaunt/icu", optional = true } +oliphaunt-icu = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/icu", optional = true } tokio = { version = "1", features = ["io-util", "rt-multi-thread"] } wasmer = { version = "7.2.0-alpha.3", default-features = false, features = [ "sys", diff --git a/src/runtimes/liboliphaunt/icu/Cargo.toml b/src/runtimes/liboliphaunt/icu/Cargo.toml index d146766e..b96f8dc4 100644 --- a/src/runtimes/liboliphaunt/icu/Cargo.toml +++ b/src/runtimes/liboliphaunt/icu/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oliphaunt-icu" -version = "0.0.0" +version = "0.1.0" edition = "2024" rust-version = "1.93" description = "Optional ICU data files for Oliphaunt runtimes." diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index 1cba30ee..506e2e4f 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -15,10 +15,12 @@ from __future__ import annotations import argparse +import gzip import hashlib import json import os import platform as host_platform +import re import shutil import subprocess import sys @@ -39,6 +41,8 @@ DEFAULT_REGISTRY_ROOT = ROOT / "target" / "local-registries" DEFAULT_ARTIFACT_ROOT = ROOT / "target" / "local-registry-artifacts" NPM_PACKAGE_SIZE_LIMIT_BYTES = 10 * 1024 * 1024 +CRATES_IO_INDEX = "https://github.com/rust-lang/crates.io-index" +CARGO_PACKAGE_SIZE_LIMIT_BYTES = 10 * 1024 * 1024 LOCAL_PUBLISH_ARTIFACTS = [ "liboliphaunt-native-release-assets", @@ -241,6 +245,31 @@ def host_npm_target() -> str | None: return None +def host_cargo_release_target() -> str | None: + machine = host_platform.machine().lower() + if sys.platform == "linux" and machine in {"x86_64", "amd64"}: + return "linux-x64-gnu" + if sys.platform == "linux" and machine in {"aarch64", "arm64"}: + return "linux-arm64-gnu" + if sys.platform == "darwin" and machine == "arm64": + return "macos-arm64" + if sys.platform == "win32" and machine in {"amd64", "x86_64"}: + return "windows-x64-msvc" + return None + + +def cargo_target_triple(target: str) -> str | None: + if target == "linux-x64-gnu": + return "x86_64-unknown-linux-gnu" + if target == "linux-arm64-gnu": + return "aarch64-unknown-linux-gnu" + if target == "macos-arm64": + return "aarch64-apple-darwin" + if target == "windows-x64-msvc": + return "x86_64-pc-windows-msvc" + return None + + def npm_platform_constraints(target: str) -> dict[str, list[str]]: if target == "linux-x64-gnu": return {"os": ["linux"], "cpu": ["x64"], "libc": ["glibc"]} @@ -1020,20 +1049,31 @@ def publish_npm(roots: list[Path], registry_root: Path, dry_run: bool, strict: b result.published.append(f"dry-run npm publish {label}") continue if identity is not None and npm_package_exists(registry_url, npmrc, identity[0], identity[1]): - result.add_skip(f"already published {identity[0]}@{identity[1]}") - continue - command = [ + command = [ "npm", - "publish", - str(tarball), + "unpublish", + f"{identity[0]}@{identity[1]}", "--registry", registry_url, - "--provenance=false", - "--ignore-scripts", - "--access", - "public", + "--force", "--loglevel=error", ] + if npmrc is not None: + command.extend(["--userconfig", str(npmrc)]) + run(command) + result.staged.append(f"replaced {identity[0]}@{identity[1]}") + command = [ + "npm", + "publish", + str(tarball), + "--registry", + registry_url, + "--provenance=false", + "--ignore-scripts", + "--access", + "public", + "--loglevel=error", + ] if npmrc is not None: command.extend(["--userconfig", str(npmrc)]) run(command) @@ -1041,6 +1081,477 @@ def publish_npm(roots: list[Path], registry_root: Path, dry_run: bool, strict: b return result +def read_cargo_package_name_version(manifest: Path) -> tuple[str, str]: + data = tomllib.loads(manifest.read_text(encoding="utf-8")) + package = data.get("package") + if not isinstance(package, dict): + raise RuntimeError(f"{rel(manifest)} is missing [package]") + name = package.get("name") + version = package.get("version") + if not isinstance(name, str) or not isinstance(version, str) or not name or not version: + raise RuntimeError(f"{rel(manifest)} must declare package name and version") + return name, version + + +def packaged_cargo_manifest_text(text: str) -> str: + text = text.replace( + "repository.workspace = true", + 'repository = "https://github.com/f0rr0/oliphaunt"', + ).replace( + "homepage.workspace = true", + 'homepage = "https://oliphaunt.dev"', + ) + text = re.sub(r', path = "[^"]+"', "", text) + if "\n[workspace]" not in text: + text = text.rstrip() + "\n\n[workspace]\n" + return text + + +def cargo_package_name_from_crate(crate_path: Path) -> str | None: + try: + with tarfile.open(crate_path, "r:gz") as archive: + manifests = [ + member + for member in archive.getmembers() + if member.isfile() and member.name.count("/") == 1 and member.name.endswith("/Cargo.toml") + ] + if not manifests: + return None + extracted = archive.extractfile(manifests[0]) + if extracted is None: + return None + data = tomllib.loads(extracted.read().decode("utf-8")) + except (tarfile.TarError, tomllib.TOMLDecodeError, UnicodeDecodeError, OSError): + return None + package = data.get("package") + if not isinstance(package, dict): + return None + name = package.get("name") + return name if isinstance(name, str) and name else None + + +def cargo_package_names_from_roots(roots: list[Path]) -> set[str]: + names: set[str] = set() + for crate_path in discover_files(roots, (".crate",)): + name = cargo_package_name_from_crate(crate_path) + if name is not None: + names.add(name) + return names + + +def prune_missing_local_artifact_target_dependencies( + manifest: Path, + available_package_names: set[str], + result: SurfaceResult, +) -> None: + text = manifest.read_text(encoding="utf-8") + lines = text.splitlines() + output: list[str] = [] + removed: list[tuple[str, list[str]]] = [] + index = 0 + while index < len(lines): + line = lines[index] + if not re.match(r"^\[target\..*\.dependencies\]$", line): + output.append(line) + index += 1 + continue + + block = [line] + index += 1 + while index < len(lines) and not re.match(r"^\[[^\]]+\]$", lines[index]): + block.append(lines[index]) + index += 1 + + dependency_names = [] + for block_line in block[1:]: + match = re.match(r"^([A-Za-z0-9_-]+)\s*=", block_line) + if match: + dependency_names.append(match.group(1)) + missing = sorted(name for name in dependency_names if name not in available_package_names) + if missing: + removed.append((line, missing)) + while output and output[-1] == "": + output.pop() + continue + if output and output[-1] != "": + output.append("") + output.extend(block) + + if not removed: + return + manifest.write_text("\n".join(output).rstrip() + "\n", encoding="utf-8") + for header, missing in removed: + result.add_skip( + f"{rel(manifest)} pruned {header} because local registry inputs are missing {', '.join(missing)}" + ) + + +def cargo_metadata_package_from_manifest(manifest: Path) -> dict[str, Any]: + completed = run( + [ + "cargo", + "metadata", + "--manifest-path", + str(manifest), + "--format-version", + "1", + "--no-deps", + ], + check=False, + capture=True, + ) + if completed.returncode != 0: + raise RuntimeError( + f"cargo metadata failed for {rel(manifest)}: {completed.stderr.strip()}" + ) + packages = json.loads(completed.stdout).get("packages") + if not isinstance(packages, list) or len(packages) != 1: + raise RuntimeError(f"cargo metadata for {rel(manifest)} did not return exactly one package") + package = packages[0] + if not isinstance(package, dict): + raise RuntimeError(f"cargo metadata for {rel(manifest)} returned an invalid package") + return package + + +def manual_cargo_package_source(manifest: Path, output_dir: Path) -> Path: + name, version = read_cargo_package_name_version(manifest) + source_dir = manifest.parent + package_root = f"{name}-{version}" + stage_root = output_dir / "manual-package-stage" + stage_dir = stage_root / package_root + crate_path = output_dir / f"{package_root}.crate" + shutil.rmtree(stage_dir, ignore_errors=True) + stage_dir.parent.mkdir(parents=True, exist_ok=True) + output_dir.mkdir(parents=True, exist_ok=True) + shutil.copytree( + source_dir, + stage_dir, + ignore=shutil.ignore_patterns("target", ".git", ".DS_Store"), + ) + staged_manifest = stage_dir / "Cargo.toml" + staged_manifest.write_text( + packaged_cargo_manifest_text(staged_manifest.read_text(encoding="utf-8")), + encoding="utf-8", + ) + package = cargo_metadata_package_from_manifest(staged_manifest) + if package.get("name") != name or package.get("version") != version: + raise RuntimeError(f"{rel(staged_manifest)} produced unexpected cargo metadata") + if crate_path.exists(): + crate_path.unlink() + with crate_path.open("wb") as raw_output: + with gzip.GzipFile(fileobj=raw_output, mode="wb", mtime=0) as gzip_output: + with tarfile.open(fileobj=gzip_output, mode="w") as archive: + for path in sorted(item for item in stage_dir.rglob("*") if item.is_file()): + arcname = f"{package_root}/{path.relative_to(stage_dir).as_posix()}" + info = archive.gettarinfo(path, arcname) + info.uid = 0 + info.gid = 0 + info.uname = "" + info.gname = "" + info.mtime = 0 + with path.open("rb") as handle: + archive.addfile(info, handle) + size = crate_path.stat().st_size + if size > CARGO_PACKAGE_SIZE_LIMIT_BYTES: + raise RuntimeError(f"{rel(crate_path)} is {size} bytes, above the crates.io 10 MiB package limit") + return crate_path + + +def stage_cargo_source_crates( + roots: list[Path], + registry_root: Path, + dry_run: bool, + result: SurfaceResult, +) -> list[Path]: + output_dir = registry_root / "cargo-generated" / "source-crates" + if dry_run: + result.staged.append("dry-run generated local Cargo source crates") + return [] + shutil.rmtree(output_dir, ignore_errors=True) + output_dir.mkdir(parents=True, exist_ok=True) + + generated: list[Path] = [] + build_manifest = ROOT / "src/sdks/rust/crates/oliphaunt-build/Cargo.toml" + generated.append(manual_cargo_package_source(build_manifest, output_dir)) + + sys.path.insert(0, str(ROOT / "tools/release")) + import release # type: ignore + + oliphaunt_manifest = release.prepare_oliphaunt_release_source( + release.current_product_version("oliphaunt-rust") + ) + available_package_names = cargo_package_names_from_roots(roots) + native_source_root = ROOT / "target/liboliphaunt/cargo-package-sources" + if native_source_root.is_dir(): + for manifest in sorted(native_source_root.glob("liboliphaunt-native-*/Cargo.toml")): + name, _version = read_cargo_package_name_version(manifest) + if "-part-" not in name: + available_package_names.add(name) + prune_missing_local_artifact_target_dependencies( + oliphaunt_manifest, + available_package_names, + result, + ) + generated.append(manual_cargo_package_source(oliphaunt_manifest, output_dir)) + + wasix_manifest = ROOT / "src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml" + generated.append(manual_cargo_package_source(wasix_manifest, output_dir)) + + if native_source_root.is_dir(): + for manifest in sorted(native_source_root.glob("liboliphaunt-native-*/Cargo.toml")): + name, _version = read_cargo_package_name_version(manifest) + if "-part-" in name: + continue + generated.append(manual_cargo_package_source(manifest, output_dir)) + + result.staged.extend(rel(path) for path in generated) + return generated + + +def native_extension_cargo_package_name(product: str, target: str) -> str: + return f"{product}-{target}" + + +def native_extension_cargo_links_name(product: str, target: str) -> str: + stem = f"extension_{product.removeprefix('oliphaunt-extension-')}_{target}" + return "oliphaunt_artifact_" + stem.replace("-", "_") + + +def write_native_extension_cargo_crate( + crate_dir: Path, + *, + product: str, + version: str, + sql_name: str, + target: str, + triple: str, + asset: Path, +) -> None: + name = native_extension_cargo_package_name(product, target) + links = native_extension_cargo_links_name(product, target) + runtime_dir = crate_dir / "payload" + extract_extension_runtime(asset, runtime_dir) + if not any(runtime_dir.rglob("*")): + raise RuntimeError(f"{rel(asset)} did not contain extension runtime files") + (crate_dir / "src").mkdir(parents=True, exist_ok=True) + (crate_dir / "README.md").write_text( + "\n".join( + [ + f"# {name}", + "", + f"Cargo artifact crate for the `{sql_name}` Oliphaunt native extension on `{target}`.", + "", + ] + ), + encoding="utf-8", + ) + (crate_dir / "Cargo.toml").write_text( + "\n".join( + [ + "[package]", + f'name = "{name}"', + f'version = "{version}"', + 'edition = "2024"', + 'rust-version = "1.93"', + f'description = "Cargo artifact crate for the {sql_name} Oliphaunt native extension on {target}."', + 'readme = "README.md"', + 'repository = "https://github.com/f0rr0/oliphaunt"', + 'homepage = "https://oliphaunt.dev"', + 'license = "MIT AND Apache-2.0 AND PostgreSQL"', + f'links = "{links}"', + 'build = "build.rs"', + 'include = ["Cargo.toml", "README.md", "build.rs", "src/**", "payload/**"]', + "", + "[lib]", + 'path = "src/lib.rs"', + "", + "[build-dependencies]", + 'sha2 = "0.10"', + "", + "[workspace]", + "", + ] + ), + encoding="utf-8", + ) + (crate_dir / "src/lib.rs").write_text( + "\n".join( + [ + f'pub const PRODUCT: &str = "{product}";', + 'pub const KIND: &str = "extension";', + f'pub const SQL_NAME: &str = "{sql_name}";', + f'pub const RELEASE_TARGET: &str = "{target}";', + f'pub const CARGO_TARGET: &str = "{triple}";', + "", + ] + ), + encoding="utf-8", + ) + (crate_dir / "build.rs").write_text( + f"""use sha2::{{Digest, Sha256}}; +use std::env; +use std::fs; +use std::io::Read; +use std::path::{{Path, PathBuf}}; + +const SCHEMA: &str = "oliphaunt-artifact-manifest-v1"; +const PRODUCT: &str = {json.dumps(product)}; +const VERSION: &str = env!("CARGO_PKG_VERSION"); +const KIND: &str = "extension"; +const TARGET: &str = {json.dumps(triple)}; +const EXTENSION: &str = {json.dumps(sql_name)}; + +fn main() {{ + let manifest_dir = + PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set")); + let payload = manifest_dir.join("payload"); + println!("cargo::rerun-if-changed={{}}", payload.display()); + if !payload.is_dir() {{ + if env::var_os("OLIPHAUNT_ARTIFACT_CRATE_REQUIRE_PAYLOAD").is_some() {{ + panic!("missing packaged extension payload under {{}}", payload.display()); + }} + return; + }} + let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set")); + let manifest = out_dir.join("oliphaunt-artifact.toml"); + let mut text = format!( + "schema = {{SCHEMA:?}}\\nproduct = {{PRODUCT:?}}\\nversion = {{VERSION:?}}\\nkind = {{KIND:?}}\\ntarget = {{TARGET:?}}\\nextension = {{EXTENSION:?}}\\n" + ); + for file in payload_files(&payload) {{ + let relative = file.strip_prefix(&payload).expect("payload file stays under payload"); + let sha256 = sha256_file(&file); + text.push_str(&format!( + "\\n[[files]]\\nsource = {{:?}}\\nrelative = {{:?}}\\nsha256 = {{sha256:?}}\\nexecutable = false\\n", + file.display().to_string(), + relative.to_string_lossy().replace('\\\\', "/"), + )); + }} + fs::write(&manifest, text).expect("write Oliphaunt extension artifact manifest"); + println!("cargo::metadata=manifest={{}}", manifest.display()); +}} + +fn payload_files(root: &Path) -> Vec {{ + let mut files = Vec::new(); + collect_payload_files(root, &mut files); + files.sort(); + files +}} + +fn collect_payload_files(root: &Path, files: &mut Vec) {{ + for entry in fs::read_dir(root).expect("read payload directory") {{ + let path = entry.expect("read payload entry").path(); + if path.is_dir() {{ + collect_payload_files(&path, files); + }} else if path.is_file() {{ + files.push(path); + }} + }} +}} + +fn sha256_file(path: &Path) -> String {{ + let mut file = fs::File::open(path).expect("open payload file for hashing"); + let mut hasher = Sha256::new(); + let mut buffer = [0u8; 8192]; + loop {{ + let read = file.read(&mut buffer).expect("read payload file for hashing"); + if read == 0 {{ + break; + }} + hasher.update(&buffer[..read]); + }} + format!("{{:x}}", hasher.finalize()) +}} +""", + encoding="utf-8", + ) + + +def package_native_extension_cargo_crates( + roots: list[Path], + staging_root: Path, + target: str | None, + dry_run: bool, + strict: bool, + result: SurfaceResult, +) -> list[Path]: + if target is None: + result.add_skip("current host does not map to a supported native extension Cargo target") + return [] + triple = cargo_target_triple(target) + if triple is None: + result.add_skip(f"unsupported native extension Cargo target {target}") + return [] + manifests = discover_extension_manifests(roots) + if not manifests: + result.add_skip("no extension-artifacts.json manifests found for native extension Cargo crates") + return [] + if dry_run: + result.staged.append(f"dry-run native extension Cargo crates for {target}") + return [] + + source_root = staging_root / "native-extension-sources" + output_dir = staging_root / "native-extension-crates" + cargo_target_dir = staging_root / "native-extension-cargo-target" + shutil.rmtree(source_root, ignore_errors=True) + shutil.rmtree(output_dir, ignore_errors=True) + shutil.rmtree(cargo_target_dir, ignore_errors=True) + source_root.mkdir(parents=True, exist_ok=True) + output_dir.mkdir(parents=True, exist_ok=True) + + outputs: list[Path] = [] + for manifest_path in manifests: + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + product = manifest.get("product") + version = manifest.get("version") + sql_name = manifest.get("sqlName") + if not all(isinstance(value, str) and value for value in [product, version, sql_name]): + result.add_skip(f"{rel(manifest_path)} is missing product, version, or sqlName") + continue + release_manifest = extension_release_manifest(manifest_path.parent, str(product), str(version)) + asset = extension_runtime_asset(manifest_path.parent, release_manifest or manifest, target) + if asset is None: + result.add_skip(f"{product}@{version} has no {target} native runtime asset") + continue + name = native_extension_cargo_package_name(str(product), target) + crate_dir = source_root / name + write_native_extension_cargo_crate( + crate_dir, + product=str(product), + version=str(version), + sql_name=str(sql_name), + target=target, + triple=triple, + asset=asset, + ) + run( + [ + "cargo", + "package", + "--manifest-path", + str(crate_dir / "Cargo.toml"), + "--target-dir", + str(cargo_target_dir), + "--allow-dirty", + ], + env={**os.environ, "OLIPHAUNT_ARTIFACT_CRATE_REQUIRE_PAYLOAD": "1"}, + ) + crate_path = cargo_target_dir / "package" / f"{name}-{version}.crate" + if not crate_path.is_file(): + raise RuntimeError(f"cargo package did not create {rel(crate_path)}") + size = crate_path.stat().st_size + if size > CARGO_PACKAGE_SIZE_LIMIT_BYTES: + message = f"{rel(crate_path)} is {size} bytes, above the crates.io 10 MiB package limit" + result.add_skip(message) + if strict: + raise RuntimeError(message) + continue + output = output_dir / crate_path.name + shutil.copy2(crate_path, output) + outputs.append(output) + result.staged.extend(rel(path) for path in outputs) + return outputs + + def crate_index_path(name: str) -> Path: lower = name.lower() if len(lower) == 1: @@ -1078,8 +1589,10 @@ def cargo_metadata_for_crate(crate_path: Path) -> dict[str, Any]: return package -def cargo_index_dependency(dep: dict[str, Any]) -> dict[str, Any]: +def cargo_index_dependency(dep: dict[str, Any], local_package_names: set[str]) -> dict[str, Any]: registry = dep.get("registry") + if registry is None and dep["name"] not in local_package_names: + registry = CRATES_IO_INDEX return { "name": dep["name"], "req": dep.get("req", "*"), @@ -1093,13 +1606,15 @@ def cargo_index_dependency(dep: dict[str, Any]) -> dict[str, Any]: } -def cargo_index_entry(crate_path: Path) -> dict[str, Any]: - package = cargo_metadata_for_crate(crate_path) +def cargo_index_entry(crate_path: Path, package: dict[str, Any], local_package_names: set[str]) -> dict[str, Any]: checksum = hashlib.sha256(crate_path.read_bytes()).hexdigest() return { "name": package["name"], "vers": package["version"], - "deps": [cargo_index_dependency(dep) for dep in package.get("dependencies", [])], + "deps": [ + cargo_index_dependency(dep, local_package_names) + for dep in package.get("dependencies", []) + ], "features": package.get("features", {}), "features2": None, "cksum": checksum, @@ -1110,8 +1625,41 @@ def cargo_index_entry(crate_path: Path) -> dict[str, Any]: } +def cargo_crate_priority(path: Path, registry_root: Path) -> tuple[int, str]: + resolved = path.resolve() + priority = 20 + for root, value in [ + (registry_root / "cargo-generated", 100), + (ROOT / "target/oliphaunt-wasix/cargo-artifacts-check", 90), + (ROOT / "target/local-registry-generated", 80), + (ROOT / "target/oliphaunt-wasix/cargo-artifacts", 70), + (ROOT / "target/package/tmp-registry", 40), + (ROOT / "target/package/tmp-crate", 30), + ]: + try: + resolved.relative_to(root.resolve()) + except ValueError: + continue + priority = value + break + return priority, str(path) + + def publish_cargo(roots: list[Path], registry_root: Path, dry_run: bool, strict: bool) -> SurfaceResult: result = SurfaceResult("cargo") + generated_roots = stage_cargo_source_crates(roots, registry_root, dry_run, result) + generated_roots.extend( + package_native_extension_cargo_crates( + roots, + registry_root / "cargo-generated", + host_cargo_release_target(), + dry_run, + strict, + result, + ) + ) + if generated_roots: + roots = [*roots, *generated_roots] crates = discover_files(roots, (".crate",)) if not crates: result.add_skip("no .crate artifacts found") @@ -1136,21 +1684,27 @@ def publish_cargo(roots: list[Path], registry_root: Path, dry_run: bool, strict: encoding="utf-8", ) - entries_by_path: dict[Path, list[dict[str, Any]]] = {} - copied: set[str] = set() - for crate_path in crates: + packages_by_target_name: dict[str, tuple[Path, dict[str, Any]]] = {} + for crate_path in sorted(crates, key=lambda path: cargo_crate_priority(path, registry_root)): try: - entry = cargo_index_entry(crate_path) + package = cargo_metadata_for_crate(crate_path) except RuntimeError as error: result.add_skip(str(error)) if strict: raise continue - target_name = f"{entry['name']}-{entry['vers']}.crate" - if target_name in copied: - continue + target_name = f"{package['name']}-{package['version']}.crate" + packages_by_target_name[target_name] = (crate_path, package) + + local_package_names = { + str(package["name"]) + for _crate_path, package in packages_by_target_name.values() + if isinstance(package.get("name"), str) + } + entries_by_path: dict[Path, list[dict[str, Any]]] = {} + for target_name, (crate_path, package) in sorted(packages_by_target_name.items()): + entry = cargo_index_entry(crate_path, package, local_package_names) shutil.copy2(crate_path, crates_dir / target_name) - copied.add(target_name) entries_by_path.setdefault(crate_index_path(entry["name"]), []).append(entry) result.published.append(target_name) diff --git a/tools/release/package_broker_cargo_artifacts.py b/tools/release/package_broker_cargo_artifacts.py index 5f608028..74f64a8d 100755 --- a/tools/release/package_broker_cargo_artifacts.py +++ b/tools/release/package_broker_cargo_artifacts.py @@ -199,6 +199,12 @@ def parse_args(argv: list[str]) -> argparse.Namespace: default="target/oliphaunt-broker/cargo-artifacts", help="directory where generated .crate files are written", ) + parser.add_argument( + "--target", + action="append", + default=[], + help="release target id to package, such as linux-x64-gnu; may be passed more than once", + ) parser.add_argument("--version", default=product_metadata.read_current_version(PRODUCT)) return parser.parse_args(argv) @@ -228,6 +234,12 @@ def main(argv: list[str]) -> int: surface=SURFACE, published_only=True, ) + if args.target: + selected_targets = set(args.target) + unknown = selected_targets - {target.target for target in targets} + if unknown: + fail("unsupported broker target(s): " + ", ".join(sorted(unknown))) + targets = [target for target in targets if target.target in selected_targets] for target in targets: outputs.append( package_target( From 033e094668f916be573fffc0303a207a7e1beaa5 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Thu, 25 Jun 2026 14:00:17 +0000 Subject: [PATCH 006/137] fix(release): split oversized native extension crates --- examples/tauri/src-tauri/Cargo.lock | 6 +- tools/release/local_registry_publish.py | 512 +++++++++++++++++++++++- 2 files changed, 494 insertions(+), 24 deletions(-) diff --git a/examples/tauri/src-tauri/Cargo.lock b/examples/tauri/src-tauri/Cargo.lock index 94d782a3..826a857d 100644 --- a/examples/tauri/src-tauri/Cargo.lock +++ b/examples/tauri/src-tauri/Cargo.lock @@ -2285,7 +2285,7 @@ dependencies = [ name = "oliphaunt-extension-hstore-linux-x64-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "16ddd8e9bb0a2ead98c126723aebb820f849f9e3c5d47dd97fc2c25c4feb5536" +checksum = "6a4ff122d6b692bcc1a0b7e3c20e88c4255f76deb9507c0c6300f67870839efd" dependencies = [ "sha2", ] @@ -2294,7 +2294,7 @@ dependencies = [ name = "oliphaunt-extension-pg-trgm-linux-x64-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "87cbc3eb3707976f30efd3e928bebc08a9db45e1bf553f33e8c89c8a97107742" +checksum = "1877c71f7a75afadc5cd5a34bc3b246a1b1603c24f06aa9a1c762145a6672596" dependencies = [ "sha2", ] @@ -2303,7 +2303,7 @@ dependencies = [ name = "oliphaunt-extension-unaccent-linux-x64-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "ab031ebd7d25afc721114420cbb7d26dbec6f8b413590700ab2a7e13d2d07872" +checksum = "9eabb41963dd6935ae1418179f0667b89a604eb30a636b781583157527f21901" dependencies = [ "sha2", ] diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index 506e2e4f..33e31d8a 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -43,6 +43,8 @@ NPM_PACKAGE_SIZE_LIMIT_BYTES = 10 * 1024 * 1024 CRATES_IO_INDEX = "https://github.com/rust-lang/crates.io-index" CARGO_PACKAGE_SIZE_LIMIT_BYTES = 10 * 1024 * 1024 +CARGO_EXTENSION_PART_BYTES = 7 * 1024 * 1024 +CARGO_EXTENSION_SPLIT_THRESHOLD_BYTES = 9 * 1024 * 1024 LOCAL_PUBLISH_ARTIFACTS = [ "liboliphaunt-native-release-assets", @@ -1317,6 +1319,442 @@ def native_extension_cargo_links_name(product: str, target: str) -> str: return "oliphaunt_artifact_" + stem.replace("-", "_") +def native_extension_cargo_part_package_name(product: str, target: str, index: int) -> str: + return f"{native_extension_cargo_package_name(product, target)}-part-{index:03d}" + + +def rust_crate_ident(crate_name: str) -> str: + return crate_name.replace("-", "_") + + +def toml_string(value: str) -> str: + return json.dumps(value) + + +def payload_files(source_root: Path) -> list[Path]: + return sorted(path for path in source_root.rglob("*") if path.is_file()) + + +def write_chunk(path: Path, data: bytes) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(data) + + +def copy_payload_file(source: Path, destination: Path) -> None: + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, destination) + + +def write_native_extension_cargo_part_crate( + crate_dir: Path, + *, + product: str, + version: str, + sql_name: str, + target: str, + index: int, +) -> None: + name = native_extension_cargo_part_package_name(product, target, index) + (crate_dir / "src").mkdir(parents=True, exist_ok=True) + (crate_dir / "Cargo.toml").write_text( + "\n".join( + [ + "[package]", + f'name = "{name}"', + f'version = "{version}"', + 'edition = "2024"', + 'rust-version = "1.93"', + f'description = "Cargo payload part {index:03d} for the {sql_name} Oliphaunt native extension on {target}."', + 'readme = "README.md"', + 'repository = "https://github.com/f0rr0/oliphaunt"', + 'homepage = "https://oliphaunt.dev"', + 'license = "MIT AND Apache-2.0 AND PostgreSQL"', + 'include = ["Cargo.toml", "README.md", "src/**", "payload/**"]', + "", + "[lib]", + 'path = "src/lib.rs"', + "", + "[workspace]", + "", + ] + ), + encoding="utf-8", + ) + (crate_dir / "README.md").write_text( + "\n".join( + [ + f"# {name}", + "", + f"Cargo payload part for the `{sql_name}` Oliphaunt native extension on `{target}`.", + "Applications do not depend on this crate directly.", + "", + ] + ), + encoding="utf-8", + ) + (crate_dir / "src" / "lib.rs").write_text( + "\n".join( + [ + f'pub const PRODUCT: &str = "{product}";', + 'pub const KIND: &str = "extension-part";', + f'pub const SQL_NAME: &str = "{sql_name}";', + f'pub const RELEASE_TARGET: &str = "{target}";', + f"pub const PART_INDEX: usize = {index};", + 'pub const PAYLOAD_ROOT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/payload");', + "", + ] + ), + encoding="utf-8", + ) + + +def build_native_extension_part_crates( + runtime_dir: Path, + source_root: Path, + *, + product: str, + version: str, + sql_name: str, + target: str, + part_bytes: int = CARGO_EXTENSION_PART_BYTES, +) -> list[Path]: + part_dirs: list[Path] = [] + current_dir: Path | None = None + current_size = 0 + + def start_part() -> Path: + index = len(part_dirs) + part_dir = source_root / native_extension_cargo_part_package_name(product, target, index) + write_native_extension_cargo_part_crate( + part_dir, + product=product, + version=version, + sql_name=sql_name, + target=target, + index=index, + ) + part_dirs.append(part_dir) + return part_dir + + for source in payload_files(runtime_dir): + relative = source.relative_to(runtime_dir).as_posix() + size = source.stat().st_size + if size > part_bytes: + current_dir = None + current_size = 0 + with source.open("rb") as handle: + chunk_index = 0 + while True: + data = handle.read(part_bytes) + if not data: + break + part_dir = start_part() + write_chunk( + part_dir / "payload" / "chunks" / f"{relative}.part{chunk_index:03d}", + data, + ) + chunk_index += 1 + continue + if current_dir is None or current_size + size > part_bytes: + current_dir = start_part() + current_size = 0 + copy_payload_file(source, current_dir / "payload" / "files" / relative) + current_size += size + + if not part_dirs: + raise RuntimeError(f"{product}@{version} generated no native extension Cargo part crates") + return part_dirs + + +NATIVE_EXTENSION_AGGREGATOR_BUILD_RS = r'''use sha2::{Digest, Sha256}; +use std::collections::BTreeMap; +use std::env; +use std::fs; +use std::io::{self, Read}; +use std::path::{Path, PathBuf}; + +const SCHEMA: &str = __SCHEMA__; +const PRODUCT: &str = __PRODUCT__; +const VERSION: &str = env!("CARGO_PKG_VERSION"); +const KIND: &str = "extension"; +const TARGET: &str = __TARGET__; +const EXTENSION: &str = __EXTENSION__; +const PART_ROOTS: &[&str] = &[ +__PART_ROOTS__ +]; + +fn main() { + emit_manifest(); +} + +fn emit_manifest() { + let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set")); + let payload = out_dir.join("payload"); + if payload.exists() { + fs::remove_dir_all(&payload).expect("remove stale Oliphaunt extension payload"); + } + fs::create_dir_all(&payload).expect("create Oliphaunt extension payload directory"); + + let part_roots = part_roots(); + if part_roots.is_empty() { + if env::var_os("OLIPHAUNT_ARTIFACT_CRATE_REQUIRE_PAYLOAD").is_some() { + panic!("missing Oliphaunt extension payload part crates"); + } + return; + } + + let mut chunk_files: BTreeMap> = BTreeMap::new(); + for root in part_roots { + println!("cargo::rerun-if-changed={}", root.display()); + copy_complete_files(&root.join("files"), &payload).expect("copy complete extension payload files"); + collect_chunks(&root.join("chunks"), &root.join("chunks"), &mut chunk_files) + .expect("collect extension payload chunks"); + } + + for (relative, mut chunks) in chunk_files { + chunks.sort_by_key(|(index, _)| *index); + for (expected, (actual, _)) in chunks.iter().enumerate() { + if *actual != expected { + panic!("non-contiguous Oliphaunt extension chunk indexes for {relative}"); + } + } + let output = payload.join(&relative); + if let Some(parent) = output.parent() { + fs::create_dir_all(parent).expect("create reconstructed extension file parent"); + } + let mut writer = fs::File::create(&output).expect("create reconstructed extension payload file"); + for (_, path) in chunks { + let mut reader = fs::File::open(&path).expect("open extension payload chunk"); + io::copy(&mut reader, &mut writer).expect("append extension payload chunk"); + } + } + + let files = collect_files(&payload).expect("collect reconstructed extension payload files"); + if files.is_empty() { + panic!("Oliphaunt extension payload part crates produced no files"); + } + let manifest = out_dir.join("oliphaunt-artifact.toml"); + let mut text = format!( + "schema = {SCHEMA:?}\nproduct = {PRODUCT:?}\nversion = {VERSION:?}\nkind = {KIND:?}\ntarget = {TARGET:?}\nextension = {EXTENSION:?}\n" + ); + for file in files { + let relative = file.strip_prefix(&payload) + .expect("payload file stays under payload root") + .to_string_lossy() + .replace('\\', "/"); + let sha256 = sha256_file(&file).expect("hash extension payload file"); + text.push_str(&format!( + "\n[[files]]\nsource = {:?}\nrelative = {:?}\nsha256 = {:?}\nexecutable = false\n", + file.display().to_string(), + relative, + sha256, + )); + } + fs::write(&manifest, text).expect("write Oliphaunt extension artifact manifest"); + println!("cargo::metadata=manifest={}", manifest.display()); +} + +fn part_roots() -> Vec { + PART_ROOTS.iter().map(PathBuf::from).collect() +} + +fn copy_complete_files(source: &Path, destination: &Path) -> io::Result<()> { + if !source.is_dir() { + return Ok(()); + } + for entry in fs::read_dir(source)? { + let entry = entry?; + let path = entry.path(); + let output = destination.join(path.strip_prefix(source).unwrap_or(&path)); + copy_tree_entry(&path, &output)?; + } + Ok(()) +} + +fn copy_tree_entry(source: &Path, destination: &Path) -> io::Result<()> { + let metadata = fs::metadata(source)?; + if metadata.is_dir() { + fs::create_dir_all(destination)?; + for entry in fs::read_dir(source)? { + let entry = entry?; + copy_tree_entry(&entry.path(), &destination.join(entry.file_name()))?; + } + } else if metadata.is_file() { + if let Some(parent) = destination.parent() { + fs::create_dir_all(parent)?; + } + fs::copy(source, destination)?; + } + Ok(()) +} + +fn collect_chunks( + root: &Path, + current: &Path, + chunks: &mut BTreeMap>, +) -> io::Result<()> { + if !current.is_dir() { + return Ok(()); + } + for entry in fs::read_dir(current)? { + let entry = entry?; + let path = entry.path(); + let metadata = fs::metadata(&path)?; + if metadata.is_dir() { + collect_chunks(root, &path, chunks)?; + continue; + } + if !metadata.is_file() { + continue; + } + let relative = path.strip_prefix(root).unwrap_or(&path).to_string_lossy().replace('\\', "/"); + let (file_relative, part_index) = split_part_relative(&relative) + .unwrap_or_else(|| panic!("invalid Oliphaunt extension chunk file name {relative}")); + chunks.entry(file_relative).or_default().push((part_index, path)); + } + Ok(()) +} + +fn split_part_relative(relative: &str) -> Option<(String, usize)> { + let (file, index) = relative.rsplit_once(".part")?; + if file.is_empty() || index.len() != 3 || !index.bytes().all(|byte| byte.is_ascii_digit()) { + return None; + } + Some((file.to_owned(), index.parse().ok()?)) +} + +fn collect_files(root: &Path) -> io::Result> { + let mut files = Vec::new(); + collect_files_inner(root, &mut files)?; + files.sort(); + Ok(files) +} + +fn collect_files_inner(path: &Path, files: &mut Vec) -> io::Result<()> { + if !path.is_dir() { + return Ok(()); + } + for entry in fs::read_dir(path)? { + let entry = entry?; + let entry_path = entry.path(); + let metadata = fs::metadata(&entry_path)?; + if metadata.is_dir() { + collect_files_inner(&entry_path, files)?; + } else if metadata.is_file() { + files.push(entry_path); + } + } + Ok(()) +} + +fn sha256_file(path: &Path) -> io::Result { + let mut file = fs::File::open(path)?; + let mut digest = Sha256::new(); + let mut buffer = [0_u8; 1024 * 64]; + loop { + let read = file.read(&mut buffer)?; + if read == 0 { + break; + } + digest.update(&buffer[..read]); + } + let digest = digest.finalize(); + let mut output = String::with_capacity(digest.len() * 2); + for byte in digest { + use std::fmt::Write as _; + let _ = write!(&mut output, "{byte:02x}"); + } + Ok(output) +} +''' + + +def write_native_extension_split_aggregator_crate( + crate_dir: Path, + *, + product: str, + version: str, + sql_name: str, + target: str, + triple: str, + part_dirs: list[Path], +) -> None: + name = native_extension_cargo_package_name(product, target) + links = native_extension_cargo_links_name(product, target) + shutil.rmtree(crate_dir / "payload", ignore_errors=True) + dependency_lines = [] + for index, part_dir in enumerate(part_dirs): + dependency_name = native_extension_cargo_part_package_name(product, target, index) + dependency_path = Path(os.path.relpath(part_dir, crate_dir)).as_posix() + dependency_lines.append( + f'{dependency_name} = {{ version = "={version}", path = "{dependency_path}" }}' + ) + part_roots = [ + f" {rust_crate_ident(native_extension_cargo_part_package_name(product, target, index))}::PAYLOAD_ROOT," + for index in range(len(part_dirs)) + ] + (crate_dir / "Cargo.toml").write_text( + "\n".join( + [ + "[package]", + f'name = "{name}"', + f'version = "{version}"', + 'edition = "2024"', + 'rust-version = "1.93"', + f'description = "Cargo artifact crate for the {sql_name} Oliphaunt native extension on {target}."', + 'readme = "README.md"', + 'repository = "https://github.com/f0rr0/oliphaunt"', + 'homepage = "https://oliphaunt.dev"', + 'license = "MIT AND Apache-2.0 AND PostgreSQL"', + f'links = "{links}"', + 'build = "build.rs"', + 'include = ["Cargo.toml", "README.md", "build.rs", "src/**"]', + "", + "[lib]", + 'path = "src/lib.rs"', + "", + "[build-dependencies]", + 'sha2 = "0.10"', + *dependency_lines, + "", + "[workspace]", + "", + ] + ), + encoding="utf-8", + ) + build_rs = ( + NATIVE_EXTENSION_AGGREGATOR_BUILD_RS.replace( + "__SCHEMA__", toml_string("oliphaunt-artifact-manifest-v1") + ) + .replace("__PRODUCT__", toml_string(product)) + .replace("__TARGET__", toml_string(triple)) + .replace("__EXTENSION__", toml_string(sql_name)) + .replace("__PART_ROOTS__", "\n".join(part_roots)) + ) + (crate_dir / "build.rs").write_text(build_rs, encoding="utf-8") + + +def cargo_package(crate_dir: Path, target_dir: Path, *, no_verify: bool = False) -> Path: + name, version = read_cargo_package_name_version(crate_dir / "Cargo.toml") + command = [ + "cargo", + "package", + "--manifest-path", + str(crate_dir / "Cargo.toml"), + "--target-dir", + str(target_dir), + "--allow-dirty", + ] + if no_verify: + command.append("--no-verify") + run(command, env={**os.environ, "OLIPHAUNT_ARTIFACT_CRATE_REQUIRE_PAYLOAD": "1"}) + crate_path = target_dir / "package" / f"{name}-{version}.crate" + if not crate_path.is_file(): + raise RuntimeError(f"cargo package did not create {rel(crate_path)}") + return crate_path + + def write_native_extension_cargo_crate( crate_dir: Path, *, @@ -1331,6 +1769,7 @@ def write_native_extension_cargo_crate( links = native_extension_cargo_links_name(product, target) runtime_dir = crate_dir / "payload" extract_extension_runtime(asset, runtime_dir) + strip_extension_modules(runtime_dir, target) if not any(runtime_dir.rglob("*")): raise RuntimeError(f"{rel(asset)} did not contain extension runtime files") (crate_dir / "src").mkdir(parents=True, exist_ok=True) @@ -1523,28 +1962,59 @@ def package_native_extension_cargo_crates( triple=triple, asset=asset, ) - run( - [ - "cargo", - "package", - "--manifest-path", - str(crate_dir / "Cargo.toml"), - "--target-dir", - str(cargo_target_dir), - "--allow-dirty", - ], - env={**os.environ, "OLIPHAUNT_ARTIFACT_CRATE_REQUIRE_PAYLOAD": "1"}, - ) - crate_path = cargo_target_dir / "package" / f"{name}-{version}.crate" - if not crate_path.is_file(): - raise RuntimeError(f"cargo package did not create {rel(crate_path)}") + crate_path = cargo_package(crate_dir, cargo_target_dir) size = crate_path.stat().st_size - if size > CARGO_PACKAGE_SIZE_LIMIT_BYTES: - message = f"{rel(crate_path)} is {size} bytes, above the crates.io 10 MiB package limit" - result.add_skip(message) - if strict: - raise RuntimeError(message) - continue + if size > CARGO_EXTENSION_SPLIT_THRESHOLD_BYTES: + part_dirs = build_native_extension_part_crates( + crate_dir / "payload", + source_root, + product=str(product), + version=str(version), + sql_name=str(sql_name), + target=target, + ) + write_native_extension_split_aggregator_crate( + crate_dir, + product=str(product), + version=str(version), + sql_name=str(sql_name), + target=target, + triple=triple, + part_dirs=part_dirs, + ) + part_failed = False + for part_dir in part_dirs: + part_crate_path = cargo_package(part_dir, cargo_target_dir) + part_size = part_crate_path.stat().st_size + if part_size > CARGO_PACKAGE_SIZE_LIMIT_BYTES: + message = ( + f"{rel(part_crate_path)} is {part_size} bytes, above the crates.io " + "10 MiB package limit" + ) + result.add_skip(message) + if strict: + raise RuntimeError(message) + part_failed = True + continue + output = output_dir / part_crate_path.name + shutil.copy2(part_crate_path, output) + outputs.append(output) + if part_failed: + continue + crate_path = manual_cargo_package_source( + crate_dir / "Cargo.toml", + cargo_target_dir / "manual-package", + ) + size = crate_path.stat().st_size + if size > CARGO_PACKAGE_SIZE_LIMIT_BYTES: + message = ( + f"{rel(crate_path)} is {size} bytes after splitting, above the crates.io " + "10 MiB package limit" + ) + result.add_skip(message) + if strict: + raise RuntimeError(message) + continue output = output_dir / crate_path.name shutil.copy2(crate_path, output) outputs.append(output) From 26a787fb248188856bc34f393e55177946b93fb2 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Thu, 25 Jun 2026 14:40:48 +0000 Subject: [PATCH 007/137] fix(release): discard split extension probe crates --- tools/release/local_registry_publish.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index 33e31d8a..26851edb 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -1755,6 +1755,11 @@ def cargo_package(crate_dir: Path, target_dir: Path, *, no_verify: bool = False) return crate_path +def discard_cargo_package_artifact(crate_path: Path) -> None: + crate_path.unlink(missing_ok=True) + (crate_path.parent / "tmp-crate" / crate_path.name).unlink(missing_ok=True) + + def write_native_extension_cargo_crate( crate_dir: Path, *, @@ -1965,6 +1970,7 @@ def package_native_extension_cargo_crates( crate_path = cargo_package(crate_dir, cargo_target_dir) size = crate_path.stat().st_size if size > CARGO_EXTENSION_SPLIT_THRESHOLD_BYTES: + discard_cargo_package_artifact(crate_path) part_dirs = build_native_extension_part_crates( crate_dir / "payload", source_root, From d23cdaba63acdd289f877c5bebf4fe0034f63d60 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Thu, 25 Jun 2026 15:33:11 +0000 Subject: [PATCH 008/137] fix(release): strip native release artifacts --- Cargo.toml | 3 + .../tools/extension-artifact-packager.mjs | 21 +++ .../bin/build-postgres18-android-arm64.sh | 2 +- .../native/bin/build-postgres18-ios-device.sh | 2 +- .../bin/build-postgres18-ios-simulator.sh | 2 +- .../native/bin/build-postgres18-linux.sh | 8 +- .../native/bin/build-postgres18-macos.sh | 9 +- .../liboliphaunt/native/bin/common.sh | 13 ++ .../native/bin/mobile-postgis-extensions.sh | 8 +- .../node-direct/tools/build-node-addon.sh | 2 + tools/release/package-broker-assets.sh | 10 ++ .../package-liboliphaunt-linux-assets.sh | 4 + .../package-liboliphaunt-macos-assets.sh | 3 + .../package-liboliphaunt-mobile-assets.sh | 4 + .../package-liboliphaunt-windows-assets.ps1 | 6 + .../release/strip_native_release_binaries.py | 169 ++++++++++++++++++ 16 files changed, 251 insertions(+), 15 deletions(-) create mode 100644 tools/release/strip_native_release_binaries.py diff --git a/Cargo.toml b/Cargo.toml index 28a0abe6..7bcf5e70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,3 +22,6 @@ rust-version = "1.93" repository = "https://github.com/f0rr0/oliphaunt" homepage = "https://oliphaunt.dev" license = "MIT AND Apache-2.0 AND PostgreSQL" + +[profile.release] +strip = "symbols" diff --git a/src/extensions/artifacts/native/tools/extension-artifact-packager.mjs b/src/extensions/artifacts/native/tools/extension-artifact-packager.mjs index 27a02374..6fc08dce 100755 --- a/src/extensions/artifacts/native/tools/extension-artifact-packager.mjs +++ b/src/extensions/artifacts/native/tools/extension-artifact-packager.mjs @@ -1,4 +1,5 @@ #!/usr/bin/env bun +import { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import path from 'node:path'; import { promises as fs } from 'node:fs'; @@ -804,6 +805,24 @@ async function writeArtifactDirectory(artifactRoot, args) { await fs.writeFile(path.join(artifactRoot, 'manifest.properties'), manifest); } +function pythonCommand() { + return process.platform === 'win32' ? 'python' : 'python3'; +} + +function stripNativeReleaseBinaries(artifactRoot) { + const result = spawnSync( + pythonCommand(), + ['tools/release/strip_native_release_binaries.py', artifactRoot], + { cwd: root, stdio: 'inherit' }, + ); + if (result.error !== undefined) { + fail(`failed to run native release binary stripper: ${result.error.message}`); + } + if (result.status !== 0) { + fail(`native release binary stripper failed for ${artifactRoot}`); + } +} + async function prepareOutputFile(output, force) { if (await exists(output)) { if (!force) { @@ -834,6 +853,7 @@ async function createArtifact(argv) { await fs.rm(output, { recursive: true, force: true }); } await writeArtifactDirectory(output, args); + stripNativeReleaseBinaries(output); console.log(`path=${output}`); console.log(`sqlName=${args.sqlName}`); console.log('format=directory'); @@ -848,6 +868,7 @@ async function createArtifact(argv) { await fs.mkdir(artifactRoot, { recursive: true }); try { await writeArtifactDirectory(artifactRoot, args); + stripNativeReleaseBinaries(artifactRoot); if (args.format === 'tar') { await fs.writeFile(output, await createTar(artifactRoot)); } else { diff --git a/src/runtimes/liboliphaunt/native/bin/build-postgres18-android-arm64.sh b/src/runtimes/liboliphaunt/native/bin/build-postgres18-android-arm64.sh index 931593ef..94e523c8 100755 --- a/src/runtimes/liboliphaunt/native/bin/build-postgres18-android-arm64.sh +++ b/src/runtimes/liboliphaunt/native/bin/build-postgres18-android-arm64.sh @@ -171,7 +171,7 @@ fi cc_string="${cc[*]}" cxx_string="${cxx[*]}" postgres_cppflags="-D_GNU_SOURCE" -native_cflags="-O2 -g -fPIC -DOLIPHAUNT_EMBEDDED -DOLIPHAUNT_EMBEDDED_MOBILE_SHMEM -Wno-unused-command-line-argument" +native_cflags="$(oliphaunt_native_release_cflags -fPIC -DOLIPHAUNT_EMBEDDED -DOLIPHAUNT_EMBEDDED_MOBILE_SHMEM -Wno-unused-command-line-argument)" liboliphaunt_cflags="$native_cflags -DOLIPHAUNT_BUILTIN_PLPGSQL" pg_extension_cflags="$native_cflags $postgres_cppflags $icu_cflags" jobs="${OLIPHAUNT_JOBS:-$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)}" diff --git a/src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-device.sh b/src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-device.sh index 404046ef..ad552510 100755 --- a/src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-device.sh +++ b/src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-device.sh @@ -112,7 +112,7 @@ if [ "$ccache_mode" != "0" ] && [ "$ccache_mode" != "off" ]; then fi cc_string="${cc[*]}" cxx_string="${cxx[*]}" -native_cflags="-O2 -g -fPIC -march=armv8-a+crc -DOLIPHAUNT_EMBEDDED -DOLIPHAUNT_EMBEDDED_MOBILE_SHMEM" +native_cflags="$(oliphaunt_native_release_cflags -fPIC -march=armv8-a+crc -DOLIPHAUNT_EMBEDDED -DOLIPHAUNT_EMBEDDED_MOBILE_SHMEM)" liboliphaunt_cflags="$native_cflags -DOLIPHAUNT_BUILTIN_PLPGSQL" pg_extension_cflags="$native_cflags $icu_cflags" jobs="${OLIPHAUNT_JOBS:-$(sysctl -n hw.ncpu 2>/dev/null || echo 4)}" diff --git a/src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-simulator.sh b/src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-simulator.sh index 2ae637ca..ddb41fd5 100755 --- a/src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-simulator.sh +++ b/src/runtimes/liboliphaunt/native/bin/build-postgres18-ios-simulator.sh @@ -112,7 +112,7 @@ if [ "$ccache_mode" != "0" ] && [ "$ccache_mode" != "off" ]; then fi cc_string="${cc[*]}" cxx_string="${cxx[*]}" -native_cflags="-O2 -g -fPIC -DOLIPHAUNT_EMBEDDED -DOLIPHAUNT_EMBEDDED_MOBILE_SHMEM" +native_cflags="$(oliphaunt_native_release_cflags -fPIC -DOLIPHAUNT_EMBEDDED -DOLIPHAUNT_EMBEDDED_MOBILE_SHMEM)" liboliphaunt_cflags="$native_cflags -DOLIPHAUNT_BUILTIN_PLPGSQL" pg_extension_cflags="$native_cflags $icu_cflags" jobs="${OLIPHAUNT_JOBS:-$(sysctl -n hw.ncpu 2>/dev/null || echo 4)}" diff --git a/src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh b/src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh index 58bf0c19..9623112e 100755 --- a/src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh +++ b/src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh @@ -334,8 +334,8 @@ if [ "$ccache_mode" != "0" ] && [ "$ccache_mode" != "off" ]; then fi cc_string="${cc[*]}" cxx_string="${cxx[*]}" -native_cflags="-O2 -g -fPIC -DOLIPHAUNT_EMBEDDED" -postgres_embedded_copt="-g -fPIC -DOLIPHAUNT_EMBEDDED" +native_cflags="$(oliphaunt_native_release_cflags -fPIC -DOLIPHAUNT_EMBEDDED)" +postgres_embedded_copt="$(oliphaunt_native_release_cflags -fPIC -DOLIPHAUNT_EMBEDDED | sed 's/^-O2 //')" liboliphaunt_cflags="$native_cflags -DOLIPHAUNT_BUILTIN_PLPGSQL" embedded_module_be_dllibs="-Wl,--no-as-needed -Wl,-z,defs -L$out_dir -Wl,-rpath,$out_dir -loliphaunt" normal_module_be_dllibs="" @@ -1016,12 +1016,12 @@ build_native_postgis_sqlite_dependency() { rsync -a --delete --exclude .git "$source_dir/" "$build_root/" ( cd "$build_root" - CC="$native_cc" CFLAGS="-O2 -g -fPIC" ./configure \ + CC="$native_cc" CFLAGS="$(oliphaunt_native_release_cflags -fPIC)" ./configure \ --disable-shared \ --enable-static \ --prefix="$dependency_dir" >> "$postgis_dependency_log" 2>&1 make -j"$jobs" sqlite3.c >> "$postgis_dependency_log" 2>&1 - "$native_cc" -O2 -g -fPIC \ + "$native_cc" $(oliphaunt_native_release_cflags -fPIC) \ -DSQLITE_THREADSAFE=0 \ -DSQLITE_OMIT_LOAD_EXTENSION \ -c sqlite3.c \ diff --git a/src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh b/src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh index eccfa0bd..6f99b9d7 100755 --- a/src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh +++ b/src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh @@ -582,12 +582,14 @@ else export CXX="$native_cxx" fi +native_cflags="$(oliphaunt_native_release_cflags -fPIC -DOLIPHAUNT_EMBEDDED)" desired_patch_hash="$(patch_series_hash)" desired_build_hash="$( { printf 'patches=%s\n' "$desired_patch_hash" printf 'cc=%s\n' "$CC" printf 'cxx=%s\n' "$CXX" + printf 'native_cflags=%s\n' "$native_cflags" printf 'icu_source=%s\n' "$(oliphaunt_icu_source_commit "$icu_source_dir")" printf 'icu_script=%s\n' "$(oliphaunt_icu_script_sha256 "$script_dir")" printf 'postgres_configure=with-icu\n' @@ -598,7 +600,6 @@ if [ -f "$build_stamp" ]; then current_build_hash="$(cat "$build_stamp")" fi -native_cflags="-O2 -g -fPIC -DOLIPHAUNT_EMBEDDED" normal_module_be_dllibs="-bundle_loader $install_dir/bin/postgres" embedded_module_be_dllibs="-L$out_dir -loliphaunt -Wl,-rpath,$out_dir" postgis_cc="${OLIPHAUNT_POSTGIS_CC:-$native_cc}" @@ -781,7 +782,7 @@ audit_embedded_module() { compile_liboliphaunt_objects() { local index for index in "${!liboliphaunt_sources[@]}"; do - $CC -O2 -g -fPIC \ + $CC $(oliphaunt_native_release_cflags -fPIC) \ -I"$repo_root/src/runtimes/liboliphaunt/native/include" \ -I"$repo_root/src/runtimes/liboliphaunt/native/src" \ -c "${liboliphaunt_sources[$index]}" \ @@ -995,12 +996,12 @@ build_native_postgis_sqlite_dependency() { rsync -a --delete --exclude .git "$source_dir/" "$build_root/" ( cd "$build_root" - CC="$native_cc" CFLAGS="-O2 -g -fPIC" ./configure \ + CC="$native_cc" CFLAGS="$(oliphaunt_native_release_cflags -fPIC)" ./configure \ --disable-shared \ --enable-static \ --prefix="$dependency_dir" >> "$postgis_dependency_log" 2>&1 make -j"$jobs" sqlite3.c >> "$postgis_dependency_log" 2>&1 - "$native_cc" -O2 -g -fPIC \ + "$native_cc" $(oliphaunt_native_release_cflags -fPIC) \ -DSQLITE_THREADSAFE=0 \ -DSQLITE_OMIT_LOAD_EXTENSION \ -c sqlite3.c \ diff --git a/src/runtimes/liboliphaunt/native/bin/common.sh b/src/runtimes/liboliphaunt/native/bin/common.sh index e139c132..904df9e6 100755 --- a/src/runtimes/liboliphaunt/native/bin/common.sh +++ b/src/runtimes/liboliphaunt/native/bin/common.sh @@ -8,3 +8,16 @@ oliphaunt_resolve_repo_root() { fi cd "$script_dir/../../../../.." && pwd } + +oliphaunt_native_release_cflags() { + printf '%s' '-O2' + case "${OLIPHAUNT_NATIVE_DEBUG_SYMBOLS:-0}" in + 1|true|TRUE|yes|YES|on|ON) + printf ' %s' '-g' + ;; + esac + while [ "$#" -gt 0 ]; do + printf ' %s' "$1" + shift + done +} diff --git a/src/runtimes/liboliphaunt/native/bin/mobile-postgis-extensions.sh b/src/runtimes/liboliphaunt/native/bin/mobile-postgis-extensions.sh index 715c625c..e6d744dd 100644 --- a/src/runtimes/liboliphaunt/native/bin/mobile-postgis-extensions.sh +++ b/src/runtimes/liboliphaunt/native/bin/mobile-postgis-extensions.sh @@ -106,13 +106,13 @@ build_postgis_sqlite_dependency() { cd "$build_root" case "$oliphaunt_mobile_target" in ios-simulator | ios-device) - CC="$cc_string" CFLAGS="-O2 -g -fPIC" ./configure \ + CC="$cc_string" CFLAGS="$(oliphaunt_native_release_cflags -fPIC)" ./configure \ --host=aarch64-apple-darwin \ --disable-shared \ --enable-static \ --prefix="$dependency_dir" >> "$make_log" 2>&1 make -j"$jobs" sqlite3.c >> "$make_log" 2>&1 - "${cc[@]}" -O2 -g -fPIC \ + "${cc[@]}" $(oliphaunt_native_release_cflags -fPIC) \ -DSQLITE_THREADSAFE=0 \ -DSQLITE_OMIT_LOAD_EXTENSION \ -c sqlite3.c \ @@ -120,13 +120,13 @@ build_postgis_sqlite_dependency() { "$libtool_path" -static -o "$archive" sqlite3.o >> "$make_log" 2>&1 ;; android-arm64 | android-x86_64) - CC="$clang_path" CFLAGS="-O2 -g -fPIC" ./configure \ + CC="$clang_path" CFLAGS="$(oliphaunt_native_release_cflags -fPIC)" ./configure \ --host="$android_host" \ --disable-shared \ --enable-static \ --prefix="$dependency_dir" >> "$make_log" 2>&1 make -j"$jobs" sqlite3.c >> "$make_log" 2>&1 - "$clang_path" -O2 -g -fPIC \ + "$clang_path" $(oliphaunt_native_release_cflags -fPIC) \ -DSQLITE_THREADSAFE=0 \ -DSQLITE_OMIT_LOAD_EXTENSION \ -c sqlite3.c \ diff --git a/src/runtimes/node-direct/tools/build-node-addon.sh b/src/runtimes/node-direct/tools/build-node-addon.sh index 72458f93..51b73de7 100755 --- a/src/runtimes/node-direct/tools/build-node-addon.sh +++ b/src/runtimes/node-direct/tools/build-node-addon.sh @@ -168,6 +168,8 @@ case "$platform" in ;; esac +python3 tools/release/strip_native_release_binaries.py "$addon_file" + node - "$addon" <<'JS' const addonPath = process.argv[2]; const addon = require(addonPath); diff --git a/tools/release/package-broker-assets.sh b/tools/release/package-broker-assets.sh index 4a3d5b55..a403fe7e 100755 --- a/tools/release/package-broker-assets.sh +++ b/tools/release/package-broker-assets.sh @@ -18,6 +18,15 @@ fail() { exit 1 } +python_bin="${PYTHON:-python3}" +if ! command -v "$python_bin" >/dev/null 2>&1; then + if command -v python >/dev/null 2>&1; then + python_bin=python + else + fail "missing required command: python3" + fi +fi + case "$host_os:$host_arch" in Darwin:arm64) target_id="macos-arm64" ;; Linux:x86_64|Linux:amd64) target_id="linux-x64-gnu" ;; @@ -52,6 +61,7 @@ cargo build -p oliphaunt-broker --release --locked cp "$broker_bin" "$stage/bin/$broker_stage_name" chmod 0755 "$stage/bin/$broker_stage_name" +"$python_bin" tools/release/strip_native_release_binaries.py "$stage" cat >"$stage/manifest.properties" < Stripping staged liboliphaunt $target_id release binaries" +python3 tools/release/strip_native_release_binaries.py "$stage" + echo "==> Smoke testing staged liboliphaunt $target_id release layout" env \ OLIPHAUNT_WORK_ROOT="$work_root" \ diff --git a/tools/release/package-liboliphaunt-macos-assets.sh b/tools/release/package-liboliphaunt-macos-assets.sh index bf052b4e..c1a20282 100755 --- a/tools/release/package-liboliphaunt-macos-assets.sh +++ b/tools/release/package-liboliphaunt-macos-assets.sh @@ -66,6 +66,9 @@ cp "$lib" "$stage/lib/" rsync -a --delete "$embedded_modules/" "$stage/lib/modules/" rsync -a --delete --exclude 'share/icu/***' "$runtime/" "$stage/runtime/" +echo "==> Stripping staged liboliphaunt $target_id release binaries" +python3 tools/release/strip_native_release_binaries.py "$stage" + echo "==> Smoke testing staged liboliphaunt $target_id release layout" env \ OLIPHAUNT_WORK_ROOT="$work_root" \ diff --git a/tools/release/package-liboliphaunt-mobile-assets.sh b/tools/release/package-liboliphaunt-mobile-assets.sh index ce7530ff..3734bea2 100755 --- a/tools/release/package-liboliphaunt-mobile-assets.sh +++ b/tools/release/package-liboliphaunt-mobile-assets.sh @@ -75,6 +75,8 @@ package_android() { mkdir -p "$stage/include" "$stage/jni/$abi" rsync -a --delete "$headers_dir/" "$stage/include/" cp "$lib" "$stage/jni/$abi/" + echo "==> Stripping staged liboliphaunt Android $abi release binaries" + python3 tools/release/strip_native_release_binaries.py "$stage" archive_staged_dir "$stage" } @@ -111,6 +113,8 @@ package_ios() { mkdir -p "$stage_ios" rsync -a --delete "$ios_xcframework" "$stage_ios/" + echo "==> Stripping staged liboliphaunt iOS release binaries" + python3 tools/release/strip_native_release_binaries.py "$stage_ios" archive_staged_dir "$stage_ios" archive_swiftpm_xcframework \ diff --git a/tools/release/package-liboliphaunt-windows-assets.ps1 b/tools/release/package-liboliphaunt-windows-assets.ps1 index 08846b31..94faedad 100644 --- a/tools/release/package-liboliphaunt-windows-assets.ps1 +++ b/tools/release/package-liboliphaunt-windows-assets.ps1 @@ -137,6 +137,12 @@ if (Test-Path $StagedIcu) { Remove-Item -Recurse -Force $StagedIcu } +Write-Output "==> Stripping staged liboliphaunt $TargetId release binaries" +python tools/release/strip_native_release_binaries.py $Stage +if ($LASTEXITCODE -ne 0) { + Fail "failed to strip staged Windows liboliphaunt release binaries" +} + Write-Output "==> Smoke testing staged liboliphaunt $TargetId release layout" $SmokeRoot = Join-Path $env:TEMP "liboliphaunt-release-smoke-$TargetId" Remove-Item -Recurse -Force $SmokeRoot -ErrorAction SilentlyContinue diff --git a/tools/release/strip_native_release_binaries.py b/tools/release/strip_native_release_binaries.py new file mode 100644 index 00000000..13ddb47f --- /dev/null +++ b/tools/release/strip_native_release_binaries.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +"""Strip debug/symbol data from native release payloads before archiving.""" + +from __future__ import annotations + +import argparse +import os +import shutil +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Iterable, NoReturn + + +MACHO_MAGICS = { + b"\xfe\xed\xfa\xce", + b"\xce\xfa\xed\xfe", + b"\xfe\xed\xfa\xcf", + b"\xcf\xfa\xed\xfe", + b"\xca\xfe\xba\xbe", + b"\xbe\xba\xfe\xca", +} + + +@dataclass(frozen=True) +class NativeFile: + path: Path + kind: str + archive: bool = False + + +def fail(message: str) -> NoReturn: + print(f"strip_native_release_binaries.py: {message}", file=sys.stderr) + raise SystemExit(2) + + +def read_prefix(path: Path, size: int = 8) -> bytes: + try: + with path.open("rb") as handle: + return handle.read(size) + except OSError as error: + fail(f"failed to read {path}: {error}") + + +def classify(path: Path) -> NativeFile | None: + prefix = read_prefix(path) + if prefix.startswith(b"\x7fELF"): + return NativeFile(path, "elf") + if prefix[:4] in MACHO_MAGICS: + return NativeFile(path, "macho") + if prefix.startswith(b"MZ"): + return NativeFile(path, "pe") + if prefix.startswith(b"!\n"): + return NativeFile(path, "archive", archive=True) + return None + + +def iter_files(roots: Iterable[Path]) -> Iterable[Path]: + for root in roots: + if root.is_file(): + yield root + continue + if not root.is_dir(): + fail(f"input path does not exist: {root}") + for path in sorted(root.rglob("*")): + if path.is_file(): + yield path + + +def env_tool(*names: str) -> str | None: + for name in names: + value = os.environ.get(name) + if value: + return value + return None + + +def find_tool(*names: str) -> str | None: + for name in names: + resolved = shutil.which(name) + if resolved: + return resolved + return None + + +def darwin_strip_tool() -> str | None: + override = env_tool("OLIPHAUNT_MACHO_STRIP", "OLIPHAUNT_STRIP") + if override: + return override + if sys.platform == "darwin": + result = subprocess.run( + ["xcrun", "--find", "strip"], + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, + ) + if result.returncode == 0 and result.stdout.strip(): + return result.stdout.strip() + return find_tool("strip") + + +def strip_tool_for(native: NativeFile) -> tuple[str | None, list[str]]: + if native.kind == "macho": + tool = darwin_strip_tool() + if not tool: + fail(f"missing strip tool for Mach-O file {native.path}") + return tool, ["-S"] + if native.kind == "pe": + tool = env_tool("OLIPHAUNT_PE_STRIP", "OLIPHAUNT_STRIP") or find_tool("llvm-strip", "strip") + if not tool: + print(f"skippedPeNativeFile={native.path}", file=sys.stderr) + return None, [] + return tool, ["--strip-debug"] + if native.archive and sys.platform == "darwin": + tool = darwin_strip_tool() + if not tool: + fail(f"missing strip tool for archive {native.path}") + return tool, ["-S"] + if native.archive and native.path.suffix.lower() == ".lib": + tool = env_tool("OLIPHAUNT_PE_STRIP", "OLIPHAUNT_STRIP") or find_tool("llvm-strip", "strip") + if not tool: + print(f"skippedPeNativeFile={native.path}", file=sys.stderr) + return None, [] + return tool, ["--strip-debug"] + tool = env_tool("OLIPHAUNT_ELF_STRIP", "OLIPHAUNT_STRIP") or find_tool("llvm-strip", "strip") + if not tool: + fail(f"missing strip tool for {native.kind} file {native.path}") + if native.archive: + return tool, ["--strip-debug"] + return tool, ["--strip-unneeded"] + + +def strip_native(native: NativeFile) -> bool: + before = native.path.stat().st_size + tool, flags = strip_tool_for(native) + if tool is None: + return False + result = subprocess.run( + [tool, *flags, str(native.path)], + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + if result.returncode != 0: + stderr = result.stderr.strip() + fail(f"{tool} failed for {native.path}: {stderr or f'exit {result.returncode}'}") + return native.path.stat().st_size != before + + +def main(argv: list[str]) -> int: + parser = argparse.ArgumentParser() + parser.add_argument("paths", nargs="+", type=Path) + args = parser.parse_args(argv) + + native_files = [native for path in iter_files(args.paths) if (native := classify(path)) is not None] + changed = 0 + for native in native_files: + if strip_native(native): + changed += 1 + print(f"strippedNativeFiles={changed}") + print(f"checkedNativeFiles={len(native_files)}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) From fc13dea9ac08e66739d478ced6a9c02333c7ed86 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Thu, 25 Jun 2026 16:01:21 +0000 Subject: [PATCH 009/137] fix(release): align wasix cargo artifact crates --- Cargo.lock | 20 ++--- docs/internal/DONE.md | 12 +-- docs/internal/IMPLEMENTATION_CHECKLIST.md | 2 +- docs/internal/PG18_WASIX_POSTGRES.md | 4 +- .../consumer-dx-release-blueprint.md | 2 +- docs/maintainers/release-setup.md | 10 +-- examples/electron-wasix/src-wasix/Cargo.lock | 20 ++--- examples/tauri-wasix/src-tauri/Cargo.lock | 20 ++--- .../crates/oliphaunt-wasix/Cargo.toml | 88 +++++++++---------- .../oliphaunt-wasix/src/oliphaunt/aot.rs | 41 ++++----- .../oliphaunt-wasix/src/oliphaunt/assets.rs | 22 ++--- .../tauri-sqlx-vanilla/src-tauri/Cargo.lock | 20 ++--- .../aot/aarch64-apple-darwin/Cargo.toml | 2 +- .../crates/aot/aarch64-apple-darwin/README.md | 2 +- .../crates/aot/aarch64-apple-darwin/build.rs | 4 +- .../aot/aarch64-unknown-linux-gnu/Cargo.toml | 2 +- .../aot/aarch64-unknown-linux-gnu/README.md | 2 +- .../aot/aarch64-unknown-linux-gnu/build.rs | 4 +- .../aot/x86_64-pc-windows-msvc/Cargo.toml | 2 +- .../aot/x86_64-pc-windows-msvc/README.md | 2 +- .../aot/x86_64-pc-windows-msvc/build.rs | 4 +- .../aot/x86_64-unknown-linux-gnu/Cargo.toml | 2 +- .../aot/x86_64-unknown-linux-gnu/README.md | 2 +- .../aot/x86_64-unknown-linux-gnu/build.rs | 4 +- .../wasix/crates/assets/Cargo.toml | 4 +- .../wasix/crates/assets/README.md | 2 +- src/runtimes/liboliphaunt/wasix/release.toml | 10 +-- .../wasix/tools/build-aot-target.sh | 2 +- .../fixtures/consumer-shape/products.json | 10 +-- tools/policy/check-dependency-invariants.sh | 34 ++++--- tools/policy/check-native-boundaries.sh | 4 +- tools/release/artifact_target_matrix.py | 2 +- tools/release/check_consumer_shape.py | 24 ++--- tools/release/check_release_metadata.py | 22 ++--- ...kage_liboliphaunt_wasix_cargo_artifacts.py | 10 +-- tools/release/release.py | 22 +++++ tools/xtask/src/asset_checks.rs | 10 +-- 37 files changed, 241 insertions(+), 208 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5b8d1e69..0733b7df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2303,11 +2303,11 @@ dependencies = [ "flate2", "hex", "oliphaunt-icu", - "oliphaunt-wasix-aot-aarch64-apple-darwin", - "oliphaunt-wasix-aot-aarch64-unknown-linux-gnu", - "oliphaunt-wasix-aot-x86_64-pc-windows-msvc", - "oliphaunt-wasix-aot-x86_64-unknown-linux-gnu", - "oliphaunt-wasix-assets", + "liboliphaunt-wasix-aot-aarch64-apple-darwin", + "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu", + "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc", + "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", + "liboliphaunt-wasix-portable", "regex", "serde", "serde_json", @@ -2327,7 +2327,7 @@ dependencies = [ ] [[package]] -name = "oliphaunt-wasix-aot-aarch64-apple-darwin" +name = "liboliphaunt-wasix-aot-aarch64-apple-darwin" version = "0.1.0" dependencies = [ "serde_json", @@ -2335,7 +2335,7 @@ dependencies = [ ] [[package]] -name = "oliphaunt-wasix-aot-aarch64-unknown-linux-gnu" +name = "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu" version = "0.1.0" dependencies = [ "serde_json", @@ -2343,7 +2343,7 @@ dependencies = [ ] [[package]] -name = "oliphaunt-wasix-aot-x86_64-pc-windows-msvc" +name = "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc" version = "0.1.0" dependencies = [ "serde_json", @@ -2351,7 +2351,7 @@ dependencies = [ ] [[package]] -name = "oliphaunt-wasix-aot-x86_64-unknown-linux-gnu" +name = "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu" version = "0.1.0" dependencies = [ "serde_json", @@ -2359,7 +2359,7 @@ dependencies = [ ] [[package]] -name = "oliphaunt-wasix-assets" +name = "liboliphaunt-wasix-portable" version = "0.1.0" dependencies = [ "serde", diff --git a/docs/internal/DONE.md b/docs/internal/DONE.md index 4941260d..f8cbf801 100644 --- a/docs/internal/DONE.md +++ b/docs/internal/DONE.md @@ -116,7 +116,7 @@ Production build inputs now live under `assets/`. Implemented: - root `oliphaunt-wasix` crate remains the public crate; -- `oliphaunt-wasix-assets` is the published runtime asset crate skeleton; +- `liboliphaunt-wasix-portable` is the published runtime asset crate skeleton; - source-only target AOT crate templates exist under `src/runtimes/liboliphaunt/wasix/crates/aot/*`; - `xtask` owns source checks, build orchestration, packaging, manifest checks, package sizing, upstream audits, and source-spine validation; @@ -508,7 +508,7 @@ Implemented coverage: - both generated build plans now support native and SQL-only extensions. The local WASIX build produced all requested contrib and PGXS extension payloads, generated local macOS arm64 AOT artifacts for all requested native modules, - and packaged all requested extension archives into `oliphaunt-wasix-assets`; + and packaged all requested extension archives into `liboliphaunt-wasix-portable`; - contrib packaging now carries extension-owned tsearch rule files into `share/postgresql/tsearch_data`, matching Oliphaunt behavior for `dict_xsyn` and `unaccent`; @@ -973,8 +973,8 @@ Latest local release work: explicit `OLIPHAUNT_WASM_ALLOW_ASYNCIFY_EXPERIMENT=1` override is reserved for local snapshot/journaling experiments; - final package sizes stayed under crates.io's 10 MB compressed limit: - `oliphaunt-wasix` about 7.15 MB, `oliphaunt-wasix-assets` about 4.87 MB, and - `oliphaunt-wasix-aot-aarch64-apple-darwin` about 5.62 MB; + `oliphaunt-wasix` about 7.15 MB, `liboliphaunt-wasix-portable` about 4.87 MB, and + `liboliphaunt-wasix-aot-aarch64-apple-darwin` about 5.62 MB; - `cargo test --release --workspace --all-targets`, `cargo check --workspace --no-default-features --all-targets`, `cargo run -p xtask -- assets check --strict-generated`, and @@ -1007,8 +1007,8 @@ Latest local release work: normal user dependency tree; - the public dependency graph now uses Cargo target-specific dependencies for AOT packs, so a normal `oliphaunt-wasix` install resolves the target-independent - `oliphaunt-wasix-assets` crate plus only the current platform's - `oliphaunt-wasix-aot-*` crate; + `liboliphaunt-wasix-portable` crate plus only the current platform's + `liboliphaunt-wasix-aot-*` crate; - source-only `tools/policy/check-rust-test-topology.sh` no longer runs broad Cargo product validation from the root policy lane. `pnpm moon run liboliphaunt-wasix:smoke` is now the hard runtime gate and requires portable diff --git a/docs/internal/IMPLEMENTATION_CHECKLIST.md b/docs/internal/IMPLEMENTATION_CHECKLIST.md index da618f9a..3d25988b 100644 --- a/docs/internal/IMPLEMENTATION_CHECKLIST.md +++ b/docs/internal/IMPLEMENTATION_CHECKLIST.md @@ -1000,7 +1000,7 @@ Run before claiming this architecture complete: - [x] The WASIX Rust publishing surface now uses the WASIX product name instead of the generic WASM name. The public Cargo package is `oliphaunt-wasix`, the Rust crate/import identifier is `oliphaunt_wasix`, the internal payload crates - publish as `oliphaunt-wasix-assets` and `oliphaunt-wasix-aot-*`, and CI/release + publish as `liboliphaunt-wasix-portable` and `liboliphaunt-wasix-aot-*`, and CI/release artifact paths use `target/oliphaunt-wasix`. Local evidence: hidden-file-aware scan for the retired WASM package/import spellings returns no source matches, `cargo metadata --locked --format-version 1 --no-deps` resolves the renamed diff --git a/docs/internal/PG18_WASIX_POSTGRES.md b/docs/internal/PG18_WASIX_POSTGRES.md index 790c41b0..c2d01a1c 100644 --- a/docs/internal/PG18_WASIX_POSTGRES.md +++ b/docs/internal/PG18_WASIX_POSTGRES.md @@ -487,7 +487,7 @@ The Rust asset parser preserves the same source-fingerprint metadata that xtask writes into PG18 asset manifests. Embedded PGDATA template manifests must match the top-level asset manifest fingerprint, and bundled AOT manifests must match the same fingerprint and PostgreSQL version before their module hashes are -accepted. The `oliphaunt-wasix-assets` build script probes +accepted. The `liboliphaunt-wasix-portable` build script probes `target/oliphaunt-wasix/assets` plus the publishable payload unless `OLIPHAUNT_WASM_GENERATED_ASSETS_DIR` explicitly overrides the asset directory. Any selected PG18 manifest must carry a non-empty source-fingerprint plus a @@ -503,7 +503,7 @@ PG18 lane instead of being paired with PG18 binaries. Crate package-size enforcement is deliberately released-lane only for now. The PG18 lane writes experimental generated assets under ignored target paths; it is -not staged into the publishable `oliphaunt-wasix-assets/payload` and AOT crate +not staged into the publishable `liboliphaunt-wasix-portable/payload` and AOT crate `artifacts` directories. Therefore `assets release-build --source fingerprint stable` must use `--skip-package-size` until PG18 gets a dedicated release-staging path; otherwise xtask fails instead of silently measuring the diff --git a/docs/maintainers/consumer-dx-release-blueprint.md b/docs/maintainers/consumer-dx-release-blueprint.md index 6d4f17a7..abca9aad 100644 --- a/docs/maintainers/consumer-dx-release-blueprint.md +++ b/docs/maintainers/consumer-dx-release-blueprint.md @@ -342,7 +342,7 @@ fn main() { ``` WASIX uses Cargo-selected runtime artifacts. The public `oliphaunt-wasix` crate -depends on `oliphaunt-wasix-assets` and target-specific `oliphaunt-wasix-aot-*` +depends on `liboliphaunt-wasix-portable` and target-specific `liboliphaunt-wasix-aot-*` artifact crates. Release packaging generates and packages those public artifact crates directly from staged WASIX release assets. Each generated `.crate` must fit the crates.io 10 MB package limit. Release packaging publishes the artifact diff --git a/docs/maintainers/release-setup.md b/docs/maintainers/release-setup.md index f3d44863..a1336959 100644 --- a/docs/maintainers/release-setup.md +++ b/docs/maintainers/release-setup.md @@ -107,11 +107,11 @@ Products: - `oliphaunt` - `oliphaunt-wasix` - `oliphaunt-icu` -- `oliphaunt-wasix-assets` -- `oliphaunt-wasix-aot-aarch64-apple-darwin` -- `oliphaunt-wasix-aot-x86_64-unknown-linux-gnu` -- `oliphaunt-wasix-aot-aarch64-unknown-linux-gnu` -- `oliphaunt-wasix-aot-x86_64-pc-windows-msvc` +- `liboliphaunt-wasix-portable` +- `liboliphaunt-wasix-aot-aarch64-apple-darwin` +- `liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu` +- `liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu` +- `liboliphaunt-wasix-aot-x86_64-pc-windows-msvc` Setup: diff --git a/examples/electron-wasix/src-wasix/Cargo.lock b/examples/electron-wasix/src-wasix/Cargo.lock index 62b6a011..8e4916b7 100644 --- a/examples/electron-wasix/src-wasix/Cargo.lock +++ b/examples/electron-wasix/src-wasix/Cargo.lock @@ -1888,11 +1888,11 @@ dependencies = [ "filetime", "flate2", "hex", - "oliphaunt-wasix-aot-aarch64-apple-darwin", - "oliphaunt-wasix-aot-aarch64-unknown-linux-gnu", - "oliphaunt-wasix-aot-x86_64-pc-windows-msvc", - "oliphaunt-wasix-aot-x86_64-unknown-linux-gnu", - "oliphaunt-wasix-assets", + "liboliphaunt-wasix-aot-aarch64-apple-darwin", + "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu", + "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc", + "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", + "liboliphaunt-wasix-portable", "regex", "serde", "serde_json", @@ -1910,7 +1910,7 @@ dependencies = [ ] [[package]] -name = "oliphaunt-wasix-aot-aarch64-apple-darwin" +name = "liboliphaunt-wasix-aot-aarch64-apple-darwin" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "9576d617b17ff231bd9edac4e9a4aec7e20b9e09f5db1fe1791d730e2af2b0ac" @@ -1920,7 +1920,7 @@ dependencies = [ ] [[package]] -name = "oliphaunt-wasix-aot-aarch64-unknown-linux-gnu" +name = "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "43cdd574cd33c901cab077a772364ff82760c0e4d40747c4811fe8cf102ca5c3" @@ -1930,7 +1930,7 @@ dependencies = [ ] [[package]] -name = "oliphaunt-wasix-aot-x86_64-pc-windows-msvc" +name = "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "47dbaab95593814aaa187d44e49bc54c02a14a559d6d30f09c0785282ef7467d" @@ -1940,7 +1940,7 @@ dependencies = [ ] [[package]] -name = "oliphaunt-wasix-aot-x86_64-unknown-linux-gnu" +name = "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "0afe5cb3df0987556274309165ca158c644437421bd93fa2892023b6a4578da4" @@ -1950,7 +1950,7 @@ dependencies = [ ] [[package]] -name = "oliphaunt-wasix-assets" +name = "liboliphaunt-wasix-portable" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "6aafe0b142fc074331ae191f07c3df3b0973b6d95dfcf6c88b66d4969fa0bce4" diff --git a/examples/tauri-wasix/src-tauri/Cargo.lock b/examples/tauri-wasix/src-tauri/Cargo.lock index 0f0807c3..6b8ecb4d 100644 --- a/examples/tauri-wasix/src-tauri/Cargo.lock +++ b/examples/tauri-wasix/src-tauri/Cargo.lock @@ -3362,11 +3362,11 @@ dependencies = [ "filetime", "flate2", "hex", - "oliphaunt-wasix-aot-aarch64-apple-darwin", - "oliphaunt-wasix-aot-aarch64-unknown-linux-gnu", - "oliphaunt-wasix-aot-x86_64-pc-windows-msvc", - "oliphaunt-wasix-aot-x86_64-unknown-linux-gnu", - "oliphaunt-wasix-assets", + "liboliphaunt-wasix-aot-aarch64-apple-darwin", + "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu", + "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc", + "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", + "liboliphaunt-wasix-portable", "regex", "serde", "serde_json", @@ -3384,7 +3384,7 @@ dependencies = [ ] [[package]] -name = "oliphaunt-wasix-aot-aarch64-apple-darwin" +name = "liboliphaunt-wasix-aot-aarch64-apple-darwin" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "9576d617b17ff231bd9edac4e9a4aec7e20b9e09f5db1fe1791d730e2af2b0ac" @@ -3394,7 +3394,7 @@ dependencies = [ ] [[package]] -name = "oliphaunt-wasix-aot-aarch64-unknown-linux-gnu" +name = "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "43cdd574cd33c901cab077a772364ff82760c0e4d40747c4811fe8cf102ca5c3" @@ -3404,7 +3404,7 @@ dependencies = [ ] [[package]] -name = "oliphaunt-wasix-aot-x86_64-pc-windows-msvc" +name = "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "47dbaab95593814aaa187d44e49bc54c02a14a559d6d30f09c0785282ef7467d" @@ -3414,7 +3414,7 @@ dependencies = [ ] [[package]] -name = "oliphaunt-wasix-aot-x86_64-unknown-linux-gnu" +name = "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "0afe5cb3df0987556274309165ca158c644437421bd93fa2892023b6a4578da4" @@ -3424,7 +3424,7 @@ dependencies = [ ] [[package]] -name = "oliphaunt-wasix-assets" +name = "liboliphaunt-wasix-portable" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "6aafe0b142fc074331ae191f07c3df3b0973b6d95dfcf6c88b66d4969fa0bce4" diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml b/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml index d1ec3f62..1d5f4359 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml @@ -20,45 +20,45 @@ exclude = [ [features] default = [] extensions = [] -extension-amcheck = ["extensions", "oliphaunt-wasix-assets/extension-amcheck"] -extension-auto-explain = ["extensions", "oliphaunt-wasix-assets/extension-auto-explain"] -extension-bloom = ["extensions", "oliphaunt-wasix-assets/extension-bloom"] -extension-btree-gin = ["extensions", "oliphaunt-wasix-assets/extension-btree-gin"] -extension-btree-gist = ["extensions", "oliphaunt-wasix-assets/extension-btree-gist"] -extension-citext = ["extensions", "oliphaunt-wasix-assets/extension-citext"] -extension-cube = ["extensions", "oliphaunt-wasix-assets/extension-cube"] -extension-dict-int = ["extensions", "oliphaunt-wasix-assets/extension-dict-int"] -extension-dict-xsyn = ["extensions", "oliphaunt-wasix-assets/extension-dict-xsyn"] -extension-earthdistance = ["extensions", "oliphaunt-wasix-assets/extension-earthdistance"] -extension-file-fdw = ["extensions", "oliphaunt-wasix-assets/extension-file-fdw"] -extension-fuzzystrmatch = ["extensions", "oliphaunt-wasix-assets/extension-fuzzystrmatch"] -extension-hstore = ["extensions", "oliphaunt-wasix-assets/extension-hstore"] -extension-intarray = ["extensions", "oliphaunt-wasix-assets/extension-intarray"] -extension-isn = ["extensions", "oliphaunt-wasix-assets/extension-isn"] -extension-lo = ["extensions", "oliphaunt-wasix-assets/extension-lo"] -extension-ltree = ["extensions", "oliphaunt-wasix-assets/extension-ltree"] -extension-pageinspect = ["extensions", "oliphaunt-wasix-assets/extension-pageinspect"] -extension-pg-buffercache = ["extensions", "oliphaunt-wasix-assets/extension-pg-buffercache"] -extension-pg-freespacemap = ["extensions", "oliphaunt-wasix-assets/extension-pg-freespacemap"] -extension-pg-hashids = ["extensions", "oliphaunt-wasix-assets/extension-pg-hashids"] -extension-pg-ivm = ["extensions", "oliphaunt-wasix-assets/extension-pg-ivm"] -extension-pg-surgery = ["extensions", "oliphaunt-wasix-assets/extension-pg-surgery"] -extension-pg-textsearch = ["extensions", "oliphaunt-wasix-assets/extension-pg-textsearch"] -extension-pg-trgm = ["extensions", "oliphaunt-wasix-assets/extension-pg-trgm"] -extension-pg-uuidv7 = ["extensions", "oliphaunt-wasix-assets/extension-pg-uuidv7"] -extension-pg-visibility = ["extensions", "oliphaunt-wasix-assets/extension-pg-visibility"] -extension-pg-walinspect = ["extensions", "oliphaunt-wasix-assets/extension-pg-walinspect"] -extension-pgcrypto = ["extensions", "oliphaunt-wasix-assets/extension-pgcrypto"] -extension-pgtap = ["extensions", "oliphaunt-wasix-assets/extension-pgtap"] -extension-postgis = ["extensions", "oliphaunt-wasix-assets/extension-postgis"] -extension-seg = ["extensions", "oliphaunt-wasix-assets/extension-seg"] -extension-tablefunc = ["extensions", "oliphaunt-wasix-assets/extension-tablefunc"] -extension-tcn = ["extensions", "oliphaunt-wasix-assets/extension-tcn"] -extension-tsm-system-rows = ["extensions", "oliphaunt-wasix-assets/extension-tsm-system-rows"] -extension-tsm-system-time = ["extensions", "oliphaunt-wasix-assets/extension-tsm-system-time"] -extension-unaccent = ["extensions", "oliphaunt-wasix-assets/extension-unaccent"] -extension-uuid-ossp = ["extensions", "oliphaunt-wasix-assets/extension-uuid-ossp"] -extension-vector = ["extensions", "oliphaunt-wasix-assets/extension-vector"] +extension-amcheck = ["extensions", "liboliphaunt-wasix-portable/extension-amcheck"] +extension-auto-explain = ["extensions", "liboliphaunt-wasix-portable/extension-auto-explain"] +extension-bloom = ["extensions", "liboliphaunt-wasix-portable/extension-bloom"] +extension-btree-gin = ["extensions", "liboliphaunt-wasix-portable/extension-btree-gin"] +extension-btree-gist = ["extensions", "liboliphaunt-wasix-portable/extension-btree-gist"] +extension-citext = ["extensions", "liboliphaunt-wasix-portable/extension-citext"] +extension-cube = ["extensions", "liboliphaunt-wasix-portable/extension-cube"] +extension-dict-int = ["extensions", "liboliphaunt-wasix-portable/extension-dict-int"] +extension-dict-xsyn = ["extensions", "liboliphaunt-wasix-portable/extension-dict-xsyn"] +extension-earthdistance = ["extensions", "liboliphaunt-wasix-portable/extension-earthdistance"] +extension-file-fdw = ["extensions", "liboliphaunt-wasix-portable/extension-file-fdw"] +extension-fuzzystrmatch = ["extensions", "liboliphaunt-wasix-portable/extension-fuzzystrmatch"] +extension-hstore = ["extensions", "liboliphaunt-wasix-portable/extension-hstore"] +extension-intarray = ["extensions", "liboliphaunt-wasix-portable/extension-intarray"] +extension-isn = ["extensions", "liboliphaunt-wasix-portable/extension-isn"] +extension-lo = ["extensions", "liboliphaunt-wasix-portable/extension-lo"] +extension-ltree = ["extensions", "liboliphaunt-wasix-portable/extension-ltree"] +extension-pageinspect = ["extensions", "liboliphaunt-wasix-portable/extension-pageinspect"] +extension-pg-buffercache = ["extensions", "liboliphaunt-wasix-portable/extension-pg-buffercache"] +extension-pg-freespacemap = ["extensions", "liboliphaunt-wasix-portable/extension-pg-freespacemap"] +extension-pg-hashids = ["extensions", "liboliphaunt-wasix-portable/extension-pg-hashids"] +extension-pg-ivm = ["extensions", "liboliphaunt-wasix-portable/extension-pg-ivm"] +extension-pg-surgery = ["extensions", "liboliphaunt-wasix-portable/extension-pg-surgery"] +extension-pg-textsearch = ["extensions", "liboliphaunt-wasix-portable/extension-pg-textsearch"] +extension-pg-trgm = ["extensions", "liboliphaunt-wasix-portable/extension-pg-trgm"] +extension-pg-uuidv7 = ["extensions", "liboliphaunt-wasix-portable/extension-pg-uuidv7"] +extension-pg-visibility = ["extensions", "liboliphaunt-wasix-portable/extension-pg-visibility"] +extension-pg-walinspect = ["extensions", "liboliphaunt-wasix-portable/extension-pg-walinspect"] +extension-pgcrypto = ["extensions", "liboliphaunt-wasix-portable/extension-pgcrypto"] +extension-pgtap = ["extensions", "liboliphaunt-wasix-portable/extension-pgtap"] +extension-postgis = ["extensions", "liboliphaunt-wasix-portable/extension-postgis"] +extension-seg = ["extensions", "liboliphaunt-wasix-portable/extension-seg"] +extension-tablefunc = ["extensions", "liboliphaunt-wasix-portable/extension-tablefunc"] +extension-tcn = ["extensions", "liboliphaunt-wasix-portable/extension-tcn"] +extension-tsm-system-rows = ["extensions", "liboliphaunt-wasix-portable/extension-tsm-system-rows"] +extension-tsm-system-time = ["extensions", "liboliphaunt-wasix-portable/extension-tsm-system-time"] +extension-unaccent = ["extensions", "liboliphaunt-wasix-portable/extension-unaccent"] +extension-uuid-ossp = ["extensions", "liboliphaunt-wasix-portable/extension-uuid-ossp"] +extension-vector = ["extensions", "liboliphaunt-wasix-portable/extension-vector"] icu = ["dep:oliphaunt-icu"] [package.metadata.oliphaunt-wasix.assets] @@ -89,7 +89,7 @@ hex = "0.4" sha2 = "0.10" dunce = "1" filetime = "0.2" -oliphaunt-wasix-assets = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/assets" } +liboliphaunt-wasix-portable = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/assets" } oliphaunt-icu = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/icu", optional = true } tokio = { version = "1", features = ["io-util", "rt-multi-thread"] } wasmer = { version = "7.2.0-alpha.3", default-features = false, features = [ @@ -109,16 +109,16 @@ wasmer-wasix = { version = "0.702.0-alpha.3", default-features = false, features webc = "=12.0.0" [target.'cfg(all(target_os = "macos", target_arch = "aarch64"))'.dependencies] -oliphaunt-wasix-aot-aarch64-apple-darwin = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin" } +liboliphaunt-wasix-aot-aarch64-apple-darwin = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin" } [target.'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))'.dependencies] -oliphaunt-wasix-aot-x86_64-unknown-linux-gnu = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu" } +liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu" } [target.'cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))'.dependencies] -oliphaunt-wasix-aot-aarch64-unknown-linux-gnu = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu" } +liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu" } [target.'cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))'.dependencies] -oliphaunt-wasix-aot-x86_64-pc-windows-msvc = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc" } +liboliphaunt-wasix-aot-x86_64-pc-windows-msvc = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc" } [dev-dependencies] sqlx = { version = "0.8", default-features = false, features = [ diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs index 88d310d7..73585274 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs @@ -462,10 +462,11 @@ fn target_aot_manifest() -> Result { ) } -fn merge_extension_aot_manifests(manifest: &mut AotManifest) -> Result<()> { +fn merge_extension_aot_manifests(_manifest: &mut AotManifest) -> Result<()> { #[cfg(feature = "extensions")] { - for sql_name in oliphaunt_wasix_assets::SELECTED_EXTENSION_SQL_NAMES { + let manifest = _manifest; + for sql_name in liboliphaunt_wasix_portable::SELECTED_EXTENSION_SQL_NAMES { let Some(json) = assets::extension_aot_manifest_json(target_triple(), sql_name) else { continue; }; @@ -693,10 +694,10 @@ fn target_aot_manifest_json() -> Option<&'static str> { target_aot_manifest_json_for_crate() } -fn extension_aot_artifact_bytes(name: &str) -> Option<&'static [u8]> { +fn extension_aot_artifact_bytes(_name: &str) -> Option<&'static [u8]> { #[cfg(feature = "extensions")] { - return assets::extension_aot_artifact_bytes(target_triple(), name); + return assets::extension_aot_artifact_bytes(target_triple(), _name); } #[allow(unreachable_code)] None @@ -704,58 +705,58 @@ fn extension_aot_artifact_bytes(name: &str) -> Option<&'static [u8]> { #[cfg(all(target_os = "macos", target_arch = "aarch64"))] fn target_aot_artifact_bytes(name: &str) -> Option<&'static [u8]> { - if !oliphaunt_wasix_aot_aarch64_apple_darwin::HAS_EMBEDDED_AOT { + if !liboliphaunt_wasix_aot_aarch64_apple_darwin::HAS_EMBEDDED_AOT { return None; } - oliphaunt_wasix_aot_aarch64_apple_darwin::artifact_bytes(name) + liboliphaunt_wasix_aot_aarch64_apple_darwin::artifact_bytes(name) } #[cfg(all(target_os = "macos", target_arch = "aarch64"))] fn target_aot_manifest_json_for_crate() -> Option<&'static str> { - oliphaunt_wasix_aot_aarch64_apple_darwin::HAS_EMBEDDED_AOT - .then_some(oliphaunt_wasix_aot_aarch64_apple_darwin::MANIFEST_JSON) + liboliphaunt_wasix_aot_aarch64_apple_darwin::HAS_EMBEDDED_AOT + .then_some(liboliphaunt_wasix_aot_aarch64_apple_darwin::MANIFEST_JSON) } #[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))] fn target_aot_artifact_bytes(name: &str) -> Option<&'static [u8]> { - if !oliphaunt_wasix_aot_x86_64_unknown_linux_gnu::HAS_EMBEDDED_AOT { + if !liboliphaunt_wasix_aot_x86_64_unknown_linux_gnu::HAS_EMBEDDED_AOT { return None; } - oliphaunt_wasix_aot_x86_64_unknown_linux_gnu::artifact_bytes(name) + liboliphaunt_wasix_aot_x86_64_unknown_linux_gnu::artifact_bytes(name) } #[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))] fn target_aot_manifest_json_for_crate() -> Option<&'static str> { - oliphaunt_wasix_aot_x86_64_unknown_linux_gnu::HAS_EMBEDDED_AOT - .then_some(oliphaunt_wasix_aot_x86_64_unknown_linux_gnu::MANIFEST_JSON) + liboliphaunt_wasix_aot_x86_64_unknown_linux_gnu::HAS_EMBEDDED_AOT + .then_some(liboliphaunt_wasix_aot_x86_64_unknown_linux_gnu::MANIFEST_JSON) } #[cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))] fn target_aot_artifact_bytes(name: &str) -> Option<&'static [u8]> { - if !oliphaunt_wasix_aot_aarch64_unknown_linux_gnu::HAS_EMBEDDED_AOT { + if !liboliphaunt_wasix_aot_aarch64_unknown_linux_gnu::HAS_EMBEDDED_AOT { return None; } - oliphaunt_wasix_aot_aarch64_unknown_linux_gnu::artifact_bytes(name) + liboliphaunt_wasix_aot_aarch64_unknown_linux_gnu::artifact_bytes(name) } #[cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))] fn target_aot_manifest_json_for_crate() -> Option<&'static str> { - oliphaunt_wasix_aot_aarch64_unknown_linux_gnu::HAS_EMBEDDED_AOT - .then_some(oliphaunt_wasix_aot_aarch64_unknown_linux_gnu::MANIFEST_JSON) + liboliphaunt_wasix_aot_aarch64_unknown_linux_gnu::HAS_EMBEDDED_AOT + .then_some(liboliphaunt_wasix_aot_aarch64_unknown_linux_gnu::MANIFEST_JSON) } #[cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))] fn target_aot_artifact_bytes(name: &str) -> Option<&'static [u8]> { - if !oliphaunt_wasix_aot_x86_64_pc_windows_msvc::HAS_EMBEDDED_AOT { + if !liboliphaunt_wasix_aot_x86_64_pc_windows_msvc::HAS_EMBEDDED_AOT { return None; } - oliphaunt_wasix_aot_x86_64_pc_windows_msvc::artifact_bytes(name) + liboliphaunt_wasix_aot_x86_64_pc_windows_msvc::artifact_bytes(name) } #[cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))] fn target_aot_manifest_json_for_crate() -> Option<&'static str> { - oliphaunt_wasix_aot_x86_64_pc_windows_msvc::HAS_EMBEDDED_AOT - .then_some(oliphaunt_wasix_aot_x86_64_pc_windows_msvc::MANIFEST_JSON) + liboliphaunt_wasix_aot_x86_64_pc_windows_msvc::HAS_EMBEDDED_AOT + .then_some(liboliphaunt_wasix_aot_x86_64_pc_windows_msvc::MANIFEST_JSON) } #[cfg(not(any( diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/assets.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/assets.rs index 89eeb432..d883bb59 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/assets.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/assets.rs @@ -14,7 +14,7 @@ pub struct AssetManifestMetadata { pub fn asset_manifest_metadata() -> Result { let manifest = - oliphaunt_wasix_assets::manifest().context("parse oliphaunt-wasix asset manifest")?; + liboliphaunt_wasix_portable::manifest().context("parse oliphaunt-wasix asset manifest")?; Ok(AssetManifestMetadata { source_lane: manifest.source_lane, source_fingerprint: manifest.source_fingerprint, @@ -35,31 +35,31 @@ pub fn asset_manifest_metadata() -> Result { } pub(crate) fn runtime_archive() -> Option<&'static [u8]> { - oliphaunt_wasix_assets::runtime_archive() + liboliphaunt_wasix_portable::runtime_archive() } pub(crate) fn expected_runtime_archive_sha256() -> Result { let manifest = - oliphaunt_wasix_assets::manifest().context("parse oliphaunt-wasix asset manifest")?; + liboliphaunt_wasix_portable::manifest().context("parse oliphaunt-wasix asset manifest")?; Ok(manifest.runtime.sha256) } pub(crate) fn pgdata_template_archive() -> Option<&'static [u8]> { - oliphaunt_wasix_assets::pgdata_template_archive() + liboliphaunt_wasix_portable::pgdata_template_archive() } pub(crate) fn pgdata_template_manifest() -> Option<&'static [u8]> { - oliphaunt_wasix_assets::pgdata_template_manifest() + liboliphaunt_wasix_portable::pgdata_template_manifest() } #[allow(dead_code)] pub(crate) fn pg_dump_wasm() -> Option<&'static [u8]> { - oliphaunt_wasix_assets::pg_dump_wasm() + liboliphaunt_wasix_portable::pg_dump_wasm() } #[allow(dead_code)] pub(crate) fn initdb_wasm() -> Option<&'static [u8]> { - oliphaunt_wasix_assets::initdb_wasm() + liboliphaunt_wasix_portable::initdb_wasm() } pub(crate) fn icu_data_archive() -> Option<&'static [u8]> { @@ -75,12 +75,12 @@ pub(crate) fn icu_data_archive() -> Option<&'static [u8]> { #[cfg(feature = "extensions")] pub(crate) fn extension_archive(sql_name: &str) -> Option<&'static [u8]> { - oliphaunt_wasix_assets::extension_archive(sql_name) + liboliphaunt_wasix_portable::extension_archive(sql_name) } #[cfg(feature = "extensions")] pub(crate) fn expected_extension_archive_sha256(sql_name: &str) -> Result { - oliphaunt_wasix_assets::expected_extension_archive_sha256(sql_name) + liboliphaunt_wasix_portable::expected_extension_archive_sha256(sql_name) .map(str::to_owned) .ok_or_else(|| { anyhow!("extension asset '{sql_name}' is not embedded in this oliphaunt-wasix build") @@ -89,10 +89,10 @@ pub(crate) fn expected_extension_archive_sha256(sql_name: &str) -> Result Option<&'static str> { - oliphaunt_wasix_assets::extension_aot_manifest_json(target, sql_name) + liboliphaunt_wasix_portable::extension_aot_manifest_json(target, sql_name) } #[cfg(feature = "extensions")] pub(crate) fn extension_aot_artifact_bytes(target: &str, name: &str) -> Option<&'static [u8]> { - oliphaunt_wasix_assets::extension_aot_artifact_bytes(target, name) + liboliphaunt_wasix_portable::extension_aot_artifact_bytes(target, name) } diff --git a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock index 1e68042b..f96a4c55 100644 --- a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock +++ b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock @@ -3535,11 +3535,11 @@ dependencies = [ "filetime", "flate2", "hex", - "oliphaunt-wasix-aot-aarch64-apple-darwin", - "oliphaunt-wasix-aot-aarch64-unknown-linux-gnu", - "oliphaunt-wasix-aot-x86_64-pc-windows-msvc", - "oliphaunt-wasix-aot-x86_64-unknown-linux-gnu", - "oliphaunt-wasix-assets", + "liboliphaunt-wasix-aot-aarch64-apple-darwin", + "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu", + "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc", + "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", + "liboliphaunt-wasix-portable", "regex", "serde", "serde_json", @@ -3557,23 +3557,23 @@ dependencies = [ ] [[package]] -name = "oliphaunt-wasix-aot-aarch64-apple-darwin" +name = "liboliphaunt-wasix-aot-aarch64-apple-darwin" version = "0.1.0" [[package]] -name = "oliphaunt-wasix-aot-aarch64-unknown-linux-gnu" +name = "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu" version = "0.1.0" [[package]] -name = "oliphaunt-wasix-aot-x86_64-pc-windows-msvc" +name = "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc" version = "0.1.0" [[package]] -name = "oliphaunt-wasix-aot-x86_64-unknown-linux-gnu" +name = "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu" version = "0.1.0" [[package]] -name = "oliphaunt-wasix-assets" +name = "liboliphaunt-wasix-portable" version = "0.1.0" dependencies = [ "serde", diff --git a/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/Cargo.toml b/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/Cargo.toml index 616d284c..9f70bee5 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/Cargo.toml +++ b/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "oliphaunt-wasix-aot-aarch64-apple-darwin" +name = "liboliphaunt-wasix-aot-aarch64-apple-darwin" version = "0.1.0" edition = "2024" rust-version = "1.93" diff --git a/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/README.md b/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/README.md index c187ecc1..f668a911 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/README.md +++ b/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/README.md @@ -1,4 +1,4 @@ -# oliphaunt-wasix-aot-aarch64-apple-darwin +# liboliphaunt-wasix-aot-aarch64-apple-darwin Internal target-specific Wasmer AOT artifact crate for `oliphaunt-wasix`. Do not depend on this crate directly. diff --git a/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/build.rs b/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/build.rs index 73f13fbb..f53b55d9 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/build.rs +++ b/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/build.rs @@ -14,8 +14,8 @@ fn main() { let target = env::var("CARGO_PKG_NAME") .expect("CARGO_PKG_NAME is set by Cargo") - .strip_prefix("oliphaunt-wasix-aot-") - .expect("AOT crate name starts with oliphaunt-wasix-aot-") + .strip_prefix("liboliphaunt-wasix-aot-") + .expect("AOT crate name starts with liboliphaunt-wasix-aot-") .to_owned(); emit_expected_artifact_inputs(&target); diff --git a/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/Cargo.toml b/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/Cargo.toml index 45238663..64f16d8a 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/Cargo.toml +++ b/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "oliphaunt-wasix-aot-aarch64-unknown-linux-gnu" +name = "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu" version = "0.1.0" edition = "2024" rust-version = "1.93" diff --git a/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/README.md b/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/README.md index 0b7cc227..b875d8c3 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/README.md +++ b/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/README.md @@ -1,4 +1,4 @@ -# oliphaunt-wasix-aot-aarch64-unknown-linux-gnu +# liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu Internal target-specific Wasmer AOT artifact crate for `oliphaunt-wasix`. Do not depend on this crate directly. diff --git a/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/build.rs b/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/build.rs index 73f13fbb..f53b55d9 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/build.rs +++ b/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/build.rs @@ -14,8 +14,8 @@ fn main() { let target = env::var("CARGO_PKG_NAME") .expect("CARGO_PKG_NAME is set by Cargo") - .strip_prefix("oliphaunt-wasix-aot-") - .expect("AOT crate name starts with oliphaunt-wasix-aot-") + .strip_prefix("liboliphaunt-wasix-aot-") + .expect("AOT crate name starts with liboliphaunt-wasix-aot-") .to_owned(); emit_expected_artifact_inputs(&target); diff --git a/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/Cargo.toml b/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/Cargo.toml index 1319c3b8..d8534a75 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/Cargo.toml +++ b/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "oliphaunt-wasix-aot-x86_64-pc-windows-msvc" +name = "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc" version = "0.1.0" edition = "2024" rust-version = "1.93" diff --git a/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/README.md b/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/README.md index ed2ee60c..5a34efd9 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/README.md +++ b/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/README.md @@ -1,4 +1,4 @@ -# oliphaunt-wasix-aot-x86_64-pc-windows-msvc +# liboliphaunt-wasix-aot-x86_64-pc-windows-msvc Internal target-specific Wasmer AOT artifact crate for `oliphaunt-wasix`. Do not depend on this crate directly. diff --git a/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/build.rs b/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/build.rs index 73f13fbb..f53b55d9 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/build.rs +++ b/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/build.rs @@ -14,8 +14,8 @@ fn main() { let target = env::var("CARGO_PKG_NAME") .expect("CARGO_PKG_NAME is set by Cargo") - .strip_prefix("oliphaunt-wasix-aot-") - .expect("AOT crate name starts with oliphaunt-wasix-aot-") + .strip_prefix("liboliphaunt-wasix-aot-") + .expect("AOT crate name starts with liboliphaunt-wasix-aot-") .to_owned(); emit_expected_artifact_inputs(&target); diff --git a/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/Cargo.toml b/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/Cargo.toml index 23a3dd86..fde81f39 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/Cargo.toml +++ b/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "oliphaunt-wasix-aot-x86_64-unknown-linux-gnu" +name = "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu" version = "0.1.0" edition = "2024" rust-version = "1.93" diff --git a/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/README.md b/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/README.md index 41e7d548..1838f842 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/README.md +++ b/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/README.md @@ -1,4 +1,4 @@ -# oliphaunt-wasix-aot-x86_64-unknown-linux-gnu +# liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu Internal target-specific Wasmer AOT artifact crate for `oliphaunt-wasix`. Do not depend on this crate directly. diff --git a/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/build.rs b/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/build.rs index 73f13fbb..f53b55d9 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/build.rs +++ b/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/build.rs @@ -14,8 +14,8 @@ fn main() { let target = env::var("CARGO_PKG_NAME") .expect("CARGO_PKG_NAME is set by Cargo") - .strip_prefix("oliphaunt-wasix-aot-") - .expect("AOT crate name starts with oliphaunt-wasix-aot-") + .strip_prefix("liboliphaunt-wasix-aot-") + .expect("AOT crate name starts with liboliphaunt-wasix-aot-") .to_owned(); emit_expected_artifact_inputs(&target); diff --git a/src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml b/src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml index 0bca7958..c4a540bf 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml +++ b/src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml @@ -1,12 +1,12 @@ [package] -name = "oliphaunt-wasix-assets" +name = "liboliphaunt-wasix-portable" version = "0.1.0" edition = "2024" rust-version = "1.93" description = "Internal Oliphaunt runtime and extension assets for oliphaunt-wasix" repository = "https://github.com/f0rr0/oliphaunt" homepage = "https://oliphaunt.dev" -documentation = "https://docs.rs/oliphaunt-wasix-assets" +documentation = "https://docs.rs/liboliphaunt-wasix-portable" license = "MIT AND Apache-2.0 AND PostgreSQL" publish = false links = "oliphaunt_artifact_liboliphaunt_wasix_runtime" diff --git a/src/runtimes/liboliphaunt/wasix/crates/assets/README.md b/src/runtimes/liboliphaunt/wasix/crates/assets/README.md index b044a745..a54678ef 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/assets/README.md +++ b/src/runtimes/liboliphaunt/wasix/crates/assets/README.md @@ -1,4 +1,4 @@ -# oliphaunt-wasix-assets +# liboliphaunt-wasix-portable Portable runtime artifact crate for `oliphaunt-wasix`. diff --git a/src/runtimes/liboliphaunt/wasix/release.toml b/src/runtimes/liboliphaunt/wasix/release.toml index dae72d66..38e916e9 100644 --- a/src/runtimes/liboliphaunt/wasix/release.toml +++ b/src/runtimes/liboliphaunt/wasix/release.toml @@ -4,11 +4,11 @@ kind = "wasm-runtime" publish_targets = ["github-release-assets", "crates-io"] registry_packages = [ "crates:oliphaunt-icu", - "crates:oliphaunt-wasix-assets", - "crates:oliphaunt-wasix-aot-aarch64-apple-darwin", - "crates:oliphaunt-wasix-aot-aarch64-unknown-linux-gnu", - "crates:oliphaunt-wasix-aot-x86_64-pc-windows-msvc", - "crates:oliphaunt-wasix-aot-x86_64-unknown-linux-gnu", + "crates:liboliphaunt-wasix-portable", + "crates:liboliphaunt-wasix-aot-aarch64-apple-darwin", + "crates:liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu", + "crates:liboliphaunt-wasix-aot-x86_64-pc-windows-msvc", + "crates:liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", ] release_artifacts = [ "release-assets", diff --git a/src/runtimes/liboliphaunt/wasix/tools/build-aot-target.sh b/src/runtimes/liboliphaunt/wasix/tools/build-aot-target.sh index 3f36e575..4c1d09e0 100755 --- a/src/runtimes/liboliphaunt/wasix/tools/build-aot-target.sh +++ b/src/runtimes/liboliphaunt/wasix/tools/build-aot-target.sh @@ -16,7 +16,7 @@ target="${AOT_TARGET:-${1:-}}" if [ -z "$target" ]; then target="$(rustc -vV | awk '/^host:/{print $2}')" fi -package="${AOT_PACKAGE:-oliphaunt-wasix-aot-${target}}" +package="${AOT_PACKAGE:-liboliphaunt-wasix-aot-${target}}" cargo run -p xtask -- assets aot --target-triple "$target" cargo run -p xtask -- assets package-aot --target-triple "$target" diff --git a/src/shared/fixtures/consumer-shape/products.json b/src/shared/fixtures/consumer-shape/products.json index c52600e9..7ffb454b 100644 --- a/src/shared/fixtures/consumer-shape/products.json +++ b/src/shared/fixtures/consumer-shape/products.json @@ -48,12 +48,12 @@ "src/runtimes/liboliphaunt/wasix/release.toml": [ "kind = \"wasm-runtime\"", "publish_targets = [\"github-release-assets\", \"crates-io\"]", - "\"crates:oliphaunt-wasix-assets\"", - "\"crates:oliphaunt-wasix-aot-x86_64-unknown-linux-gnu\"", + "\"crates:liboliphaunt-wasix-portable\"", + "\"crates:liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu\"", "\"release-assets\"" ], "src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml": [ - "name = \"oliphaunt-wasix-assets\"", + "name = \"liboliphaunt-wasix-portable\"", "links = \"oliphaunt_artifact_liboliphaunt_wasix_runtime\"" ], "tools/release/package_liboliphaunt_wasix_cargo_artifacts.py": [ @@ -879,8 +879,8 @@ "src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml": [ "default = []", "extensions = []", - "oliphaunt-wasix-assets", - "oliphaunt-wasix-aot-x86_64-unknown-linux-gnu" + "liboliphaunt-wasix-portable", + "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu" ] } } diff --git a/tools/policy/check-dependency-invariants.sh b/tools/policy/check-dependency-invariants.sh index e55d56b0..8d70210f 100755 --- a/tools/policy/check-dependency-invariants.sh +++ b/tools/policy/check-dependency-invariants.sh @@ -44,8 +44,8 @@ def dependency_path(spec): return None -def is_internal_payload_crate(name): - return name == "oliphaunt-wasix-assets" or name.startswith("oliphaunt-wasix-aot-") +def is_wasix_artifact_crate(name): + return name == "liboliphaunt-wasix-portable" or name.startswith("liboliphaunt-wasix-aot-") errors = [] @@ -53,7 +53,7 @@ product_deps = {} for table_name, deps in dependency_tables(product_manifest): for dep_key, spec in deps.items(): name = dependency_name(dep_key, spec) - if not is_internal_payload_crate(name): + if not is_wasix_artifact_crate(name): continue if name in product_deps: errors.append(f"{name} is declared more than once in oliphaunt-wasix dependencies") @@ -67,21 +67,31 @@ for manifest_path in internal_manifest_paths: package = manifest["package"] name = package["name"] version = package["version"] - if not is_internal_payload_crate(name): - errors.append(f"{manifest_path}: unexpected internal crate name {name!r}") + if not is_wasix_artifact_crate(name): + errors.append(f"{manifest_path}: unexpected WASIX artifact crate name {name!r}") continue if version != runtime_version: errors.append( f"{manifest_path}: {name} version {version} does not match liboliphaunt-wasix runtime version {runtime_version}" ) if package.get("publish") is not False: - errors.append(f"{manifest_path}: private payload crate {name} must declare publish = false") - -for name, (table_name, _spec) in sorted(product_deps.items()): - errors.append( - "src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml " - f"{table_name}.{name} must not depend on private runtime asset/AOT crates" - ) + errors.append(f"{manifest_path}: source artifact crate template {name} must declare publish = false") + if name not in product_deps: + errors.append(f"oliphaunt-wasix must depend on WASIX artifact crate {name}") + +for name, (table_name, spec) in sorted(product_deps.items()): + version = dependency_version(spec) + path = dependency_path(spec) + if version != f"={runtime_version}": + errors.append( + "src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml " + f"{table_name}.{name} must use exact liboliphaunt-wasix version ={runtime_version}, got {version!r}" + ) + if not path: + errors.append( + "src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml " + f"{table_name}.{name} must keep a source-checkout path dependency" + ) if errors: print("release version invariant violations:", file=sys.stderr) diff --git a/tools/policy/check-native-boundaries.sh b/tools/policy/check-native-boundaries.sh index 30f7d5c5..f4f5fe67 100755 --- a/tools/policy/check-native-boundaries.sh +++ b/tools/policy/check-native-boundaries.sh @@ -19,10 +19,10 @@ errors: list[str] = [] legacy_package_names = { "oliphaunt-wasix", - "oliphaunt-wasix-assets", + "liboliphaunt-wasix-portable", } legacy_name_prefixes = ( - "oliphaunt-wasix-aot-", + "liboliphaunt-wasix-aot-", ) legacy_runtime_names = { "wasmer", diff --git a/tools/release/artifact_target_matrix.py b/tools/release/artifact_target_matrix.py index 6ab64645..7bf61349 100755 --- a/tools/release/artifact_target_matrix.py +++ b/tools/release/artifact_target_matrix.py @@ -311,7 +311,7 @@ def liboliphaunt_wasix_aot_runtime_matrix(wasm_target: str = "all") -> dict[str, "os": target.runner, "target": target.triple, "target_id": target.target, - "package": f"oliphaunt-wasix-aot-{target.triple}", + "package": f"liboliphaunt-wasix-aot-{target.triple}", "artifact": f"liboliphaunt-wasix-runtime-aot-{target.target}", "llvm_url": target.llvm_url, } diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 942f99cb..4c9e9a3c 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -1368,7 +1368,7 @@ def check_wasm(findings: list[Finding]) -> None: runtime_version = product_metadata.read_current_version("liboliphaunt-wasix") dependencies = manifest.get("dependencies", {}) target_tables = manifest.get("target", {}) - expected_runtime_dependency = dependencies.get("oliphaunt-wasix-assets") + expected_runtime_dependency = dependencies.get("liboliphaunt-wasix-portable") require( findings, product, @@ -1376,14 +1376,14 @@ def check_wasm(findings: list[Finding]) -> None: isinstance(expected_runtime_dependency, dict) and expected_runtime_dependency.get("version") == f"={runtime_version}", "WASM crate must depend on the public portable runtime artifact crate at the liboliphaunt-wasix version.", - f"oliphaunt-wasix-assets dependency={expected_runtime_dependency!r}", + f"liboliphaunt-wasix-portable dependency={expected_runtime_dependency!r}", severity="P0", ) expected_aot_dependencies = { - 'cfg(all(target_os = "macos", target_arch = "aarch64"))': "oliphaunt-wasix-aot-aarch64-apple-darwin", - 'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))': "oliphaunt-wasix-aot-x86_64-unknown-linux-gnu", - 'cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))': "oliphaunt-wasix-aot-aarch64-unknown-linux-gnu", - 'cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))': "oliphaunt-wasix-aot-x86_64-pc-windows-msvc", + 'cfg(all(target_os = "macos", target_arch = "aarch64"))': "liboliphaunt-wasix-aot-aarch64-apple-darwin", + 'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))': "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", + 'cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))': "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu", + 'cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))': "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc", } missing_aot_dependencies = [] for cfg, crate in expected_aot_dependencies.items(): @@ -1485,7 +1485,7 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: findings, product, "wasix-assets-crate", - asset_package.get("name") == "oliphaunt-wasix-assets" + asset_package.get("name") == "liboliphaunt-wasix-portable" and asset_package.get("version") == product_metadata.read_current_version(product), "WASIX runtime asset crate must publish under the runtime product version.", f"src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml package={asset_package!r}", @@ -1503,11 +1503,11 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: registry_packages = set(product_registry_packages(product)) expected_registry_packages = { "crates:oliphaunt-icu", - "crates:oliphaunt-wasix-assets", - "crates:oliphaunt-wasix-aot-aarch64-apple-darwin", - "crates:oliphaunt-wasix-aot-aarch64-unknown-linux-gnu", - "crates:oliphaunt-wasix-aot-x86_64-pc-windows-msvc", - "crates:oliphaunt-wasix-aot-x86_64-unknown-linux-gnu", + "crates:liboliphaunt-wasix-portable", + "crates:liboliphaunt-wasix-aot-aarch64-apple-darwin", + "crates:liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu", + "crates:liboliphaunt-wasix-aot-x86_64-pc-windows-msvc", + "crates:liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", } require( findings, diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 309f21b6..b277c2c9 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1021,14 +1021,14 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None fail(f"{path} must use oliphaunt-wasix binding version {wasm_binding_version}") manifest = tomllib.loads(read_text("src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml")) dependencies = manifest.get("dependencies", {}) - runtime_dependency = dependencies.get("oliphaunt-wasix-assets") + runtime_dependency = dependencies.get("liboliphaunt-wasix-portable") if not isinstance(runtime_dependency, dict) or runtime_dependency.get("version") != f"={wasix_runtime_version}": - fail("oliphaunt-wasix must depend on oliphaunt-wasix-assets at the exact liboliphaunt-wasix runtime version") + fail("oliphaunt-wasix must depend on liboliphaunt-wasix-portable at the exact liboliphaunt-wasix runtime version") expected_aot_dependencies = { - 'cfg(all(target_os = "macos", target_arch = "aarch64"))': "oliphaunt-wasix-aot-aarch64-apple-darwin", - 'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))': "oliphaunt-wasix-aot-x86_64-unknown-linux-gnu", - 'cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))': "oliphaunt-wasix-aot-aarch64-unknown-linux-gnu", - 'cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))': "oliphaunt-wasix-aot-x86_64-pc-windows-msvc", + 'cfg(all(target_os = "macos", target_arch = "aarch64"))': "liboliphaunt-wasix-aot-aarch64-apple-darwin", + 'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))': "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", + 'cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))': "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu", + 'cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))': "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc", } target_tables = manifest.get("target", {}) for cfg, crate in expected_aot_dependencies.items(): @@ -1062,11 +1062,11 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None registry_packages = set(product_metadata.string_list(runtime_config, "registry_packages", "liboliphaunt-wasix")) expected_registry_packages = { "crates:oliphaunt-icu", - "crates:oliphaunt-wasix-assets", - "crates:oliphaunt-wasix-aot-aarch64-apple-darwin", - "crates:oliphaunt-wasix-aot-aarch64-unknown-linux-gnu", - "crates:oliphaunt-wasix-aot-x86_64-pc-windows-msvc", - "crates:oliphaunt-wasix-aot-x86_64-unknown-linux-gnu", + "crates:liboliphaunt-wasix-portable", + "crates:liboliphaunt-wasix-aot-aarch64-apple-darwin", + "crates:liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu", + "crates:liboliphaunt-wasix-aot-x86_64-pc-windows-msvc", + "crates:liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", } if registry_packages != expected_registry_packages: fail( diff --git a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py index 3f791a80..0ad5204b 100644 --- a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py +++ b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py @@ -23,14 +23,14 @@ PRODUCT = "liboliphaunt-wasix" SCHEMA = "oliphaunt-liboliphaunt-wasix-cargo-artifacts-v2" CRATES_IO_MAX_BYTES = 10 * 1024 * 1024 -RUNTIME_PACKAGE = "oliphaunt-wasix-assets" +RUNTIME_PACKAGE = "liboliphaunt-wasix-portable" ICU_PACKAGE = "oliphaunt-icu" ICU_PAYLOAD_ARCHIVE = "icu-data.tar.zst" AOT_PACKAGES = { - "macos-arm64": "oliphaunt-wasix-aot-aarch64-apple-darwin", - "linux-arm64-gnu": "oliphaunt-wasix-aot-aarch64-unknown-linux-gnu", - "linux-x64-gnu": "oliphaunt-wasix-aot-x86_64-unknown-linux-gnu", - "windows-x64-msvc": "oliphaunt-wasix-aot-x86_64-pc-windows-msvc", + "macos-arm64": "liboliphaunt-wasix-aot-aarch64-apple-darwin", + "linux-arm64-gnu": "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu", + "linux-x64-gnu": "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", + "windows-x64-msvc": "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc", } AOT_TARGET_TRIPLES = { "macos-arm64": "aarch64-apple-darwin", diff --git a/tools/release/release.py b/tools/release/release.py index e6f6c2be..87f74f50 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -498,6 +498,18 @@ def wait_for_cratesio_package(crate: str, version: str, *, retries: int = 12, re fail(f"crates.io did not report {crate} {version} after publish") +def verify_generated_cratesio_packages_published(product: str, crates: list[str], version: str) -> None: + generated_crates = sorted(set(crates)) + if not generated_crates: + fail(f"{product} generated no Cargo artifact crates to verify") + for crate in generated_crates: + wait_for_cratesio_package(crate, version) + print( + f"{product} generated Cargo artifact publication verified: " + + ", ".join(generated_crates) + ) + + def cargo_publish_package(package: str, version: str, *, allow_dirty: bool = False) -> None: if check_cratesio_publication.crate_version_exists(package, version): print(f"{package} {version} is already published on crates.io; skipping cargo publish.") @@ -2549,6 +2561,11 @@ def publish_liboliphaunt_cargo_artifacts(head_ref: str) -> None: for crate, _crate_path, manifest_path, role in packages: if role == "aggregator": cargo_publish_manifest(crate, version, manifest_path) + verify_generated_cratesio_packages_published( + "liboliphaunt-native", + [crate for crate, _crate_path, _manifest_path, _role in packages], + version, + ) run( [ "tools/release/check_registry_publication.py", @@ -2571,6 +2588,11 @@ def publish_liboliphaunt_wasix_cargo_artifacts(head_ref: str) -> None: packages = liboliphaunt_wasix_cargo_artifact_crates(version) for crate, _crate_path, manifest_path in packages: cargo_publish_manifest(crate, version, manifest_path) + verify_generated_cratesio_packages_published( + "liboliphaunt-wasix", + [crate for crate, _crate_path, _manifest_path in packages], + version, + ) run( [ "tools/release/check_registry_publication.py", diff --git a/tools/xtask/src/asset_checks.rs b/tools/xtask/src/asset_checks.rs index 5c4a5a69..08ecba18 100644 --- a/tools/xtask/src/asset_checks.rs +++ b/tools/xtask/src/asset_checks.rs @@ -648,28 +648,28 @@ fn aot_target_specs() -> &'static [AotTargetSpec] { triple: "aarch64-apple-darwin", target_id: "macos-arm64", runner_os: "macos-15", - package: "oliphaunt-wasix-aot-aarch64-apple-darwin", + package: "liboliphaunt-wasix-aot-aarch64-apple-darwin", llvm_url: "https://github.com/wasmerio/llvm-custom-builds/releases/download/22.x/llvm-darwin-aarch64.tar.xz", }, AotTargetSpec { triple: "x86_64-unknown-linux-gnu", target_id: "linux-x64-gnu", runner_os: "ubuntu-latest", - package: "oliphaunt-wasix-aot-x86_64-unknown-linux-gnu", + package: "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", llvm_url: "https://github.com/wasmerio/llvm-custom-builds/releases/download/22.x/llvm-linux-amd64.tar.xz", }, AotTargetSpec { triple: "aarch64-unknown-linux-gnu", target_id: "linux-arm64-gnu", runner_os: "ubuntu-24.04-arm", - package: "oliphaunt-wasix-aot-aarch64-unknown-linux-gnu", + package: "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu", llvm_url: "https://github.com/wasmerio/llvm-custom-builds/releases/download/22.x/llvm-linux-aarch64.tar.xz", }, AotTargetSpec { triple: "x86_64-pc-windows-msvc", target_id: "windows-x64-msvc", runner_os: "windows-latest", - package: "oliphaunt-wasix-aot-x86_64-pc-windows-msvc", + package: "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc", llvm_url: "https://github.com/wasmerio/llvm-custom-builds/releases/download/22.x/llvm-windows-amd64.tar.xz", }, ] @@ -730,7 +730,7 @@ pub(crate) fn print_supported_aot_targets() -> Result<()> { } pub(crate) fn print_internal_asset_packages() -> Result<()> { - println!("oliphaunt-wasix-assets"); + println!("liboliphaunt-wasix-portable"); for spec in aot_target_specs() { println!("{}", spec.package); } From 4247b8cc21dff6b14def9882da7b4634088c7bd3 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Thu, 25 Jun 2026 16:28:59 +0000 Subject: [PATCH 010/137] fix: optimize native runtime artifact payloads --- .../native/packages/darwin-arm64/package.json | 5 +- .../packages/linux-arm64-gnu/package.json | 5 +- .../packages/linux-x64-gnu/package.json | 5 +- .../packages/win32-x64-msvc/package.json | 5 +- src/sdks/js/src/runtime/server.ts | 8 +- src/sdks/rust/src/liboliphaunt/root.rs | 17 +- .../liboliphaunt/root/runtime/cache_key.rs | 13 +- .../src/liboliphaunt/root/runtime/install.rs | 7 +- .../rust/src/liboliphaunt/root/template.rs | 4 +- tools/release/check_artifact_targets.py | 7 +- tools/release/check_consumer_shape.py | 3 + .../check_liboliphaunt_release_assets.py | 90 ++++- tools/release/check_release_metadata.py | 8 +- .../optimize_native_runtime_payload.py | 356 ++++++++++++++++++ .../package-liboliphaunt-linux-assets.sh | 9 +- .../package-liboliphaunt-macos-assets.sh | 9 +- .../package-liboliphaunt-windows-assets.ps1 | 16 +- .../package_liboliphaunt_cargo_artifacts.py | 2 + tools/release/release.py | 9 +- 19 files changed, 536 insertions(+), 42 deletions(-) create mode 100644 tools/release/optimize_native_runtime_payload.py diff --git a/src/runtimes/liboliphaunt/native/packages/darwin-arm64/package.json b/src/runtimes/liboliphaunt/native/packages/darwin-arm64/package.json index e4aa0b53..e23753aa 100644 --- a/src/runtimes/liboliphaunt/native/packages/darwin-arm64/package.json +++ b/src/runtimes/liboliphaunt/native/packages/darwin-arm64/package.json @@ -26,7 +26,10 @@ "provenance": true, "executableFiles": [ "./runtime/bin/initdb", - "./runtime/bin/postgres" + "./runtime/bin/pg_ctl", + "./runtime/bin/pg_dump", + "./runtime/bin/postgres", + "./runtime/bin/psql" ] }, "files": [ diff --git a/src/runtimes/liboliphaunt/native/packages/linux-arm64-gnu/package.json b/src/runtimes/liboliphaunt/native/packages/linux-arm64-gnu/package.json index 3bbc6093..18f6a926 100644 --- a/src/runtimes/liboliphaunt/native/packages/linux-arm64-gnu/package.json +++ b/src/runtimes/liboliphaunt/native/packages/linux-arm64-gnu/package.json @@ -29,7 +29,10 @@ "provenance": true, "executableFiles": [ "./runtime/bin/initdb", - "./runtime/bin/postgres" + "./runtime/bin/pg_ctl", + "./runtime/bin/pg_dump", + "./runtime/bin/postgres", + "./runtime/bin/psql" ] }, "files": [ diff --git a/src/runtimes/liboliphaunt/native/packages/linux-x64-gnu/package.json b/src/runtimes/liboliphaunt/native/packages/linux-x64-gnu/package.json index 21807d1e..016ca1eb 100644 --- a/src/runtimes/liboliphaunt/native/packages/linux-x64-gnu/package.json +++ b/src/runtimes/liboliphaunt/native/packages/linux-x64-gnu/package.json @@ -29,7 +29,10 @@ "provenance": true, "executableFiles": [ "./runtime/bin/initdb", - "./runtime/bin/postgres" + "./runtime/bin/pg_ctl", + "./runtime/bin/pg_dump", + "./runtime/bin/postgres", + "./runtime/bin/psql" ] }, "files": [ diff --git a/src/runtimes/liboliphaunt/native/packages/win32-x64-msvc/package.json b/src/runtimes/liboliphaunt/native/packages/win32-x64-msvc/package.json index 0afa4ba2..e476f80c 100644 --- a/src/runtimes/liboliphaunt/native/packages/win32-x64-msvc/package.json +++ b/src/runtimes/liboliphaunt/native/packages/win32-x64-msvc/package.json @@ -26,7 +26,10 @@ "provenance": true, "executableFiles": [ "./runtime/bin/initdb.exe", - "./runtime/bin/postgres.exe" + "./runtime/bin/pg_ctl.exe", + "./runtime/bin/pg_dump.exe", + "./runtime/bin/postgres.exe", + "./runtime/bin/psql.exe" ] }, "files": [ diff --git a/src/sdks/js/src/runtime/server.ts b/src/sdks/js/src/runtime/server.ts index ce7016b2..e4835c7f 100644 --- a/src/sdks/js/src/runtime/server.ts +++ b/src/sdks/js/src/runtime/server.ts @@ -373,7 +373,7 @@ async function resolveServerExecutable(options: { process.env.OLIPHAUNT_POSTGRES, options.serverToolDirectory === undefined ? undefined - : join(options.serverToolDirectory, 'postgres'), + : join(options.serverToolDirectory, executableName('postgres')), ].filter((value): value is string => value !== undefined && value.length > 0); for (const candidate of candidates) { if (await isFile(candidate)) { @@ -390,10 +390,14 @@ async function optionalTool( if (directory === undefined) { return undefined; } - const path = join(directory, name); + const path = join(directory, executableName(name)); return (await isFile(path)) ? path : undefined; } +function executableName(name: string): string { + return process.platform === 'win32' ? `${name}.exe` : name; +} + async function isFile(path: string): Promise { try { return (await stat(path)).isFile(); diff --git a/src/sdks/rust/src/liboliphaunt/root.rs b/src/sdks/rust/src/liboliphaunt/root.rs index 38cb1007..bb1012fa 100644 --- a/src/sdks/rust/src/liboliphaunt/root.rs +++ b/src/sdks/rust/src/liboliphaunt/root.rs @@ -22,6 +22,8 @@ use crate::extension::Extension; use crate::storage::DatabaseRoot; static ACTIVE_ROOTS: OnceLock>> = OnceLock::new(); +pub(super) const NATIVE_RUNTIME_TOOLS: [&str; 5] = + ["postgres", "initdb", "pg_ctl", "pg_dump", "psql"]; pub(crate) struct MaterializedNativeResources { pub(crate) runtime_dir: PathBuf, @@ -79,7 +81,7 @@ impl PreparedNativeRoot { } pub(crate) fn tool_path(&self, tool_name: &str) -> PathBuf { - self.runtime_dir.join("bin").join(tool_name) + native_tool_path(&self.runtime_dir, tool_name) } pub(crate) fn refresh_manifest(&self) -> Result<()> { @@ -91,6 +93,19 @@ impl PreparedNativeRoot { } } +pub(super) fn native_tool_path(root: &Path, tool_name: &str) -> PathBuf { + root.join("bin") + .join(format!("{tool_name}{}", std::env::consts::EXE_SUFFIX)) +} + +pub(super) fn existing_native_tool_path(root: &Path, tool_name: &str) -> PathBuf { + let suffixed = native_tool_path(root, tool_name); + if suffixed.is_file() { + return suffixed; + } + root.join("bin").join(tool_name) +} + impl Drop for PreparedNativeRoot { fn drop(&mut self) { drop(self.lock.take()); diff --git a/src/sdks/rust/src/liboliphaunt/root/runtime/cache_key.rs b/src/sdks/rust/src/liboliphaunt/root/runtime/cache_key.rs index 081dad07..d4111791 100644 --- a/src/sdks/rust/src/liboliphaunt/root/runtime/cache_key.rs +++ b/src/sdks/rust/src/liboliphaunt/root/runtime/cache_key.rs @@ -12,6 +12,7 @@ use super::super::fingerprint::{ fingerprint_named_extension_sql_files, fingerprint_optional_file, hash_path, hash_str, new_state, }; +use super::super::{NATIVE_RUNTIME_TOOLS, existing_native_tool_path, native_tool_path}; use crate::error::{Error, Result}; use crate::extension::Extension; @@ -36,8 +37,12 @@ pub(super) fn runtime_cache_key( hash_str(&mut state, name); } - for tool in ["postgres", "initdb", "pg_ctl", "pg_dump", "psql"] { - fingerprint_optional_file(&mut state, install_dir, &install_dir.join("bin").join(tool))?; + for tool in NATIVE_RUNTIME_TOOLS { + fingerprint_optional_file( + &mut state, + install_dir, + &existing_native_tool_path(install_dir, tool), + )?; } let source_share = install_dir.join("share/postgresql"); @@ -107,8 +112,8 @@ pub(super) fn cached_runtime_is_valid( extensions: &[Extension], ) -> bool { if !cache_dir.join(".complete").is_file() - || !cache_dir.join("bin/postgres").is_file() - || !cache_dir.join("bin/initdb").is_file() + || !native_tool_path(cache_dir, "postgres").is_file() + || !native_tool_path(cache_dir, "initdb").is_file() || !cache_dir .join("share/postgresql/postgresql.conf.sample") .is_file() diff --git a/src/sdks/rust/src/liboliphaunt/root/runtime/install.rs b/src/sdks/rust/src/liboliphaunt/root/runtime/install.rs index 4c66a04a..e9bfc458 100644 --- a/src/sdks/rust/src/liboliphaunt/root/runtime/install.rs +++ b/src/sdks/rust/src/liboliphaunt/root/runtime/install.rs @@ -10,6 +10,7 @@ use super::super::extensions::{ use super::super::files::{ copy_directory_filtered, copy_file_preserving_permissions, remove_file_if_exists, }; +use super::super::{NATIVE_RUNTIME_TOOLS, existing_native_tool_path, native_tool_path}; use crate::error::{Error, Result}; use crate::extension::Extension; @@ -27,10 +28,10 @@ pub(super) fn install_cached_runtime( )) })?; - for tool in ["postgres", "initdb", "pg_ctl", "pg_dump", "psql"] { - let source = install_dir.join("bin").join(tool); + for tool in NATIVE_RUNTIME_TOOLS { + let source = existing_native_tool_path(install_dir, tool); if source.is_file() { - install_runtime_tool(&source, &runtime_dir.join("bin").join(tool))?; + install_runtime_tool(&source, &native_tool_path(runtime_dir, tool))?; } } diff --git a/src/sdks/rust/src/liboliphaunt/root/template.rs b/src/sdks/rust/src/liboliphaunt/root/template.rs index c39ff687..c4553a68 100644 --- a/src/sdks/rust/src/liboliphaunt/root/template.rs +++ b/src/sdks/rust/src/liboliphaunt/root/template.rs @@ -7,12 +7,12 @@ use std::process::{Command, Stdio}; use fs2::FileExt; -use super::NativeRuntimeProfile; use super::files::{ copy_directory_tree, directory_is_empty, pgdata_template_copy_mode, remove_file_if_exists, }; use super::fingerprint::{hash_path, hash_str, new_state}; use super::runtime::{materialize_runtime, monotonic_cache_nonce, runtime_cache_root}; +use super::{NativeRuntimeProfile, native_tool_path}; use crate::error::{Error, Result}; use crate::storage::BootstrapStrategy; @@ -190,7 +190,7 @@ fn pgdata_template_is_valid(template_dir: &Path, key: &str) -> bool { } fn run_template_initdb(runtime_dir: &Path, pgdata: &Path) -> Result<()> { - let initdb = runtime_dir.join("bin/initdb"); + let initdb = native_tool_path(runtime_dir, "initdb"); if !initdb.is_file() { return Err(Error::Engine(format!( "native PGDATA template bootstrap requires initdb at {}", diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py index 0b1df445..20ac9757 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -926,9 +926,14 @@ def validate_ci_release_artifacts() -> None: ) require_text( "tools/release/release.py", - '"package/runtime/bin/initdb"', + "required_runtime_member_paths", "liboliphaunt npm artifact packages must include the selected platform runtime tree", ) + require_text( + "tools/release/package_liboliphaunt_cargo_artifacts.py", + "optimize_native_runtime_payload.optimize_payload", + "liboliphaunt Cargo artifact packages must prune and validate native runtime payloads before splitting", + ) reject_text( ".github/workflows/release.yml", "target/release-assets/native", diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 4c9e9a3c..7f581347 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -433,6 +433,7 @@ def check_liboliphaunt(findings: list[Finding]) -> None: packaging_scripts = { "tools/release/package-liboliphaunt-macos-assets.sh": [ "oliphaunt_assert_base_runtime_has_no_optional_extensions", + "optimize_native_runtime_payload.py", "plpgsql.dylib", "$stage/lib/modules/", "liboliphaunt-${version}-${target_id}.tar.gz", @@ -440,6 +441,7 @@ def check_liboliphaunt(findings: list[Finding]) -> None: ], "tools/release/package-liboliphaunt-linux-assets.sh": [ "oliphaunt_assert_base_runtime_has_no_optional_extensions", + "optimize_native_runtime_payload.py", "plpgsql.so", "$stage/lib/modules/", "liboliphaunt-${version}-${target_id}.tar.gz", @@ -447,6 +449,7 @@ def check_liboliphaunt(findings: list[Finding]) -> None: ], "tools/release/package-liboliphaunt-windows-assets.ps1": [ "Assert-BaseRuntimeHasNoOptionalExtensions", + "optimize_native_runtime_payload.py", "plpgsql.dll", "lib/modules", 'Copy-Item -Recurse -Force (Join-Path $Runtime "*") (Join-Path $Stage "runtime")', diff --git a/tools/release/check_liboliphaunt_release_assets.py b/tools/release/check_liboliphaunt_release_assets.py index 5ee2baf2..da8cae02 100755 --- a/tools/release/check_liboliphaunt_release_assets.py +++ b/tools/release/check_liboliphaunt_release_assets.py @@ -7,13 +7,16 @@ import csv import hashlib import json +import shutil import sys import tarfile +import tempfile import zipfile -from pathlib import Path +from pathlib import Path, PurePosixPath from typing import NoReturn import artifact_targets +import optimize_native_runtime_payload import product_metadata @@ -138,6 +141,90 @@ def tar_text(path: Path, member_name: str) -> str: fail(f"{path} is not a readable tar archive: {error}") +def checked_archive_member(name: str, archive: Path) -> PurePosixPath: + path = PurePosixPath(name) + parts = tuple(part for part in path.parts if part not in {"", "."}) + if not parts: + return PurePosixPath(".") + if path.is_absolute() or any(part == ".." for part in parts): + fail(f"{archive} contains unsafe archive member {name!r}") + return PurePosixPath(*parts) + + +def extract_archive(path: Path, destination: Path) -> None: + shutil.rmtree(destination, ignore_errors=True) + destination.mkdir(parents=True, exist_ok=True) + if path.name.endswith(".zip"): + try: + with zipfile.ZipFile(path) as archive: + for info in archive.infolist(): + if info.is_dir() or info.filename.rstrip("/") in {"", ".", "./"}: + continue + member = checked_archive_member(info.filename, path) + output = destination.joinpath(*member.parts) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_bytes(archive.read(info.filename)) + mode = (info.external_attr >> 16) & 0o777 + if mode: + output.chmod(mode) + except zipfile.BadZipFile as error: + fail(f"{path} is not a readable zip archive: {error}") + return + + try: + with tarfile.open(path, "r:*") as archive: + for info in archive.getmembers(): + if info.isdir() or info.name.rstrip("/") in {"", ".", "./"}: + continue + if not info.isfile(): + fail(f"{path} member {info.name} must be a regular file") + member = checked_archive_member(info.name, path) + extracted = archive.extractfile(info) + if extracted is None: + fail(f"{path} member {info.name} could not be read") + output = destination.joinpath(*member.parts) + output.parent.mkdir(parents=True, exist_ok=True) + with extracted: + output.write_bytes(extracted.read()) + output.chmod(info.mode & 0o777) + except tarfile.TarError as error: + fail(f"{path} is not a readable tar archive: {error}") + + +def validate_native_target_artifact(path: Path, target: str, *, require_runtime: bool) -> None: + with tempfile.TemporaryDirectory(prefix=f"oliphaunt-native-{target}-") as temp: + extracted = Path(temp) / "payload" + extract_archive(path, extracted) + optimize_native_runtime_payload.validate_payload( + extracted, + target, + require_runtime=require_runtime, + ) + + +def validate_native_target_artifacts(asset_dir: Path, version: str) -> None: + runtime_targets = { + target.target + for target in artifact_targets.artifact_targets( + product="liboliphaunt-native", + kind="native-runtime", + surface="rust-native-direct", + published_only=True, + ) + } + for target in artifact_targets.artifact_targets( + product="liboliphaunt-native", + kind="native-runtime", + surface="github-release", + published_only=True, + ): + validate_native_target_artifact( + asset_dir / target.asset_name(version), + target.target, + require_runtime=target.target in runtime_targets, + ) + + def validate_base_runtime_artifact_contents( path: Path, extension_metadata: dict[str, dict[str, object]], @@ -533,6 +620,7 @@ def validate(asset_dir: Path) -> None: asset_dir / f"liboliphaunt-{version}-runtime-resources.tar.gz", metadata, ) + validate_native_target_artifacts(asset_dir, version) validate_icu_data_artifact_contents(asset_dir / f"liboliphaunt-{version}-icu-data.tar.gz") validate_package_size_report(asset_dir / f"liboliphaunt-{version}-package-size.tsv") validate_checksums(asset_dir, asset_dir / f"liboliphaunt-{version}-release-assets.sha256") diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index b277c2c9..4802677e 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -12,6 +12,7 @@ import artifact_targets import extension_artifact_targets +import optimize_native_runtime_payload import product_metadata @@ -145,10 +146,9 @@ def validate_platform_npm_packages( if metadata.get("runtimeRelativePath") != "runtime": fail(f"{target.npm_package} runtimeRelativePath must be runtime") files = ["bin", "runtime", "README.md"] if target.target == "windows-x64-msvc" else ["lib", "runtime", "README.md"] - executable_files = ( - ["./runtime/bin/initdb.exe", "./runtime/bin/postgres.exe"] - if target.target == "windows-x64-msvc" - else ["./runtime/bin/initdb", "./runtime/bin/postgres"] + executable_files = optimize_native_runtime_payload.required_runtime_member_paths( + target.target, + prefix="./runtime/bin", ) elif product == "oliphaunt-broker": if target.executable_relative_path is None: diff --git a/tools/release/optimize_native_runtime_payload.py b/tools/release/optimize_native_runtime_payload.py new file mode 100644 index 00000000..e2066ba1 --- /dev/null +++ b/tools/release/optimize_native_runtime_payload.py @@ -0,0 +1,356 @@ +#!/usr/bin/env python3 +"""Prune, strip, and validate liboliphaunt native runtime payloads.""" + +from __future__ import annotations + +import argparse +import os +import re +import shutil +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path, PurePosixPath +from typing import Literal, NoReturn + +import strip_native_release_binaries + + +ROOT = Path(__file__).resolve().parents[2] +NATIVE_RUNTIME_TOOL_STEMS = ("initdb", "pg_ctl", "pg_dump", "postgres", "psql") +ELF_DEBUG_SECTION = re.compile(r"\]\s+\.(debug_[^\s]+|symtab|strtab)\s") +MACHO_MAGICS = { + b"\xfe\xed\xfa\xce", + b"\xce\xfa\xed\xfe", + b"\xfe\xed\xfa\xcf", + b"\xcf\xfa\xed\xfe", + b"\xca\xfe\xba\xbe", + b"\xbe\xba\xfe\xca", +} +DEV_RUNTIME_DIRS = ( + PurePosixPath("include"), + PurePosixPath("lib/pkgconfig"), + PurePosixPath("lib/postgresql/pgxs"), +) +DEV_RUNTIME_SUFFIXES = (".a", ".la", ".pdb") +WINDOWS_DEV_RUNTIME_SUFFIXES = (".lib",) + + +@dataclass(frozen=True) +class NativeFile: + path: Path + kind: str + archive: bool = False + + +def fail(message: str) -> NoReturn: + print(f"optimize_native_runtime_payload.py: {message}", file=sys.stderr) + raise SystemExit(1) + + +def rel(path: Path) -> str: + try: + return path.relative_to(ROOT).as_posix() + except ValueError: + return str(path) + + +def read_prefix(path: Path, size: int = 8) -> bytes: + try: + with path.open("rb") as file: + return file.read(size) + except OSError as error: + fail(f"failed to read {path}: {error}") + + +def classify_native_file(path: Path) -> NativeFile | None: + prefix = read_prefix(path) + if prefix.startswith(b"\x7fELF"): + return NativeFile(path, "elf") + if prefix[:4] in MACHO_MAGICS: + return NativeFile(path, "macho") + if prefix.startswith(b"MZ"): + return NativeFile(path, "pe") + if prefix.startswith(b"!\n"): + return NativeFile(path, "archive", archive=True) + return None + + +def is_windows_target(target: str | None, runtime_dir: Path | None = None) -> bool: + if target is not None and target.startswith("windows-"): + return True + if runtime_dir is None: + return False + bin_dir = runtime_dir / "bin" + return any((bin_dir / f"{stem}.exe").exists() for stem in NATIVE_RUNTIME_TOOL_STEMS) + + +def required_runtime_tools(target: str | None, runtime_dir: Path | None = None) -> tuple[str, ...]: + if is_windows_target(target, runtime_dir): + return tuple(f"{stem}.exe" for stem in NATIVE_RUNTIME_TOOL_STEMS) + return NATIVE_RUNTIME_TOOL_STEMS + + +def required_runtime_member_paths(target: str | None, *, prefix: str) -> list[str]: + return [f"{prefix.rstrip('/')}/{tool}" for tool in required_runtime_tools(target)] + + +def runtime_dir_for(root: Path) -> Path | None: + for candidate in [ + root / "runtime", + root / "oliphaunt" / "runtime" / "files", + ]: + if candidate.is_dir(): + return candidate + if (root / "bin").is_dir() and ((root / "share").is_dir() or (root / "lib").is_dir()): + return root + return None + + +def remove_path(path: Path) -> None: + if path.is_dir(): + shutil.rmtree(path) + elif path.exists(): + path.unlink() + + +def prune_empty_dirs(root: Path) -> None: + if not root.is_dir(): + return + for path in sorted((item for item in root.rglob("*") if item.is_dir()), reverse=True): + try: + path.rmdir() + except OSError: + pass + + +def is_dev_runtime_file(relative: PurePosixPath, *, windows: bool) -> bool: + name = relative.name.lower() + if name.endswith(DEV_RUNTIME_SUFFIXES): + return True + if windows and name.endswith(WINDOWS_DEV_RUNTIME_SUFFIXES): + return True + return False + + +def prune_runtime_payload(root: Path, target: str | None = None) -> None: + runtime_dir = runtime_dir_for(root) + if runtime_dir is None: + return + + windows = is_windows_target(target, runtime_dir) + required_tools = set(required_runtime_tools(target, runtime_dir)) + bin_dir = runtime_dir / "bin" + if bin_dir.is_dir(): + for path in sorted(bin_dir.iterdir()): + name = path.name + if windows: + if name.lower().endswith(".exe") and name not in required_tools: + remove_path(path) + elif name not in required_tools: + remove_path(path) + + for relative in DEV_RUNTIME_DIRS: + remove_path(runtime_dir.joinpath(*relative.parts)) + + for path in sorted(runtime_dir.rglob("*"), reverse=True): + if path.is_dir() and path.name.endswith(".dSYM"): + remove_path(path) + continue + if not path.is_file(): + continue + relative = PurePosixPath(path.relative_to(runtime_dir).as_posix()) + if is_dev_runtime_file(relative, windows=windows): + remove_path(path) + + prune_empty_dirs(runtime_dir) + + +def strip_supported_for_target(target: str | None) -> bool: + if target is None: + return True + if target.startswith(("linux-", "android-")): + return sys.platform.startswith("linux") + if target.startswith(("macos-", "ios-")): + return sys.platform == "darwin" + if target.startswith("windows-"): + return bool( + os.environ.get("OLIPHAUNT_PE_STRIP") + or os.environ.get("OLIPHAUNT_STRIP") + or shutil.which("llvm-strip") + or sys.platform == "win32" + ) + return True + + +def strip_payload(root: Path) -> None: + result = strip_native_release_binaries.main([str(root)]) + if result != 0: + fail(f"failed to strip native payload under {rel(root)}") + + +def iter_files(root: Path) -> list[Path]: + return sorted(path for path in root.rglob("*") if path.is_file()) + + +def file_output(path: Path) -> str | None: + file_tool = shutil.which("file") + if file_tool is None: + return None + result = subprocess.run( + [file_tool, str(path)], + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + if result.returncode != 0: + return None + return result.stdout + + +def elf_debug_errors(path: Path) -> list[str]: + readelf = shutil.which("readelf") + if readelf is not None: + result = subprocess.run( + [readelf, "-S", str(path)], + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + if result.returncode != 0: + return [f"{rel(path)} could not be inspected with readelf: {result.stderr.strip()}"] + sections = sorted({match.group(1) for match in ELF_DEBUG_SECTION.finditer(result.stdout)}) + return [f"{rel(path)} contains unstripped ELF section .{section}" for section in sections] + + output = file_output(path) + if output is not None and ("not stripped" in output or "with debug_info" in output): + return [f"{rel(path)} appears to contain unstripped ELF debug/symbol data"] + return [] + + +def validate_native_files(root: Path) -> list[str]: + errors: list[str] = [] + for path in iter_files(root): + native = classify_native_file(path) + if native is None: + continue + if native.kind == "elf" and not native.archive: + errors.extend(elf_debug_errors(path)) + return errors + + +def validate_runtime_tree(root: Path, target: str | None, require_runtime: bool) -> list[str]: + errors: list[str] = [] + runtime_dir = runtime_dir_for(root) + if runtime_dir is None: + if require_runtime: + errors.append(f"{rel(root)} is missing a runtime tree") + return errors + + windows = is_windows_target(target, runtime_dir) + required_tools = set(required_runtime_tools(target, runtime_dir)) + bin_dir = runtime_dir / "bin" + if require_runtime and not bin_dir.is_dir(): + errors.append(f"{rel(runtime_dir)} is missing bin") + if bin_dir.is_dir(): + for tool in sorted(required_tools): + path = bin_dir / tool + if not path.is_file(): + errors.append(f"{rel(runtime_dir)} is missing required runtime tool bin/{tool}") + continue + if not windows and not os.access(path, os.X_OK): + errors.append(f"{rel(path)} must be executable") + for path in sorted(bin_dir.iterdir()): + if windows: + if path.name.lower().endswith(".exe") and path.name not in required_tools: + errors.append(f"{rel(path)} is an extra Windows runtime executable") + elif path.name not in required_tools: + errors.append(f"{rel(path)} is an extra runtime tool") + + for relative in DEV_RUNTIME_DIRS: + path = runtime_dir.joinpath(*relative.parts) + if path.exists(): + errors.append(f"{rel(path)} is a development-only runtime path") + + for path in sorted(runtime_dir.rglob("*")): + if path.is_dir() and path.name.endswith(".dSYM"): + errors.append(f"{rel(path)} is a development-only debug symbol bundle") + continue + if not path.is_file(): + continue + relative = PurePosixPath(path.relative_to(runtime_dir).as_posix()) + if is_dev_runtime_file(relative, windows=windows): + errors.append(f"{rel(path)} is a development-only runtime file") + + return errors + + +def validate_payload( + root: Path, + target: str | None = None, + *, + require_runtime: bool = True, +) -> None: + errors = [ + *validate_runtime_tree(root, target, require_runtime=require_runtime), + *validate_native_files(root), + ] + if errors: + for error in errors: + print(error, file=sys.stderr) + fail(f"{rel(root)} is not an optimized native runtime payload") + + +def optimize_payload( + root: Path, + target: str | None = None, + *, + strip: bool | Literal["auto"] = "auto", + require_runtime: bool = True, +) -> None: + prune_runtime_payload(root, target) + should_strip = strip is True or (strip == "auto" and strip_supported_for_target(target)) + if should_strip: + strip_payload(root) + validate_payload(root, target, require_runtime=require_runtime) + + +def parse_args(argv: list[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("root", type=Path) + parser.add_argument("--target", default=None) + parser.add_argument("--check", action="store_true", help="validate without mutating the payload") + parser.add_argument( + "--no-strip", + action="store_true", + help="prune but skip native binary stripping before validation", + ) + parser.add_argument( + "--allow-missing-runtime", + action="store_true", + help="validate native files even when the archive is a library-only mobile payload", + ) + return parser.parse_args(argv) + + +def main(argv: list[str]) -> int: + args = parse_args(argv) + root = args.root.resolve() + if not root.exists(): + fail(f"payload root does not exist: {root}") + if args.check: + validate_payload(root, args.target, require_runtime=not args.allow_missing_runtime) + return 0 + optimize_payload( + root, + args.target, + strip=False if args.no_strip else "auto", + require_runtime=not args.allow_missing_runtime, + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/tools/release/package-liboliphaunt-linux-assets.sh b/tools/release/package-liboliphaunt-linux-assets.sh index 0296a231..23595ad9 100755 --- a/tools/release/package-liboliphaunt-linux-assets.sh +++ b/tools/release/package-liboliphaunt-linux-assets.sh @@ -61,8 +61,9 @@ src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh >/tmp/liboliphaun [ -f "$lib" ] || fail "missing Linux liboliphaunt shared library at $lib" [ -f "$embedded_modules/plpgsql.so" ] || fail "missing Linux embedded plpgsql module at $embedded_modules/plpgsql.so" -[ -x "$runtime/bin/initdb" ] || fail "missing Linux initdb at $runtime/bin/initdb" -[ -x "$runtime/bin/postgres" ] || fail "missing Linux postgres at $runtime/bin/postgres" +for tool in initdb pg_ctl pg_dump postgres psql; do + [ -x "$runtime/bin/$tool" ] || fail "missing Linux $tool at $runtime/bin/$tool" +done echo "==> Verifying base liboliphaunt $target_id runtime is extension-clean" cargo run -p oliphaunt --bin oliphaunt-resources --locked -- --list-extensions >"$catalog_file" @@ -74,8 +75,8 @@ cp "$lib" "$stage/lib/" rsync -a --delete "$embedded_modules/" "$stage/lib/modules/" rsync -a --delete --exclude 'share/icu/***' "$runtime/" "$stage/runtime/" -echo "==> Stripping staged liboliphaunt $target_id release binaries" -python3 tools/release/strip_native_release_binaries.py "$stage" +echo "==> Optimizing staged liboliphaunt $target_id release payload" +python3 tools/release/optimize_native_runtime_payload.py "$stage" --target "$target_id" echo "==> Smoke testing staged liboliphaunt $target_id release layout" env \ diff --git a/tools/release/package-liboliphaunt-macos-assets.sh b/tools/release/package-liboliphaunt-macos-assets.sh index c1a20282..81d3e5d8 100755 --- a/tools/release/package-liboliphaunt-macos-assets.sh +++ b/tools/release/package-liboliphaunt-macos-assets.sh @@ -53,8 +53,9 @@ OLIPHAUNT_BUILD_EXTENSIONS="${OLIPHAUNT_BUILD_EXTENSIONS:-0}" \ [ -f "$lib" ] || fail "missing macOS liboliphaunt dylib at $lib" [ -f "$embedded_modules/plpgsql.dylib" ] || fail "missing macOS embedded plpgsql module at $embedded_modules/plpgsql.dylib" -[ -x "$runtime/bin/initdb" ] || fail "missing macOS initdb at $runtime/bin/initdb" -[ -x "$runtime/bin/postgres" ] || fail "missing macOS postgres at $runtime/bin/postgres" +for tool in initdb pg_ctl pg_dump postgres psql; do + [ -x "$runtime/bin/$tool" ] || fail "missing macOS $tool at $runtime/bin/$tool" +done echo "==> Verifying base liboliphaunt $target_id runtime is extension-clean" cargo run -p oliphaunt --bin oliphaunt-resources --locked -- --list-extensions >"$catalog_file" @@ -66,8 +67,8 @@ cp "$lib" "$stage/lib/" rsync -a --delete "$embedded_modules/" "$stage/lib/modules/" rsync -a --delete --exclude 'share/icu/***' "$runtime/" "$stage/runtime/" -echo "==> Stripping staged liboliphaunt $target_id release binaries" -python3 tools/release/strip_native_release_binaries.py "$stage" +echo "==> Optimizing staged liboliphaunt $target_id release payload" +python3 tools/release/optimize_native_runtime_payload.py "$stage" --target "$target_id" echo "==> Smoke testing staged liboliphaunt $target_id release layout" env \ diff --git a/tools/release/package-liboliphaunt-windows-assets.ps1 b/tools/release/package-liboliphaunt-windows-assets.ps1 index 94faedad..b01cd2af 100644 --- a/tools/release/package-liboliphaunt-windows-assets.ps1 +++ b/tools/release/package-liboliphaunt-windows-assets.ps1 @@ -113,11 +113,11 @@ if (-not (Test-Path $ImportLib)) { if (-not (Test-Path (Join-Path $EmbeddedModules "plpgsql.dll"))) { Fail "missing Windows embedded plpgsql module at $(Join-Path $EmbeddedModules "plpgsql.dll")" } -if (-not (Test-Path (Join-Path $Runtime "bin/initdb.exe"))) { - Fail "missing Windows initdb at $(Join-Path $Runtime "bin/initdb.exe")" -} -if (-not (Test-Path (Join-Path $Runtime "bin/postgres.exe"))) { - Fail "missing Windows postgres at $(Join-Path $Runtime "bin/postgres.exe")" +foreach ($Tool in @("initdb.exe", "pg_ctl.exe", "pg_dump.exe", "postgres.exe", "psql.exe")) { + $ToolPath = Join-Path (Join-Path $Runtime "bin") $Tool + if (-not (Test-Path $ToolPath)) { + Fail "missing Windows $Tool at $ToolPath" + } } Write-Output "==> Verifying base liboliphaunt $TargetId runtime is extension-clean" @@ -137,10 +137,10 @@ if (Test-Path $StagedIcu) { Remove-Item -Recurse -Force $StagedIcu } -Write-Output "==> Stripping staged liboliphaunt $TargetId release binaries" -python tools/release/strip_native_release_binaries.py $Stage +Write-Output "==> Optimizing staged liboliphaunt $TargetId release payload" +python tools/release/optimize_native_runtime_payload.py $Stage --target $TargetId if ($LASTEXITCODE -ne 0) { - Fail "failed to strip staged Windows liboliphaunt release binaries" + Fail "failed to optimize staged Windows liboliphaunt release payload" } Write-Output "==> Smoke testing staged liboliphaunt $TargetId release layout" diff --git a/tools/release/package_liboliphaunt_cargo_artifacts.py b/tools/release/package_liboliphaunt_cargo_artifacts.py index 43207044..f69e7f8e 100644 --- a/tools/release/package_liboliphaunt_cargo_artifacts.py +++ b/tools/release/package_liboliphaunt_cargo_artifacts.py @@ -17,6 +17,7 @@ from typing import NoReturn import artifact_targets +import optimize_native_runtime_payload import product_metadata @@ -601,6 +602,7 @@ def package_target( fail(f"missing liboliphaunt native release asset: {rel(archive)}") extracted_root = source_root / f"{target.target}-extracted" extract_archive(archive, extracted_root) + optimize_native_runtime_payload.optimize_payload(extracted_root, target.target) part_dirs = build_part_crates( extracted_root, source_root, diff --git a/tools/release/release.py b/tools/release/release.py index 87f74f50..a7e96510 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -19,6 +19,7 @@ import artifact_targets import check_cratesio_publication import extension_artifact_targets +import optimize_native_runtime_payload import package_broker_cargo_artifacts import package_liboliphaunt_cargo_artifacts import package_liboliphaunt_wasix_cargo_artifacts @@ -2194,6 +2195,7 @@ def stage_liboliphaunt_npm_payloads(version: str) -> dict[str, Path]: stage / target.library_relative_path, ) extract_tar_tree(archive, "runtime", stage / "runtime") + optimize_native_runtime_payload.optimize_payload(stage, target.target) stages[package_name] = stage return stages @@ -2288,10 +2290,9 @@ def liboliphaunt_npm_tarballs(version: str) -> list[tuple[str, Path]]: ): if target.library_relative_path is None: fail(f"{target.id} must declare library_relative_path for npm artifact package publication") - runtime_members = ( - ["package/runtime/bin/initdb.exe", "package/runtime/bin/postgres.exe"] - if target.target == "windows-x64-msvc" - else ["package/runtime/bin/initdb", "package/runtime/bin/postgres"] + runtime_members = optimize_native_runtime_payload.required_runtime_member_paths( + target.target, + prefix="package/runtime/bin", ) required_members = [f"package/{target.library_relative_path}", *runtime_members] package_dir = stages[package_name] From 9f8a39ceadc4bd786cb4bb5c320e06881f82faf3 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Thu, 25 Jun 2026 18:31:30 +0000 Subject: [PATCH 011/137] fix: split runtime tools into artifact crates --- Cargo.lock | 60 +++- Cargo.toml | 5 + release-please-config.json | 25 ++ .../crates/oliphaunt-wasix/Cargo.toml | 6 + .../oliphaunt-wasix/src/oliphaunt/aot.rs | 127 +++++++- .../oliphaunt-wasix/src/oliphaunt/assets.rs | 7 +- src/runtimes/liboliphaunt/native/release.toml | 4 + .../wasix/assets/build/docker_psql.sh | 100 ++++++ .../crates/aot/aarch64-apple-darwin/build.rs | 12 +- .../aot/aarch64-unknown-linux-gnu/build.rs | 12 +- .../aot/x86_64-pc-windows-msvc/build.rs | 12 +- .../aot/x86_64-unknown-linux-gnu/build.rs | 12 +- .../liboliphaunt/wasix/crates/assets/build.rs | 8 +- .../wasix/crates/assets/src/lib.rs | 2 + .../tools-aot/aarch64-apple-darwin/Cargo.toml | 18 ++ .../tools-aot/aarch64-apple-darwin/README.md | 4 + .../tools-aot/aarch64-apple-darwin/build.rs | 289 ++++++++++++++++++ .../tools-aot/aarch64-apple-darwin/src/lib.rs | 3 + .../aarch64-unknown-linux-gnu/Cargo.toml | 18 ++ .../aarch64-unknown-linux-gnu/README.md | 4 + .../aarch64-unknown-linux-gnu/build.rs | 289 ++++++++++++++++++ .../aarch64-unknown-linux-gnu/src/lib.rs | 3 + .../x86_64-pc-windows-msvc/Cargo.toml | 18 ++ .../x86_64-pc-windows-msvc/README.md | 4 + .../tools-aot/x86_64-pc-windows-msvc/build.rs | 289 ++++++++++++++++++ .../x86_64-pc-windows-msvc/src/lib.rs | 3 + .../x86_64-unknown-linux-gnu/Cargo.toml | 18 ++ .../x86_64-unknown-linux-gnu/README.md | 4 + .../x86_64-unknown-linux-gnu/build.rs | 289 ++++++++++++++++++ .../x86_64-unknown-linux-gnu/src/lib.rs | 3 + .../wasix/crates/tools/Cargo.toml | 25 ++ .../liboliphaunt/wasix/crates/tools/README.md | 5 + .../liboliphaunt/wasix/crates/tools/build.rs | 169 ++++++++++ .../wasix/crates/tools/src/lib.rs | 3 + src/runtimes/liboliphaunt/wasix/release.toml | 5 + .../rust/crates/oliphaunt-build/src/lib.rs | 118 ++++++- src/sdks/rust/src/liboliphaunt/root.rs | 4 +- .../rust/src/liboliphaunt/root/runtime.rs | 7 +- .../liboliphaunt/root/runtime/cache_key.rs | 30 +- .../src/liboliphaunt/root/runtime/install.rs | 58 +++- .../src/liboliphaunt/root/runtime/locate.rs | 44 ++- src/sdks/rust/tools/check-sdk.sh | 3 + tools/release/check_consumer_shape.py | 46 ++- tools/release/check_release_metadata.py | 26 +- .../optimize_native_runtime_payload.py | 28 +- .../package_liboliphaunt_cargo_artifacts.py | 222 +++++++++++--- ...kage_liboliphaunt_wasix_cargo_artifacts.py | 164 +++++++++- tools/release/release.py | 60 +++- tools/xtask/src/asset_checks.rs | 12 +- tools/xtask/src/asset_manifest.rs | 8 +- tools/xtask/src/asset_pipeline.rs | 60 +++- tools/xtask/src/postgres_guard.rs | 11 +- 52 files changed, 2626 insertions(+), 130 deletions(-) create mode 100755 src/runtimes/liboliphaunt/wasix/assets/build/docker_psql.sh create mode 100644 src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/Cargo.toml create mode 100644 src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/README.md create mode 100644 src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/build.rs create mode 100644 src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/src/lib.rs create mode 100644 src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/Cargo.toml create mode 100644 src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/README.md create mode 100644 src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/build.rs create mode 100644 src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/src/lib.rs create mode 100644 src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/Cargo.toml create mode 100644 src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/README.md create mode 100644 src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/build.rs create mode 100644 src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/src/lib.rs create mode 100644 src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/Cargo.toml create mode 100644 src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/README.md create mode 100644 src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/build.rs create mode 100644 src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/src/lib.rs create mode 100644 src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml create mode 100644 src/runtimes/liboliphaunt/wasix/crates/tools/README.md create mode 100644 src/runtimes/liboliphaunt/wasix/crates/tools/build.rs create mode 100644 src/runtimes/liboliphaunt/wasix/crates/tools/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 0733b7df..b1989e6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1854,6 +1854,47 @@ dependencies = [ "windows-link", ] +[[package]] +name = "liboliphaunt-wasix-aot-aarch64-apple-darwin" +version = "0.1.0" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu" +version = "0.1.0" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc" +version = "0.1.0" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu" +version = "0.1.0" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "liboliphaunt-wasix-portable" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "sha2 0.10.9", +] + [[package]] name = "libredox" version = "0.1.17" @@ -2302,12 +2343,17 @@ dependencies = [ "filetime", "flate2", "hex", - "oliphaunt-icu", "liboliphaunt-wasix-aot-aarch64-apple-darwin", "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu", "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc", "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", "liboliphaunt-wasix-portable", + "oliphaunt-icu", + "oliphaunt-wasix-tools", + "oliphaunt-wasix-tools-aot-aarch64-apple-darwin", + "oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu", + "oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc", + "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu", "regex", "serde", "serde_json", @@ -2327,15 +2373,14 @@ dependencies = [ ] [[package]] -name = "liboliphaunt-wasix-aot-aarch64-apple-darwin" +name = "oliphaunt-wasix-tools" version = "0.1.0" dependencies = [ - "serde_json", "sha2 0.10.9", ] [[package]] -name = "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu" +name = "oliphaunt-wasix-tools-aot-aarch64-apple-darwin" version = "0.1.0" dependencies = [ "serde_json", @@ -2343,7 +2388,7 @@ dependencies = [ ] [[package]] -name = "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc" +name = "oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu" version = "0.1.0" dependencies = [ "serde_json", @@ -2351,7 +2396,7 @@ dependencies = [ ] [[package]] -name = "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu" +name = "oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc" version = "0.1.0" dependencies = [ "serde_json", @@ -2359,10 +2404,9 @@ dependencies = [ ] [[package]] -name = "liboliphaunt-wasix-portable" +name = "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu" version = "0.1.0" dependencies = [ - "serde", "serde_json", "sha2 0.10.9", ] diff --git a/Cargo.toml b/Cargo.toml index 7bcf5e70..7034eef4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,10 +6,15 @@ members = [ "src/runtimes/broker", "src/runtimes/liboliphaunt/icu", "src/runtimes/liboliphaunt/wasix/crates/assets", + "src/runtimes/liboliphaunt/wasix/crates/tools", "src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin", "src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu", "src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu", "src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc", + "src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin", + "src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu", + "src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu", + "src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc", "tools/perf/runner", "tools/xtask", ] diff --git a/release-please-config.json b/release-please-config.json index 147c7509..77a8dcbe 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -462,25 +462,50 @@ "path": "crates/assets/Cargo.toml", "jsonpath": "$.package.version" }, + { + "type": "toml", + "path": "crates/tools/Cargo.toml", + "jsonpath": "$.package.version" + }, { "type": "toml", "path": "crates/aot/aarch64-apple-darwin/Cargo.toml", "jsonpath": "$.package.version" }, + { + "type": "toml", + "path": "crates/tools-aot/aarch64-apple-darwin/Cargo.toml", + "jsonpath": "$.package.version" + }, { "type": "toml", "path": "crates/aot/aarch64-unknown-linux-gnu/Cargo.toml", "jsonpath": "$.package.version" }, + { + "type": "toml", + "path": "crates/tools-aot/aarch64-unknown-linux-gnu/Cargo.toml", + "jsonpath": "$.package.version" + }, { "type": "toml", "path": "crates/aot/x86_64-pc-windows-msvc/Cargo.toml", "jsonpath": "$.package.version" }, + { + "type": "toml", + "path": "crates/tools-aot/x86_64-pc-windows-msvc/Cargo.toml", + "jsonpath": "$.package.version" + }, { "type": "toml", "path": "crates/aot/x86_64-unknown-linux-gnu/Cargo.toml", "jsonpath": "$.package.version" + }, + { + "type": "toml", + "path": "crates/tools-aot/x86_64-unknown-linux-gnu/Cargo.toml", + "jsonpath": "$.package.version" } ] }, diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml b/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml index 1d5f4359..1ec14a38 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml @@ -71,6 +71,7 @@ runtime-archive-sha256 = "810a238bbb430b24b9a606bcdf9c2346270d729530f24e5c61772f oliphaunt-wasix-sha256 = "d6438a0dd57c13cd160d6f58de3c5549f5b94c8d99d834ebed63ade841716f72" pgdata-template-archive-sha256 = "c525b376a9667fdc7b7beb74d902ab56da5b017a4571e5ab62cd1b1bb4c0d65a" pg-dump-wasix-sha256 = "19579204268759917a3efafa81ae1de7f2e67c7e0f4de11ea8aa03f948bf15bd" +psql-wasix-sha256 = "0000000000000000000000000000000000000000000000000000000000000000" initdb-wasix-sha256 = "91cfb13243c371d4937d4e6fca513aaa82a33dfde42be17f04ad64c4cb75e6e1" [dependencies] @@ -90,6 +91,7 @@ sha2 = "0.10" dunce = "1" filetime = "0.2" liboliphaunt-wasix-portable = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/assets" } +oliphaunt-wasix-tools = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/tools" } oliphaunt-icu = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/icu", optional = true } tokio = { version = "1", features = ["io-util", "rt-multi-thread"] } wasmer = { version = "7.2.0-alpha.3", default-features = false, features = [ @@ -110,15 +112,19 @@ webc = "=12.0.0" [target.'cfg(all(target_os = "macos", target_arch = "aarch64"))'.dependencies] liboliphaunt-wasix-aot-aarch64-apple-darwin = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin" } +oliphaunt-wasix-tools-aot-aarch64-apple-darwin = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin" } [target.'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))'.dependencies] liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu" } +oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu" } [target.'cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))'.dependencies] liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu" } +oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu" } [target.'cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))'.dependencies] liboliphaunt-wasix-aot-x86_64-pc-windows-msvc = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc" } +oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc" } [dev-dependencies] sqlx = { version = "0.8", default-features = false, features = [ diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs index 73585274..4b62dbb4 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs @@ -453,6 +453,7 @@ fn target_aot_manifest() -> Result { if let Some(json) = target_aot_manifest_json() { let mut manifest: AotManifest = serde_json::from_str(json).context("parse package-manager-resolved AOT manifest")?; + merge_tools_aot_manifest(&mut manifest)?; merge_extension_aot_manifests(&mut manifest)?; return Ok(manifest); } @@ -462,6 +463,48 @@ fn target_aot_manifest() -> Result { ) } +fn merge_tools_aot_manifest(manifest: &mut AotManifest) -> Result<()> { + let Some(json) = target_tools_aot_manifest_json() else { + return Ok(()); + }; + let tools_manifest: AotManifest = + serde_json::from_str(json).context("parse package-manager-resolved tools AOT manifest")?; + ensure!( + tools_manifest.target_triple == manifest.target_triple, + "tools AOT manifest target mismatch: manifest={} core={}", + tools_manifest.target_triple, + manifest.target_triple + ); + ensure!( + tools_manifest.engine == manifest.engine, + "tools AOT manifest engine mismatch: manifest={} core={}", + tools_manifest.engine, + manifest.engine + ); + ensure!( + tools_manifest.wasmer_version == manifest.wasmer_version, + "tools AOT manifest Wasmer version mismatch: manifest={} core={}", + tools_manifest.wasmer_version, + manifest.wasmer_version + ); + ensure!( + tools_manifest.wasmer_wasix_version == manifest.wasmer_wasix_version, + "tools AOT manifest wasmer-wasix version mismatch: manifest={} core={}", + tools_manifest.wasmer_wasix_version, + manifest.wasmer_wasix_version + ); + ensure!( + tools_manifest.source_fingerprint == manifest.source_fingerprint, + "tools AOT manifest source fingerprint mismatch" + ); + ensure!( + tools_manifest.postgres_version == manifest.postgres_version, + "tools AOT manifest postgres version mismatch" + ); + manifest.artifacts.extend(tools_manifest.artifacts); + Ok(()) +} + fn merge_extension_aot_manifests(_manifest: &mut AotManifest) -> Result<()> { #[cfg(feature = "extensions")] { @@ -687,13 +730,19 @@ fn target_triple() -> &'static str { } fn target_artifact_bytes(name: &str) -> Option<&'static [u8]> { - target_aot_artifact_bytes(name).or_else(|| extension_aot_artifact_bytes(name)) + target_aot_artifact_bytes(name) + .or_else(|| target_tools_aot_artifact_bytes(name)) + .or_else(|| extension_aot_artifact_bytes(name)) } fn target_aot_manifest_json() -> Option<&'static str> { target_aot_manifest_json_for_crate() } +fn target_tools_aot_manifest_json() -> Option<&'static str> { + target_tools_aot_manifest_json_for_crate() +} + fn extension_aot_artifact_bytes(_name: &str) -> Option<&'static [u8]> { #[cfg(feature = "extensions")] { @@ -717,6 +766,20 @@ fn target_aot_manifest_json_for_crate() -> Option<&'static str> { .then_some(liboliphaunt_wasix_aot_aarch64_apple_darwin::MANIFEST_JSON) } +#[cfg(all(target_os = "macos", target_arch = "aarch64"))] +fn target_tools_aot_artifact_bytes(name: &str) -> Option<&'static [u8]> { + if !oliphaunt_wasix_tools_aot_aarch64_apple_darwin::HAS_EMBEDDED_AOT { + return None; + } + oliphaunt_wasix_tools_aot_aarch64_apple_darwin::artifact_bytes(name) +} + +#[cfg(all(target_os = "macos", target_arch = "aarch64"))] +fn target_tools_aot_manifest_json_for_crate() -> Option<&'static str> { + oliphaunt_wasix_tools_aot_aarch64_apple_darwin::HAS_EMBEDDED_AOT + .then_some(oliphaunt_wasix_tools_aot_aarch64_apple_darwin::MANIFEST_JSON) +} + #[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))] fn target_aot_artifact_bytes(name: &str) -> Option<&'static [u8]> { if !liboliphaunt_wasix_aot_x86_64_unknown_linux_gnu::HAS_EMBEDDED_AOT { @@ -731,6 +794,20 @@ fn target_aot_manifest_json_for_crate() -> Option<&'static str> { .then_some(liboliphaunt_wasix_aot_x86_64_unknown_linux_gnu::MANIFEST_JSON) } +#[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))] +fn target_tools_aot_artifact_bytes(name: &str) -> Option<&'static [u8]> { + if !oliphaunt_wasix_tools_aot_x86_64_unknown_linux_gnu::HAS_EMBEDDED_AOT { + return None; + } + oliphaunt_wasix_tools_aot_x86_64_unknown_linux_gnu::artifact_bytes(name) +} + +#[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))] +fn target_tools_aot_manifest_json_for_crate() -> Option<&'static str> { + oliphaunt_wasix_tools_aot_x86_64_unknown_linux_gnu::HAS_EMBEDDED_AOT + .then_some(oliphaunt_wasix_tools_aot_x86_64_unknown_linux_gnu::MANIFEST_JSON) +} + #[cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))] fn target_aot_artifact_bytes(name: &str) -> Option<&'static [u8]> { if !liboliphaunt_wasix_aot_aarch64_unknown_linux_gnu::HAS_EMBEDDED_AOT { @@ -745,6 +822,20 @@ fn target_aot_manifest_json_for_crate() -> Option<&'static str> { .then_some(liboliphaunt_wasix_aot_aarch64_unknown_linux_gnu::MANIFEST_JSON) } +#[cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))] +fn target_tools_aot_artifact_bytes(name: &str) -> Option<&'static [u8]> { + if !oliphaunt_wasix_tools_aot_aarch64_unknown_linux_gnu::HAS_EMBEDDED_AOT { + return None; + } + oliphaunt_wasix_tools_aot_aarch64_unknown_linux_gnu::artifact_bytes(name) +} + +#[cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))] +fn target_tools_aot_manifest_json_for_crate() -> Option<&'static str> { + oliphaunt_wasix_tools_aot_aarch64_unknown_linux_gnu::HAS_EMBEDDED_AOT + .then_some(oliphaunt_wasix_tools_aot_aarch64_unknown_linux_gnu::MANIFEST_JSON) +} + #[cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))] fn target_aot_artifact_bytes(name: &str) -> Option<&'static [u8]> { if !liboliphaunt_wasix_aot_x86_64_pc_windows_msvc::HAS_EMBEDDED_AOT { @@ -759,6 +850,20 @@ fn target_aot_manifest_json_for_crate() -> Option<&'static str> { .then_some(liboliphaunt_wasix_aot_x86_64_pc_windows_msvc::MANIFEST_JSON) } +#[cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))] +fn target_tools_aot_artifact_bytes(name: &str) -> Option<&'static [u8]> { + if !oliphaunt_wasix_tools_aot_x86_64_pc_windows_msvc::HAS_EMBEDDED_AOT { + return None; + } + oliphaunt_wasix_tools_aot_x86_64_pc_windows_msvc::artifact_bytes(name) +} + +#[cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))] +fn target_tools_aot_manifest_json_for_crate() -> Option<&'static str> { + oliphaunt_wasix_tools_aot_x86_64_pc_windows_msvc::HAS_EMBEDDED_AOT + .then_some(oliphaunt_wasix_tools_aot_x86_64_pc_windows_msvc::MANIFEST_JSON) +} + #[cfg(not(any( all(target_os = "macos", target_arch = "aarch64"), all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"), @@ -769,6 +874,16 @@ fn target_aot_artifact_bytes(_name: &str) -> Option<&'static [u8]> { None } +#[cfg(not(any( + all(target_os = "macos", target_arch = "aarch64"), + all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"), + all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"), + all(target_os = "windows", target_arch = "x86_64", target_env = "msvc") +)))] +fn target_tools_aot_artifact_bytes(_name: &str) -> Option<&'static [u8]> { + None +} + #[cfg(not(any( all(target_os = "macos", target_arch = "aarch64"), all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"), @@ -779,6 +894,16 @@ fn target_aot_manifest_json_for_crate() -> Option<&'static str> { None } +#[cfg(not(any( + all(target_os = "macos", target_arch = "aarch64"), + all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"), + all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"), + all(target_os = "windows", target_arch = "x86_64", target_env = "msvc") +)))] +fn target_tools_aot_manifest_json_for_crate() -> Option<&'static str> { + None +} + #[derive(Debug, Deserialize)] #[serde(rename_all = "kebab-case")] struct AotManifest { diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/assets.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/assets.rs index d883bb59..42917fac 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/assets.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/assets.rs @@ -54,7 +54,12 @@ pub(crate) fn pgdata_template_manifest() -> Option<&'static [u8]> { #[allow(dead_code)] pub(crate) fn pg_dump_wasm() -> Option<&'static [u8]> { - liboliphaunt_wasix_portable::pg_dump_wasm() + oliphaunt_wasix_tools::pg_dump_wasm() +} + +#[allow(dead_code)] +pub(crate) fn psql_wasm() -> Option<&'static [u8]> { + oliphaunt_wasix_tools::psql_wasm() } #[allow(dead_code)] diff --git a/src/runtimes/liboliphaunt/native/release.toml b/src/runtimes/liboliphaunt/native/release.toml index 2b3a8c8b..b2744667 100644 --- a/src/runtimes/liboliphaunt/native/release.toml +++ b/src/runtimes/liboliphaunt/native/release.toml @@ -7,6 +7,10 @@ registry_packages = [ "crates:liboliphaunt-native-linux-x64-gnu", "crates:liboliphaunt-native-macos-arm64", "crates:liboliphaunt-native-windows-x64-msvc", + "crates:oliphaunt-tools-linux-arm64-gnu", + "crates:oliphaunt-tools-linux-x64-gnu", + "crates:oliphaunt-tools-macos-arm64", + "crates:oliphaunt-tools-windows-x64-msvc", "npm:@oliphaunt/icu", "npm:@oliphaunt/liboliphaunt-darwin-arm64", "npm:@oliphaunt/liboliphaunt-linux-x64-gnu", diff --git a/src/runtimes/liboliphaunt/wasix/assets/build/docker_psql.sh b/src/runtimes/liboliphaunt/wasix/assets/build/docker_psql.sh new file mode 100755 index 00000000..f604357d --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix/assets/build/docker_psql.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$ROOT/wasix_third_party.sh" +REPO_ROOT="$(oliphaunt_wasix_repo_root "$ROOT")" +. "$ROOT/source_lane.sh" +SOURCE_LANE="$(oliphaunt_wasix_source_lane)" + +IMAGE="${IMAGE:-oliphaunt-wasix-wasix-build:local}" +JOBS="${JOBS:-4}" +CONTAINER_ROOT="${CONTAINER_ROOT:-/work/src/runtimes/liboliphaunt/wasix/assets/build}" +CONTAINER_GENERATED_ROOT="${CONTAINER_GENERATED_ROOT:-/work/target/oliphaunt-wasix/wasix-build}" +CONTAINER_BUILD_DIR="${CONTAINER_BUILD_DIR:-$(oliphaunt_wasix_default_build_dir "$SOURCE_LANE")}" +CONTAINER_PGSRC="${CONTAINER_PGSRC:-$(oliphaunt_wasix_prepare_source_for_docker "$SOURCE_LANE")}" +DOCKER="${DOCKER:-$(command -v docker 2>/dev/null || true)}" +if [ -z "$DOCKER" ] && [ -x /usr/local/bin/docker ]; then + DOCKER=/usr/local/bin/docker +fi +if [ -z "$DOCKER" ] && [ -x /opt/homebrew/bin/docker ]; then + DOCKER=/opt/homebrew/bin/docker +fi +if [ -z "$DOCKER" ]; then + echo "docker CLI not found; set DOCKER=/path/to/docker" >&2 + exit 127 +fi +export PATH="$(dirname "$DOCKER"):$PATH" +DOCKER_USER_ARGS=() +if [ "${OLIPHAUNT_WASM_DOCKER_AS_ROOT:-0}" != "1" ]; then + DOCKER_USER_ARGS=(--user "$(id -u):$(id -g)" -e HOME=/tmp) +fi + +if [ "${OLIPHAUNT_WASM_SKIP_IMAGE_BUILD:-0}" = "1" ]; then + "$DOCKER" image inspect "$IMAGE" >/dev/null 2>&1 || { + echo "WASIX build image is missing: $IMAGE" >&2 + exit 1 + } + echo "reusing Docker image $IMAGE" +elif [ "${FORCE_IMAGE_BUILD:-0}" = "1" ] || ! "$DOCKER" image inspect "$IMAGE" >/dev/null 2>&1; then + "$DOCKER" build \ + -t "$IMAGE" \ + -f "$ROOT/docker/Dockerfile" \ + "$ROOT/docker" +else + echo "reusing Docker image $IMAGE" +fi + +"$DOCKER" run --rm \ + "${DOCKER_USER_ARGS[@]}" \ + --cpus="$JOBS" \ + -e CONTAINER_ROOT="$CONTAINER_ROOT" \ + -e CONTAINER_GENERATED_ROOT="$CONTAINER_GENERATED_ROOT" \ + -e BUILD_DIR="$CONTAINER_BUILD_DIR" \ + -e PGSRC="$CONTAINER_PGSRC" \ + -e OLIPHAUNT_WASM_SOURCE_LANE="$SOURCE_LANE" \ + -e JOBS="$JOBS" \ + -e OLIPHAUNT_WASM_BUILD_PROFILE="${OLIPHAUNT_WASM_BUILD_PROFILE:-release}" \ + -e OLIPHAUNT_WASM_WASIX_COPT="${OLIPHAUNT_WASM_WASIX_COPT:-}" \ + -e OLIPHAUNT_WASM_WASIX_LOPT="${OLIPHAUNT_WASM_WASIX_LOPT:-}" \ + -e OLIPHAUNT_WASM_WASIX_CONFIGURE_WASM_OPT="${OLIPHAUNT_WASM_WASIX_CONFIGURE_WASM_OPT:-no}" \ + -e OLIPHAUNT_WASM_WASIX_BUILD_WASM_OPT="${OLIPHAUNT_WASM_WASIX_BUILD_WASM_OPT:-yes}" \ + -e OLIPHAUNT_WASM_WASM_OPT_FLAGS="${OLIPHAUNT_WASM_WASM_OPT_FLAGS-}" \ + -e OLIPHAUNT_WASM_WASM_OPT_SUPPRESS_DEFAULT="${OLIPHAUNT_WASM_WASM_OPT_SUPPRESS_DEFAULT-}" \ + -e OLIPHAUNT_WASM_WASM_OPT_PRESERVE_UNOPTIMIZED="${OLIPHAUNT_WASM_WASM_OPT_PRESERVE_UNOPTIMIZED-}" \ + -e OLIPHAUNT_WASM_WASIX_COMPILER_FLAGS="${OLIPHAUNT_WASM_WASIX_COMPILER_FLAGS:-}" \ + -e OLIPHAUNT_WASM_WASIX_LINKER_FLAGS="${OLIPHAUNT_WASM_WASIX_LINKER_FLAGS:-}" \ + -e OLIPHAUNT_WASM_WASIX_BACKEND_TIMING="${OLIPHAUNT_WASM_WASIX_BACKEND_TIMING:-0}" \ + -e WASIX_HOME=/opt/wasixcc-home/.wasixcc \ + -v "$REPO_ROOT:/work" \ + -w /work \ + "$IMAGE" \ + bash -lc ' + set -euo pipefail + . ./src/runtimes/liboliphaunt/wasix/assets/build/docker_wasix_env.sh + . ./src/runtimes/liboliphaunt/wasix/assets/build/profile_flags.sh + . ./src/runtimes/liboliphaunt/wasix/assets/build/source_lane.sh + . ./src/runtimes/liboliphaunt/wasix/assets/build/wasix_icu_link.sh + icu_prefix="$(./src/runtimes/liboliphaunt/wasix/assets/build/build_wasix_icu.sh)" + ICU_CFLAGS="$(oliphaunt_wasix_icu_cflags "$icu_prefix")" + ICU_LIBS="$(oliphaunt_wasix_icu_libs "$icu_prefix")" + oliphaunt_wasix_apply_wasix_profile build + export AR=wasixar + export RANLIB=wasixranlib + export NM=wasixnm + export LLVM_NM=wasixnm + + test -f "$BUILD_DIR/config.status" + oliphaunt_wasix_check_source_markers + sha256sum -c "$BUILD_DIR/.oliphaunt-wasix-bridge-sha256" >/dev/null + test "$(oliphaunt_wasix_wasix_profile_signature)" = "$(cat "$BUILD_DIR/.oliphaunt-wasix-build-profile")" + make -s -C "$BUILD_DIR/src/bin/psql" clean + make -s -C "$BUILD_DIR/src/bin/psql" psql \ + libpq="$BUILD_DIR/src/interfaces/libpq/libpq.a" \ + LIBS="$BUILD_DIR/src/common/libpgcommon.a $BUILD_DIR/src/port/libpgport.a $ICU_LIBS -lm" + test -f "$BUILD_DIR/src/bin/psql/psql" + if wasixnm -u "$BUILD_DIR/src/bin/psql/psql" | grep -E " PQ[A-Za-z0-9_]+$"; then + echo "psql still imports libpq symbols; expected standalone WASIX psql" >&2 + exit 1 + fi + ' diff --git a/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/build.rs b/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/build.rs index f53b55d9..a3d208ad 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/build.rs +++ b/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/build.rs @@ -134,7 +134,7 @@ fn write_generated_aot(out: &Path, target: &str, artifact_dir: &Path) { continue; }; let artifact_name = artifact_name_from_file_stem(stem); - if artifact_name.starts_with("extension:") { + if !artifact_belongs_to_crate(&artifact_name) { continue; } cases.push_str(&format!( @@ -190,6 +190,7 @@ fn artifact_name_from_file_stem(stem: &str) -> String { match stem { "oliphaunt" => "runtime:oliphaunt".to_owned(), "pg_dump" => "tool:pg_dump".to_owned(), + "psql" => "tool:psql".to_owned(), "initdb" => "tool:initdb".to_owned(), "plpgsql" => "runtime-support:plpgsql".to_owned(), "dict_snowball" => "runtime-support:dict_snowball".to_owned(), @@ -205,6 +206,13 @@ fn rust_string_literal(path: &Path) -> String { format!("{:?}", path.to_string_lossy()) } +fn artifact_belongs_to_crate(name: &str) -> bool { + match ARTIFACT_KIND { + "wasix-tools-aot" => matches!(name, "tool:pg_dump" | "tool:psql"), + _ => !name.starts_with("extension:") && !matches!(name, "tool:pg_dump" | "tool:psql"), + } +} + fn write_core_aot_manifest(source: &Path, destination: &Path) -> Vec { let text = fs::read_to_string(source).expect("read generated WASIX AOT manifest"); let mut manifest: serde_json::Value = @@ -221,7 +229,7 @@ fn write_core_aot_manifest(source: &Path, destination: &Path) -> Vec { .and_then(|value| value.as_str()) .expect("AOT artifact has name") .to_owned(); - if name.starts_with("extension:") { + if !artifact_belongs_to_crate(&name) { continue; } let path = artifact diff --git a/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/build.rs b/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/build.rs index f53b55d9..a3d208ad 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/build.rs +++ b/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/build.rs @@ -134,7 +134,7 @@ fn write_generated_aot(out: &Path, target: &str, artifact_dir: &Path) { continue; }; let artifact_name = artifact_name_from_file_stem(stem); - if artifact_name.starts_with("extension:") { + if !artifact_belongs_to_crate(&artifact_name) { continue; } cases.push_str(&format!( @@ -190,6 +190,7 @@ fn artifact_name_from_file_stem(stem: &str) -> String { match stem { "oliphaunt" => "runtime:oliphaunt".to_owned(), "pg_dump" => "tool:pg_dump".to_owned(), + "psql" => "tool:psql".to_owned(), "initdb" => "tool:initdb".to_owned(), "plpgsql" => "runtime-support:plpgsql".to_owned(), "dict_snowball" => "runtime-support:dict_snowball".to_owned(), @@ -205,6 +206,13 @@ fn rust_string_literal(path: &Path) -> String { format!("{:?}", path.to_string_lossy()) } +fn artifact_belongs_to_crate(name: &str) -> bool { + match ARTIFACT_KIND { + "wasix-tools-aot" => matches!(name, "tool:pg_dump" | "tool:psql"), + _ => !name.starts_with("extension:") && !matches!(name, "tool:pg_dump" | "tool:psql"), + } +} + fn write_core_aot_manifest(source: &Path, destination: &Path) -> Vec { let text = fs::read_to_string(source).expect("read generated WASIX AOT manifest"); let mut manifest: serde_json::Value = @@ -221,7 +229,7 @@ fn write_core_aot_manifest(source: &Path, destination: &Path) -> Vec { .and_then(|value| value.as_str()) .expect("AOT artifact has name") .to_owned(); - if name.starts_with("extension:") { + if !artifact_belongs_to_crate(&name) { continue; } let path = artifact diff --git a/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/build.rs b/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/build.rs index f53b55d9..a3d208ad 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/build.rs +++ b/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/build.rs @@ -134,7 +134,7 @@ fn write_generated_aot(out: &Path, target: &str, artifact_dir: &Path) { continue; }; let artifact_name = artifact_name_from_file_stem(stem); - if artifact_name.starts_with("extension:") { + if !artifact_belongs_to_crate(&artifact_name) { continue; } cases.push_str(&format!( @@ -190,6 +190,7 @@ fn artifact_name_from_file_stem(stem: &str) -> String { match stem { "oliphaunt" => "runtime:oliphaunt".to_owned(), "pg_dump" => "tool:pg_dump".to_owned(), + "psql" => "tool:psql".to_owned(), "initdb" => "tool:initdb".to_owned(), "plpgsql" => "runtime-support:plpgsql".to_owned(), "dict_snowball" => "runtime-support:dict_snowball".to_owned(), @@ -205,6 +206,13 @@ fn rust_string_literal(path: &Path) -> String { format!("{:?}", path.to_string_lossy()) } +fn artifact_belongs_to_crate(name: &str) -> bool { + match ARTIFACT_KIND { + "wasix-tools-aot" => matches!(name, "tool:pg_dump" | "tool:psql"), + _ => !name.starts_with("extension:") && !matches!(name, "tool:pg_dump" | "tool:psql"), + } +} + fn write_core_aot_manifest(source: &Path, destination: &Path) -> Vec { let text = fs::read_to_string(source).expect("read generated WASIX AOT manifest"); let mut manifest: serde_json::Value = @@ -221,7 +229,7 @@ fn write_core_aot_manifest(source: &Path, destination: &Path) -> Vec { .and_then(|value| value.as_str()) .expect("AOT artifact has name") .to_owned(); - if name.starts_with("extension:") { + if !artifact_belongs_to_crate(&name) { continue; } let path = artifact diff --git a/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/build.rs b/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/build.rs index f53b55d9..a3d208ad 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/build.rs +++ b/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/build.rs @@ -134,7 +134,7 @@ fn write_generated_aot(out: &Path, target: &str, artifact_dir: &Path) { continue; }; let artifact_name = artifact_name_from_file_stem(stem); - if artifact_name.starts_with("extension:") { + if !artifact_belongs_to_crate(&artifact_name) { continue; } cases.push_str(&format!( @@ -190,6 +190,7 @@ fn artifact_name_from_file_stem(stem: &str) -> String { match stem { "oliphaunt" => "runtime:oliphaunt".to_owned(), "pg_dump" => "tool:pg_dump".to_owned(), + "psql" => "tool:psql".to_owned(), "initdb" => "tool:initdb".to_owned(), "plpgsql" => "runtime-support:plpgsql".to_owned(), "dict_snowball" => "runtime-support:dict_snowball".to_owned(), @@ -205,6 +206,13 @@ fn rust_string_literal(path: &Path) -> String { format!("{:?}", path.to_string_lossy()) } +fn artifact_belongs_to_crate(name: &str) -> bool { + match ARTIFACT_KIND { + "wasix-tools-aot" => matches!(name, "tool:pg_dump" | "tool:psql"), + _ => !name.starts_with("extension:") && !matches!(name, "tool:pg_dump" | "tool:psql"), + } +} + fn write_core_aot_manifest(source: &Path, destination: &Path) -> Vec { let text = fs::read_to_string(source).expect("read generated WASIX AOT manifest"); let mut manifest: serde_json::Value = @@ -221,7 +229,7 @@ fn write_core_aot_manifest(source: &Path, destination: &Path) -> Vec { .and_then(|value| value.as_str()) .expect("AOT artifact has name") .to_owned(); - if name.starts_with("extension:") { + if !artifact_belongs_to_crate(&name) { continue; } let path = artifact diff --git a/src/runtimes/liboliphaunt/wasix/crates/assets/build.rs b/src/runtimes/liboliphaunt/wasix/crates/assets/build.rs index ee00f788..717c8cee 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/assets/build.rs +++ b/src/runtimes/liboliphaunt/wasix/crates/assets/build.rs @@ -460,7 +460,6 @@ fn write_generated_assets(out: &Path, asset_dir: &Path, selected_extensions: &[S let runtime = asset_dir.join("oliphaunt.wasix.tar.zst"); let pgdata_archive = asset_dir.join("prepopulated/pgdata-template.tar.zst"); let pgdata_manifest = asset_dir.join("prepopulated/pgdata-template.json"); - let pg_dump = asset_dir.join("bin/pg_dump.wasix.wasm"); let initdb = asset_dir.join("bin/initdb.wasix.wasm"); for required in [&manifest, &runtime, &initdb] { @@ -481,7 +480,6 @@ fn write_generated_assets(out: &Path, asset_dir: &Path, selected_extensions: &[S let pgdata_archive_body = optional_include_bytes_body(&pgdata_archive); let pgdata_manifest_body = optional_include_bytes_body(&pgdata_manifest); - let pg_dump_body = optional_include_bytes_body(&pg_dump); let extension_sql_names = selected_extension_sql_names_body(selected_extensions); let extension_archive_body = extension_archive_body(selected_extensions); let extension_sha256_body = expected_extension_archive_sha256_body(selected_extensions); @@ -495,7 +493,6 @@ fn write_generated_assets(out: &Path, asset_dir: &Path, selected_extensions: &[S pub fn runtime_archive() -> Option<&'static [u8]> {{ Some(include_bytes!({runtime})) }}\n\ pub fn pgdata_template_archive() -> Option<&'static [u8]> {{ {pgdata_archive_body} }}\n\ pub fn pgdata_template_manifest() -> Option<&'static [u8]> {{ {pgdata_manifest_body} }}\n\ - pub fn pg_dump_wasm() -> Option<&'static [u8]> {{ {pg_dump_body} }}\n\ pub fn initdb_wasm() -> Option<&'static [u8]> {{ Some(include_bytes!({initdb})) }}\n\ pub fn extension_archive(name: &str) -> Option<&'static [u8]> {{\n{extension_archive_body} }}\n\ pub fn expected_extension_archive_sha256(name: &str) -> Option<&'static str> {{\n{extension_sha256_body} }}\n\ @@ -505,7 +502,6 @@ fn write_generated_assets(out: &Path, asset_dir: &Path, selected_extensions: &[S runtime = rust_string_literal(&runtime), pgdata_archive_body = pgdata_archive_body, pgdata_manifest_body = pgdata_manifest_body, - pg_dump_body = pg_dump_body, initdb = rust_string_literal(&initdb), extension_sql_names = extension_sql_names, extension_archive_body = extension_archive_body, @@ -522,7 +518,6 @@ fn write_generated_assets(out: &Path, asset_dir: &Path, selected_extensions: &[S &runtime, &pgdata_archive, &pgdata_manifest, - &pg_dump, &initdb, ], ); @@ -539,11 +534,10 @@ fn write_source_only_assets(out: &Path, selected_extensions: &[SelectedExtension pub const SELECTED_EXTENSION_SQL_NAMES: &[&str] = {extension_sql_names};\n" ); text.push_str( - r##"pub const MANIFEST_JSON: &str = r#"{"format-version":1,"runtime":{"archive":"","sha256":"","module-sha256":"","postgres-version":"","runtime-kind":"source-only-template"},"runtime-support":[],"pg-dump":null,"extensions":[],"sources":[]}"#; + r##"pub const MANIFEST_JSON: &str = r#"{"format-version":1,"runtime":{"archive":"","sha256":"","module-sha256":"","postgres-version":"","runtime-kind":"source-only-template"},"runtime-support":[],"pg-dump":null,"psql":null,"extensions":[],"sources":[]}"#; pub fn runtime_archive() -> Option<&'static [u8]> { None } pub fn pgdata_template_archive() -> Option<&'static [u8]> { None } pub fn pgdata_template_manifest() -> Option<&'static [u8]> { None } -pub fn pg_dump_wasm() -> Option<&'static [u8]> { None } pub fn initdb_wasm() -> Option<&'static [u8]> { None } "##, ); diff --git a/src/runtimes/liboliphaunt/wasix/crates/assets/src/lib.rs b/src/runtimes/liboliphaunt/wasix/crates/assets/src/lib.rs index 067f9a5a..2602e568 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/assets/src/lib.rs +++ b/src/runtimes/liboliphaunt/wasix/crates/assets/src/lib.rs @@ -18,6 +18,8 @@ pub struct AssetManifest { #[serde(default)] pub pg_dump: Option, #[serde(default)] + pub psql: Option, + #[serde(default)] pub initdb: Option, #[serde(default)] pub pgdata_template: Option, diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/Cargo.toml b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/Cargo.toml new file mode 100644 index 00000000..c8e02eb4 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "oliphaunt-wasix-tools-aot-aarch64-apple-darwin" +version = "0.1.0" +edition = "2024" +rust-version = "1.93" +description = "Internal Wasmer AOT artifacts for oliphaunt-wasix tools on aarch64-apple-darwin" +repository = "https://github.com/f0rr0/oliphaunt" +license = "MIT AND Apache-2.0 AND PostgreSQL" +publish = false +links = "oliphaunt_artifact_oliphaunt_wasix_tools_aot_macos_arm64" +include = ["Cargo.toml", "README.md", "build.rs", "src/**", "artifacts/**"] + +[lib] +path = "src/lib.rs" + +[build-dependencies] +serde_json = "1" +sha2 = "0.10" diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/README.md b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/README.md new file mode 100644 index 00000000..23102d82 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/README.md @@ -0,0 +1,4 @@ +# oliphaunt-wasix-tools-aot-aarch64-apple-darwin + +Internal target-specific Wasmer AOT artifact crate for `oliphaunt-wasix`. +Do not depend on this crate directly. diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/build.rs b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/build.rs new file mode 100644 index 00000000..0a4ec32d --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/build.rs @@ -0,0 +1,289 @@ +use std::env; +use std::fs; +use std::io::{self, Read}; +use std::path::{Path, PathBuf}; + +use sha2::{Digest, Sha256}; + +const ARTIFACT_SCHEMA: &str = "oliphaunt-artifact-manifest-v1"; +const ARTIFACT_PRODUCT: &str = "oliphaunt-wasix-tools"; +const ARTIFACT_KIND: &str = "wasix-tools-aot"; + +fn main() { + println!("cargo:rerun-if-env-changed=OLIPHAUNT_WASM_GENERATED_AOT_DIR"); + + let target = env::var("CARGO_PKG_NAME") + .expect("CARGO_PKG_NAME is set by Cargo") + .strip_prefix("oliphaunt-wasix-tools-aot-") + .expect("AOT crate name starts with oliphaunt-wasix-tools-aot-") + .to_owned(); + emit_expected_artifact_inputs(&target); + + let out = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set by Cargo")) + .join("generated_aot.rs"); + if let Some(artifact_dir) = find_artifact_dir(&target) { + emit_rerun_directives(&artifact_dir); + write_generated_aot(&out, &target, &artifact_dir); + } else if env::var_os("OLIPHAUNT_ARTIFACT_CRATE_REQUIRE_PAYLOAD").is_some() { + panic!("release packaging requires package-local WASIX tools AOT artifacts for {target}"); + } else { + write_source_only_aot(&out, &target); + } +} + +fn emit_expected_artifact_inputs(target: &str) { + if let Some(path) = env::var_os("OLIPHAUNT_WASM_GENERATED_AOT_DIR") { + let path = PathBuf::from(path); + let candidate = if path.ends_with(target) { + path + } else { + path.join(target) + }; + emit_manifest_probe(&candidate); + } + + let manifest_dir = PathBuf::from( + env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Cargo"), + ); + if let Some(repo_root) = repo_root_from_manifest_dir(&manifest_dir) { + emit_manifest_probe(&repo_root.join("target/oliphaunt-wasix/aot").join(target)); + } + emit_manifest_probe(&manifest_dir.join("artifacts")); +} + +fn emit_manifest_probe(dir: &Path) { + println!("cargo:rerun-if-changed={}", dir.display()); + println!( + "cargo:rerun-if-changed={}", + dir.join("manifest.json").display() + ); +} + +fn find_artifact_dir(target: &str) -> Option { + let manifest_dir = PathBuf::from( + env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Cargo"), + ); + let package_artifacts = manifest_dir.join("artifacts"); + if package_artifacts.join("manifest.json").is_file() { + return Some(package_artifacts); + } + + if let Some(path) = env::var_os("OLIPHAUNT_WASM_GENERATED_AOT_DIR") { + let path = PathBuf::from(path); + let candidate = if path.ends_with(target) { + path + } else { + path.join(target) + }; + if candidate.join("manifest.json").is_file() { + return Some(candidate); + } + } + + if let Some(repo_root) = repo_root_from_manifest_dir(&manifest_dir) { + let target_artifacts = repo_root.join("target/oliphaunt-wasix/aot").join(target); + if target_artifacts.join("manifest.json").is_file() { + return Some(target_artifacts); + } + } + + None +} + +fn repo_root_from_manifest_dir(manifest_dir: &Path) -> Option<&Path> { + manifest_dir.ancestors().find(|candidate| { + candidate.join("Cargo.toml").is_file() + && candidate + .join("src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml") + .is_file() + }) +} + +fn emit_rerun_directives(artifact_dir: &Path) { + println!("cargo:rerun-if-changed={}", artifact_dir.display()); + if let Ok(entries) = fs::read_dir(artifact_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_file() { + println!("cargo:rerun-if-changed={}", path.display()); + } + } + } +} + +fn write_generated_aot(out: &Path, target: &str, artifact_dir: &Path) { + let manifest = artifact_dir.join("manifest.json"); + let generated_manifest = out + .parent() + .expect("generated AOT output has parent") + .join("manifest.json"); + let retained_paths = write_core_aot_manifest(&manifest, &generated_manifest); + let mut cases = String::new(); + if let Ok(entries) = fs::read_dir(artifact_dir) { + let mut files = entries + .flatten() + .map(|entry| entry.path()) + .filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("zst")) + .collect::>(); + files.sort(); + for file in files { + let Some(file_name) = file.file_name().and_then(|name| name.to_str()) else { + continue; + }; + let Some(stem) = file_name.strip_suffix("-llvm-opta.bin.zst") else { + continue; + }; + let artifact_name = artifact_name_from_file_stem(stem); + if !artifact_belongs_to_crate(&artifact_name) { + continue; + } + cases.push_str(&format!( + " {:?} => Some(include_bytes!({})),\n", + artifact_name, + rust_string_literal(&file) + )); + } + } + cases.push_str(" _ => None,\n"); + + let text = format!( + "pub const TARGET_TRIPLE: &str = {:?};\n\ + pub const ENGINE: &str = \"llvm-opta\";\n\ + pub const HAS_EMBEDDED_AOT: bool = true;\n\ + pub const MANIFEST_JSON: &str = include_str!({});\n\ + #[rustfmt::skip]\n\ + pub fn artifact_bytes(name: &str) -> Option<&'static [u8]> {{\n\ + match name {{\n\ + {cases} }}\n\ + }}\n", + target, + rust_string_literal(&generated_manifest) + ); + fs::write(out, text).expect("write generated AOT include module"); + let mut manifest_files = vec![generated_manifest]; + for relative in retained_paths { + manifest_files.push(artifact_dir.join(relative)); + } + emit_artifact_manifest( + out.parent().expect("generated AOT output has parent"), + target, + artifact_dir, + &manifest_files, + ); +} + +fn write_source_only_aot(out: &Path, target: &str) { + let manifest = format!( + "{{\"format-version\":1,\"target-triple\":{target:?},\"engine\":\"llvm-opta\",\"wasmer-version\":\"7.2.0-alpha.3\",\"wasmer-wasix-version\":\"0.702.0-alpha.3\",\"artifacts\":[]}}" + ); + let text = format!( + "pub const TARGET_TRIPLE: &str = {target:?};\n\ + pub const ENGINE: &str = \"llvm-opta\";\n\ + pub const HAS_EMBEDDED_AOT: bool = false;\n\ + pub const MANIFEST_JSON: &str = r#\"{manifest}\"#;\n\ + pub fn artifact_bytes(_name: &str) -> Option<&'static [u8]> {{ None }}\n" + ); + fs::write(out, text).expect("write source-only AOT include module"); +} + +fn artifact_name_from_file_stem(stem: &str) -> String { + match stem { + "oliphaunt" => "runtime:oliphaunt".to_owned(), + "pg_dump" => "tool:pg_dump".to_owned(), + "psql" => "tool:psql".to_owned(), + "initdb" => "tool:initdb".to_owned(), + "plpgsql" => "runtime-support:plpgsql".to_owned(), + "dict_snowball" => "runtime-support:dict_snowball".to_owned(), + extension_support if extension_support.ends_with("_deps") => { + let sql_name = extension_support.trim_end_matches("_deps"); + format!("extension:{sql_name}:{extension_support}") + } + extension => format!("extension:{extension}"), + } +} + +fn rust_string_literal(path: &Path) -> String { + format!("{:?}", path.to_string_lossy()) +} + +fn artifact_belongs_to_crate(name: &str) -> bool { + match ARTIFACT_KIND { + "wasix-tools-aot" => matches!(name, "tool:pg_dump" | "tool:psql"), + _ => !name.starts_with("extension:") && !matches!(name, "tool:pg_dump" | "tool:psql"), + } +} + +fn write_core_aot_manifest(source: &Path, destination: &Path) -> Vec { + let text = fs::read_to_string(source).expect("read generated WASIX AOT manifest"); + let mut manifest: serde_json::Value = + serde_json::from_str(&text).expect("parse generated WASIX AOT manifest"); + let artifacts = manifest + .get_mut("artifacts") + .and_then(|value| value.as_array_mut()) + .expect("generated WASIX AOT manifest has artifacts array"); + let mut retained = Vec::new(); + let mut paths = Vec::new(); + for artifact in artifacts.drain(..) { + let name = artifact + .get("name") + .and_then(|value| value.as_str()) + .expect("AOT artifact has name") + .to_owned(); + if !artifact_belongs_to_crate(&name) { + continue; + } + let path = artifact + .get("path") + .and_then(|value| value.as_str()) + .expect("AOT artifact has path") + .to_owned(); + paths.push(path); + retained.push(artifact); + } + *artifacts = retained; + let rendered = + serde_json::to_string_pretty(&manifest).expect("serialize core WASIX AOT manifest"); + fs::write(destination, format!("{rendered}\n")).expect("write core WASIX AOT manifest"); + paths +} + +fn emit_artifact_manifest(out_dir: &Path, target: &str, artifact_dir: &Path, files: &[PathBuf]) { + let version = env::var("CARGO_PKG_VERSION").expect("CARGO_PKG_VERSION is set by Cargo"); + let manifest_path = out_dir.join("oliphaunt-artifact.toml"); + let mut text = format!( + "schema = {ARTIFACT_SCHEMA:?}\nproduct = {ARTIFACT_PRODUCT:?}\nversion = {version:?}\nkind = {ARTIFACT_KIND:?}\ntarget = {target:?}\n" + ); + for file in files { + if !file.is_file() { + continue; + } + let relative = file + .strip_prefix(artifact_dir) + .ok() + .map(|path| path.to_string_lossy().replace('\\', "/")) + .unwrap_or_else(|| "manifest.json".to_owned()); + let sha256 = sha256_file(file).expect("hash WASIX AOT artifact file"); + text.push_str(&format!( + "\n[[files]]\nsource = {:?}\nrelative = {:?}\nsha256 = {:?}\nexecutable = false\n", + file.display().to_string(), + relative, + sha256, + )); + } + fs::write(&manifest_path, text).expect("write WASIX AOT Cargo artifact manifest"); + println!("cargo::metadata=manifest={}", manifest_path.display()); +} + +fn sha256_file(path: &Path) -> io::Result { + let mut file = fs::File::open(path)?; + let mut hasher = Sha256::new(); + let mut buffer = [0u8; 128 * 1024]; + loop { + let read = file.read(&mut buffer)?; + if read == 0 { + break; + } + hasher.update(&buffer[..read]); + } + Ok(format!("{:x}", hasher.finalize())) +} diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/src/lib.rs b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/src/lib.rs new file mode 100644 index 00000000..edcddc24 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/src/lib.rs @@ -0,0 +1,3 @@ +#![deny(unsafe_code)] + +include!(concat!(env!("OUT_DIR"), "/generated_aot.rs")); diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/Cargo.toml b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/Cargo.toml new file mode 100644 index 00000000..e9015723 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu" +version = "0.1.0" +edition = "2024" +rust-version = "1.93" +description = "Internal Wasmer AOT artifacts for oliphaunt-wasix tools on aarch64-unknown-linux-gnu" +repository = "https://github.com/f0rr0/oliphaunt" +license = "MIT AND Apache-2.0 AND PostgreSQL" +publish = false +links = "oliphaunt_artifact_oliphaunt_wasix_tools_aot_linux_arm64_gnu" +include = ["Cargo.toml", "README.md", "build.rs", "src/**", "artifacts/**"] + +[lib] +path = "src/lib.rs" + +[build-dependencies] +serde_json = "1" +sha2 = "0.10" diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/README.md b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/README.md new file mode 100644 index 00000000..a209c192 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/README.md @@ -0,0 +1,4 @@ +# oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu + +Internal target-specific Wasmer AOT artifact crate for `oliphaunt-wasix`. +Do not depend on this crate directly. diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/build.rs b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/build.rs new file mode 100644 index 00000000..0a4ec32d --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/build.rs @@ -0,0 +1,289 @@ +use std::env; +use std::fs; +use std::io::{self, Read}; +use std::path::{Path, PathBuf}; + +use sha2::{Digest, Sha256}; + +const ARTIFACT_SCHEMA: &str = "oliphaunt-artifact-manifest-v1"; +const ARTIFACT_PRODUCT: &str = "oliphaunt-wasix-tools"; +const ARTIFACT_KIND: &str = "wasix-tools-aot"; + +fn main() { + println!("cargo:rerun-if-env-changed=OLIPHAUNT_WASM_GENERATED_AOT_DIR"); + + let target = env::var("CARGO_PKG_NAME") + .expect("CARGO_PKG_NAME is set by Cargo") + .strip_prefix("oliphaunt-wasix-tools-aot-") + .expect("AOT crate name starts with oliphaunt-wasix-tools-aot-") + .to_owned(); + emit_expected_artifact_inputs(&target); + + let out = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set by Cargo")) + .join("generated_aot.rs"); + if let Some(artifact_dir) = find_artifact_dir(&target) { + emit_rerun_directives(&artifact_dir); + write_generated_aot(&out, &target, &artifact_dir); + } else if env::var_os("OLIPHAUNT_ARTIFACT_CRATE_REQUIRE_PAYLOAD").is_some() { + panic!("release packaging requires package-local WASIX tools AOT artifacts for {target}"); + } else { + write_source_only_aot(&out, &target); + } +} + +fn emit_expected_artifact_inputs(target: &str) { + if let Some(path) = env::var_os("OLIPHAUNT_WASM_GENERATED_AOT_DIR") { + let path = PathBuf::from(path); + let candidate = if path.ends_with(target) { + path + } else { + path.join(target) + }; + emit_manifest_probe(&candidate); + } + + let manifest_dir = PathBuf::from( + env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Cargo"), + ); + if let Some(repo_root) = repo_root_from_manifest_dir(&manifest_dir) { + emit_manifest_probe(&repo_root.join("target/oliphaunt-wasix/aot").join(target)); + } + emit_manifest_probe(&manifest_dir.join("artifacts")); +} + +fn emit_manifest_probe(dir: &Path) { + println!("cargo:rerun-if-changed={}", dir.display()); + println!( + "cargo:rerun-if-changed={}", + dir.join("manifest.json").display() + ); +} + +fn find_artifact_dir(target: &str) -> Option { + let manifest_dir = PathBuf::from( + env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Cargo"), + ); + let package_artifacts = manifest_dir.join("artifacts"); + if package_artifacts.join("manifest.json").is_file() { + return Some(package_artifacts); + } + + if let Some(path) = env::var_os("OLIPHAUNT_WASM_GENERATED_AOT_DIR") { + let path = PathBuf::from(path); + let candidate = if path.ends_with(target) { + path + } else { + path.join(target) + }; + if candidate.join("manifest.json").is_file() { + return Some(candidate); + } + } + + if let Some(repo_root) = repo_root_from_manifest_dir(&manifest_dir) { + let target_artifacts = repo_root.join("target/oliphaunt-wasix/aot").join(target); + if target_artifacts.join("manifest.json").is_file() { + return Some(target_artifacts); + } + } + + None +} + +fn repo_root_from_manifest_dir(manifest_dir: &Path) -> Option<&Path> { + manifest_dir.ancestors().find(|candidate| { + candidate.join("Cargo.toml").is_file() + && candidate + .join("src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml") + .is_file() + }) +} + +fn emit_rerun_directives(artifact_dir: &Path) { + println!("cargo:rerun-if-changed={}", artifact_dir.display()); + if let Ok(entries) = fs::read_dir(artifact_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_file() { + println!("cargo:rerun-if-changed={}", path.display()); + } + } + } +} + +fn write_generated_aot(out: &Path, target: &str, artifact_dir: &Path) { + let manifest = artifact_dir.join("manifest.json"); + let generated_manifest = out + .parent() + .expect("generated AOT output has parent") + .join("manifest.json"); + let retained_paths = write_core_aot_manifest(&manifest, &generated_manifest); + let mut cases = String::new(); + if let Ok(entries) = fs::read_dir(artifact_dir) { + let mut files = entries + .flatten() + .map(|entry| entry.path()) + .filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("zst")) + .collect::>(); + files.sort(); + for file in files { + let Some(file_name) = file.file_name().and_then(|name| name.to_str()) else { + continue; + }; + let Some(stem) = file_name.strip_suffix("-llvm-opta.bin.zst") else { + continue; + }; + let artifact_name = artifact_name_from_file_stem(stem); + if !artifact_belongs_to_crate(&artifact_name) { + continue; + } + cases.push_str(&format!( + " {:?} => Some(include_bytes!({})),\n", + artifact_name, + rust_string_literal(&file) + )); + } + } + cases.push_str(" _ => None,\n"); + + let text = format!( + "pub const TARGET_TRIPLE: &str = {:?};\n\ + pub const ENGINE: &str = \"llvm-opta\";\n\ + pub const HAS_EMBEDDED_AOT: bool = true;\n\ + pub const MANIFEST_JSON: &str = include_str!({});\n\ + #[rustfmt::skip]\n\ + pub fn artifact_bytes(name: &str) -> Option<&'static [u8]> {{\n\ + match name {{\n\ + {cases} }}\n\ + }}\n", + target, + rust_string_literal(&generated_manifest) + ); + fs::write(out, text).expect("write generated AOT include module"); + let mut manifest_files = vec![generated_manifest]; + for relative in retained_paths { + manifest_files.push(artifact_dir.join(relative)); + } + emit_artifact_manifest( + out.parent().expect("generated AOT output has parent"), + target, + artifact_dir, + &manifest_files, + ); +} + +fn write_source_only_aot(out: &Path, target: &str) { + let manifest = format!( + "{{\"format-version\":1,\"target-triple\":{target:?},\"engine\":\"llvm-opta\",\"wasmer-version\":\"7.2.0-alpha.3\",\"wasmer-wasix-version\":\"0.702.0-alpha.3\",\"artifacts\":[]}}" + ); + let text = format!( + "pub const TARGET_TRIPLE: &str = {target:?};\n\ + pub const ENGINE: &str = \"llvm-opta\";\n\ + pub const HAS_EMBEDDED_AOT: bool = false;\n\ + pub const MANIFEST_JSON: &str = r#\"{manifest}\"#;\n\ + pub fn artifact_bytes(_name: &str) -> Option<&'static [u8]> {{ None }}\n" + ); + fs::write(out, text).expect("write source-only AOT include module"); +} + +fn artifact_name_from_file_stem(stem: &str) -> String { + match stem { + "oliphaunt" => "runtime:oliphaunt".to_owned(), + "pg_dump" => "tool:pg_dump".to_owned(), + "psql" => "tool:psql".to_owned(), + "initdb" => "tool:initdb".to_owned(), + "plpgsql" => "runtime-support:plpgsql".to_owned(), + "dict_snowball" => "runtime-support:dict_snowball".to_owned(), + extension_support if extension_support.ends_with("_deps") => { + let sql_name = extension_support.trim_end_matches("_deps"); + format!("extension:{sql_name}:{extension_support}") + } + extension => format!("extension:{extension}"), + } +} + +fn rust_string_literal(path: &Path) -> String { + format!("{:?}", path.to_string_lossy()) +} + +fn artifact_belongs_to_crate(name: &str) -> bool { + match ARTIFACT_KIND { + "wasix-tools-aot" => matches!(name, "tool:pg_dump" | "tool:psql"), + _ => !name.starts_with("extension:") && !matches!(name, "tool:pg_dump" | "tool:psql"), + } +} + +fn write_core_aot_manifest(source: &Path, destination: &Path) -> Vec { + let text = fs::read_to_string(source).expect("read generated WASIX AOT manifest"); + let mut manifest: serde_json::Value = + serde_json::from_str(&text).expect("parse generated WASIX AOT manifest"); + let artifacts = manifest + .get_mut("artifacts") + .and_then(|value| value.as_array_mut()) + .expect("generated WASIX AOT manifest has artifacts array"); + let mut retained = Vec::new(); + let mut paths = Vec::new(); + for artifact in artifacts.drain(..) { + let name = artifact + .get("name") + .and_then(|value| value.as_str()) + .expect("AOT artifact has name") + .to_owned(); + if !artifact_belongs_to_crate(&name) { + continue; + } + let path = artifact + .get("path") + .and_then(|value| value.as_str()) + .expect("AOT artifact has path") + .to_owned(); + paths.push(path); + retained.push(artifact); + } + *artifacts = retained; + let rendered = + serde_json::to_string_pretty(&manifest).expect("serialize core WASIX AOT manifest"); + fs::write(destination, format!("{rendered}\n")).expect("write core WASIX AOT manifest"); + paths +} + +fn emit_artifact_manifest(out_dir: &Path, target: &str, artifact_dir: &Path, files: &[PathBuf]) { + let version = env::var("CARGO_PKG_VERSION").expect("CARGO_PKG_VERSION is set by Cargo"); + let manifest_path = out_dir.join("oliphaunt-artifact.toml"); + let mut text = format!( + "schema = {ARTIFACT_SCHEMA:?}\nproduct = {ARTIFACT_PRODUCT:?}\nversion = {version:?}\nkind = {ARTIFACT_KIND:?}\ntarget = {target:?}\n" + ); + for file in files { + if !file.is_file() { + continue; + } + let relative = file + .strip_prefix(artifact_dir) + .ok() + .map(|path| path.to_string_lossy().replace('\\', "/")) + .unwrap_or_else(|| "manifest.json".to_owned()); + let sha256 = sha256_file(file).expect("hash WASIX AOT artifact file"); + text.push_str(&format!( + "\n[[files]]\nsource = {:?}\nrelative = {:?}\nsha256 = {:?}\nexecutable = false\n", + file.display().to_string(), + relative, + sha256, + )); + } + fs::write(&manifest_path, text).expect("write WASIX AOT Cargo artifact manifest"); + println!("cargo::metadata=manifest={}", manifest_path.display()); +} + +fn sha256_file(path: &Path) -> io::Result { + let mut file = fs::File::open(path)?; + let mut hasher = Sha256::new(); + let mut buffer = [0u8; 128 * 1024]; + loop { + let read = file.read(&mut buffer)?; + if read == 0 { + break; + } + hasher.update(&buffer[..read]); + } + Ok(format!("{:x}", hasher.finalize())) +} diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/src/lib.rs b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/src/lib.rs new file mode 100644 index 00000000..edcddc24 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/src/lib.rs @@ -0,0 +1,3 @@ +#![deny(unsafe_code)] + +include!(concat!(env!("OUT_DIR"), "/generated_aot.rs")); diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/Cargo.toml b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/Cargo.toml new file mode 100644 index 00000000..2d2a7815 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc" +version = "0.1.0" +edition = "2024" +rust-version = "1.93" +description = "Internal Wasmer AOT artifacts for oliphaunt-wasix tools on x86_64-pc-windows-msvc" +repository = "https://github.com/f0rr0/oliphaunt" +license = "MIT AND Apache-2.0 AND PostgreSQL" +publish = false +links = "oliphaunt_artifact_oliphaunt_wasix_tools_aot_windows_x64_msvc" +include = ["Cargo.toml", "README.md", "build.rs", "src/**", "artifacts/**"] + +[lib] +path = "src/lib.rs" + +[build-dependencies] +serde_json = "1" +sha2 = "0.10" diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/README.md b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/README.md new file mode 100644 index 00000000..85a746d5 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/README.md @@ -0,0 +1,4 @@ +# oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc + +Internal target-specific Wasmer AOT artifact crate for `oliphaunt-wasix`. +Do not depend on this crate directly. diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/build.rs b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/build.rs new file mode 100644 index 00000000..0a4ec32d --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/build.rs @@ -0,0 +1,289 @@ +use std::env; +use std::fs; +use std::io::{self, Read}; +use std::path::{Path, PathBuf}; + +use sha2::{Digest, Sha256}; + +const ARTIFACT_SCHEMA: &str = "oliphaunt-artifact-manifest-v1"; +const ARTIFACT_PRODUCT: &str = "oliphaunt-wasix-tools"; +const ARTIFACT_KIND: &str = "wasix-tools-aot"; + +fn main() { + println!("cargo:rerun-if-env-changed=OLIPHAUNT_WASM_GENERATED_AOT_DIR"); + + let target = env::var("CARGO_PKG_NAME") + .expect("CARGO_PKG_NAME is set by Cargo") + .strip_prefix("oliphaunt-wasix-tools-aot-") + .expect("AOT crate name starts with oliphaunt-wasix-tools-aot-") + .to_owned(); + emit_expected_artifact_inputs(&target); + + let out = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set by Cargo")) + .join("generated_aot.rs"); + if let Some(artifact_dir) = find_artifact_dir(&target) { + emit_rerun_directives(&artifact_dir); + write_generated_aot(&out, &target, &artifact_dir); + } else if env::var_os("OLIPHAUNT_ARTIFACT_CRATE_REQUIRE_PAYLOAD").is_some() { + panic!("release packaging requires package-local WASIX tools AOT artifacts for {target}"); + } else { + write_source_only_aot(&out, &target); + } +} + +fn emit_expected_artifact_inputs(target: &str) { + if let Some(path) = env::var_os("OLIPHAUNT_WASM_GENERATED_AOT_DIR") { + let path = PathBuf::from(path); + let candidate = if path.ends_with(target) { + path + } else { + path.join(target) + }; + emit_manifest_probe(&candidate); + } + + let manifest_dir = PathBuf::from( + env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Cargo"), + ); + if let Some(repo_root) = repo_root_from_manifest_dir(&manifest_dir) { + emit_manifest_probe(&repo_root.join("target/oliphaunt-wasix/aot").join(target)); + } + emit_manifest_probe(&manifest_dir.join("artifacts")); +} + +fn emit_manifest_probe(dir: &Path) { + println!("cargo:rerun-if-changed={}", dir.display()); + println!( + "cargo:rerun-if-changed={}", + dir.join("manifest.json").display() + ); +} + +fn find_artifact_dir(target: &str) -> Option { + let manifest_dir = PathBuf::from( + env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Cargo"), + ); + let package_artifacts = manifest_dir.join("artifacts"); + if package_artifacts.join("manifest.json").is_file() { + return Some(package_artifacts); + } + + if let Some(path) = env::var_os("OLIPHAUNT_WASM_GENERATED_AOT_DIR") { + let path = PathBuf::from(path); + let candidate = if path.ends_with(target) { + path + } else { + path.join(target) + }; + if candidate.join("manifest.json").is_file() { + return Some(candidate); + } + } + + if let Some(repo_root) = repo_root_from_manifest_dir(&manifest_dir) { + let target_artifacts = repo_root.join("target/oliphaunt-wasix/aot").join(target); + if target_artifacts.join("manifest.json").is_file() { + return Some(target_artifacts); + } + } + + None +} + +fn repo_root_from_manifest_dir(manifest_dir: &Path) -> Option<&Path> { + manifest_dir.ancestors().find(|candidate| { + candidate.join("Cargo.toml").is_file() + && candidate + .join("src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml") + .is_file() + }) +} + +fn emit_rerun_directives(artifact_dir: &Path) { + println!("cargo:rerun-if-changed={}", artifact_dir.display()); + if let Ok(entries) = fs::read_dir(artifact_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_file() { + println!("cargo:rerun-if-changed={}", path.display()); + } + } + } +} + +fn write_generated_aot(out: &Path, target: &str, artifact_dir: &Path) { + let manifest = artifact_dir.join("manifest.json"); + let generated_manifest = out + .parent() + .expect("generated AOT output has parent") + .join("manifest.json"); + let retained_paths = write_core_aot_manifest(&manifest, &generated_manifest); + let mut cases = String::new(); + if let Ok(entries) = fs::read_dir(artifact_dir) { + let mut files = entries + .flatten() + .map(|entry| entry.path()) + .filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("zst")) + .collect::>(); + files.sort(); + for file in files { + let Some(file_name) = file.file_name().and_then(|name| name.to_str()) else { + continue; + }; + let Some(stem) = file_name.strip_suffix("-llvm-opta.bin.zst") else { + continue; + }; + let artifact_name = artifact_name_from_file_stem(stem); + if !artifact_belongs_to_crate(&artifact_name) { + continue; + } + cases.push_str(&format!( + " {:?} => Some(include_bytes!({})),\n", + artifact_name, + rust_string_literal(&file) + )); + } + } + cases.push_str(" _ => None,\n"); + + let text = format!( + "pub const TARGET_TRIPLE: &str = {:?};\n\ + pub const ENGINE: &str = \"llvm-opta\";\n\ + pub const HAS_EMBEDDED_AOT: bool = true;\n\ + pub const MANIFEST_JSON: &str = include_str!({});\n\ + #[rustfmt::skip]\n\ + pub fn artifact_bytes(name: &str) -> Option<&'static [u8]> {{\n\ + match name {{\n\ + {cases} }}\n\ + }}\n", + target, + rust_string_literal(&generated_manifest) + ); + fs::write(out, text).expect("write generated AOT include module"); + let mut manifest_files = vec![generated_manifest]; + for relative in retained_paths { + manifest_files.push(artifact_dir.join(relative)); + } + emit_artifact_manifest( + out.parent().expect("generated AOT output has parent"), + target, + artifact_dir, + &manifest_files, + ); +} + +fn write_source_only_aot(out: &Path, target: &str) { + let manifest = format!( + "{{\"format-version\":1,\"target-triple\":{target:?},\"engine\":\"llvm-opta\",\"wasmer-version\":\"7.2.0-alpha.3\",\"wasmer-wasix-version\":\"0.702.0-alpha.3\",\"artifacts\":[]}}" + ); + let text = format!( + "pub const TARGET_TRIPLE: &str = {target:?};\n\ + pub const ENGINE: &str = \"llvm-opta\";\n\ + pub const HAS_EMBEDDED_AOT: bool = false;\n\ + pub const MANIFEST_JSON: &str = r#\"{manifest}\"#;\n\ + pub fn artifact_bytes(_name: &str) -> Option<&'static [u8]> {{ None }}\n" + ); + fs::write(out, text).expect("write source-only AOT include module"); +} + +fn artifact_name_from_file_stem(stem: &str) -> String { + match stem { + "oliphaunt" => "runtime:oliphaunt".to_owned(), + "pg_dump" => "tool:pg_dump".to_owned(), + "psql" => "tool:psql".to_owned(), + "initdb" => "tool:initdb".to_owned(), + "plpgsql" => "runtime-support:plpgsql".to_owned(), + "dict_snowball" => "runtime-support:dict_snowball".to_owned(), + extension_support if extension_support.ends_with("_deps") => { + let sql_name = extension_support.trim_end_matches("_deps"); + format!("extension:{sql_name}:{extension_support}") + } + extension => format!("extension:{extension}"), + } +} + +fn rust_string_literal(path: &Path) -> String { + format!("{:?}", path.to_string_lossy()) +} + +fn artifact_belongs_to_crate(name: &str) -> bool { + match ARTIFACT_KIND { + "wasix-tools-aot" => matches!(name, "tool:pg_dump" | "tool:psql"), + _ => !name.starts_with("extension:") && !matches!(name, "tool:pg_dump" | "tool:psql"), + } +} + +fn write_core_aot_manifest(source: &Path, destination: &Path) -> Vec { + let text = fs::read_to_string(source).expect("read generated WASIX AOT manifest"); + let mut manifest: serde_json::Value = + serde_json::from_str(&text).expect("parse generated WASIX AOT manifest"); + let artifacts = manifest + .get_mut("artifacts") + .and_then(|value| value.as_array_mut()) + .expect("generated WASIX AOT manifest has artifacts array"); + let mut retained = Vec::new(); + let mut paths = Vec::new(); + for artifact in artifacts.drain(..) { + let name = artifact + .get("name") + .and_then(|value| value.as_str()) + .expect("AOT artifact has name") + .to_owned(); + if !artifact_belongs_to_crate(&name) { + continue; + } + let path = artifact + .get("path") + .and_then(|value| value.as_str()) + .expect("AOT artifact has path") + .to_owned(); + paths.push(path); + retained.push(artifact); + } + *artifacts = retained; + let rendered = + serde_json::to_string_pretty(&manifest).expect("serialize core WASIX AOT manifest"); + fs::write(destination, format!("{rendered}\n")).expect("write core WASIX AOT manifest"); + paths +} + +fn emit_artifact_manifest(out_dir: &Path, target: &str, artifact_dir: &Path, files: &[PathBuf]) { + let version = env::var("CARGO_PKG_VERSION").expect("CARGO_PKG_VERSION is set by Cargo"); + let manifest_path = out_dir.join("oliphaunt-artifact.toml"); + let mut text = format!( + "schema = {ARTIFACT_SCHEMA:?}\nproduct = {ARTIFACT_PRODUCT:?}\nversion = {version:?}\nkind = {ARTIFACT_KIND:?}\ntarget = {target:?}\n" + ); + for file in files { + if !file.is_file() { + continue; + } + let relative = file + .strip_prefix(artifact_dir) + .ok() + .map(|path| path.to_string_lossy().replace('\\', "/")) + .unwrap_or_else(|| "manifest.json".to_owned()); + let sha256 = sha256_file(file).expect("hash WASIX AOT artifact file"); + text.push_str(&format!( + "\n[[files]]\nsource = {:?}\nrelative = {:?}\nsha256 = {:?}\nexecutable = false\n", + file.display().to_string(), + relative, + sha256, + )); + } + fs::write(&manifest_path, text).expect("write WASIX AOT Cargo artifact manifest"); + println!("cargo::metadata=manifest={}", manifest_path.display()); +} + +fn sha256_file(path: &Path) -> io::Result { + let mut file = fs::File::open(path)?; + let mut hasher = Sha256::new(); + let mut buffer = [0u8; 128 * 1024]; + loop { + let read = file.read(&mut buffer)?; + if read == 0 { + break; + } + hasher.update(&buffer[..read]); + } + Ok(format!("{:x}", hasher.finalize())) +} diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/src/lib.rs b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/src/lib.rs new file mode 100644 index 00000000..edcddc24 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/src/lib.rs @@ -0,0 +1,3 @@ +#![deny(unsafe_code)] + +include!(concat!(env!("OUT_DIR"), "/generated_aot.rs")); diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/Cargo.toml b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/Cargo.toml new file mode 100644 index 00000000..7a9c55fd --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu" +version = "0.1.0" +edition = "2024" +rust-version = "1.93" +description = "Internal Wasmer AOT artifacts for oliphaunt-wasix tools on x86_64-unknown-linux-gnu" +repository = "https://github.com/f0rr0/oliphaunt" +license = "MIT AND Apache-2.0 AND PostgreSQL" +publish = false +links = "oliphaunt_artifact_oliphaunt_wasix_tools_aot_linux_x64_gnu" +include = ["Cargo.toml", "README.md", "build.rs", "src/**", "artifacts/**"] + +[lib] +path = "src/lib.rs" + +[build-dependencies] +serde_json = "1" +sha2 = "0.10" diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/README.md b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/README.md new file mode 100644 index 00000000..e7b3bf74 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/README.md @@ -0,0 +1,4 @@ +# oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu + +Internal target-specific Wasmer AOT artifact crate for `oliphaunt-wasix`. +Do not depend on this crate directly. diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/build.rs b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/build.rs new file mode 100644 index 00000000..0a4ec32d --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/build.rs @@ -0,0 +1,289 @@ +use std::env; +use std::fs; +use std::io::{self, Read}; +use std::path::{Path, PathBuf}; + +use sha2::{Digest, Sha256}; + +const ARTIFACT_SCHEMA: &str = "oliphaunt-artifact-manifest-v1"; +const ARTIFACT_PRODUCT: &str = "oliphaunt-wasix-tools"; +const ARTIFACT_KIND: &str = "wasix-tools-aot"; + +fn main() { + println!("cargo:rerun-if-env-changed=OLIPHAUNT_WASM_GENERATED_AOT_DIR"); + + let target = env::var("CARGO_PKG_NAME") + .expect("CARGO_PKG_NAME is set by Cargo") + .strip_prefix("oliphaunt-wasix-tools-aot-") + .expect("AOT crate name starts with oliphaunt-wasix-tools-aot-") + .to_owned(); + emit_expected_artifact_inputs(&target); + + let out = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set by Cargo")) + .join("generated_aot.rs"); + if let Some(artifact_dir) = find_artifact_dir(&target) { + emit_rerun_directives(&artifact_dir); + write_generated_aot(&out, &target, &artifact_dir); + } else if env::var_os("OLIPHAUNT_ARTIFACT_CRATE_REQUIRE_PAYLOAD").is_some() { + panic!("release packaging requires package-local WASIX tools AOT artifacts for {target}"); + } else { + write_source_only_aot(&out, &target); + } +} + +fn emit_expected_artifact_inputs(target: &str) { + if let Some(path) = env::var_os("OLIPHAUNT_WASM_GENERATED_AOT_DIR") { + let path = PathBuf::from(path); + let candidate = if path.ends_with(target) { + path + } else { + path.join(target) + }; + emit_manifest_probe(&candidate); + } + + let manifest_dir = PathBuf::from( + env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Cargo"), + ); + if let Some(repo_root) = repo_root_from_manifest_dir(&manifest_dir) { + emit_manifest_probe(&repo_root.join("target/oliphaunt-wasix/aot").join(target)); + } + emit_manifest_probe(&manifest_dir.join("artifacts")); +} + +fn emit_manifest_probe(dir: &Path) { + println!("cargo:rerun-if-changed={}", dir.display()); + println!( + "cargo:rerun-if-changed={}", + dir.join("manifest.json").display() + ); +} + +fn find_artifact_dir(target: &str) -> Option { + let manifest_dir = PathBuf::from( + env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by Cargo"), + ); + let package_artifacts = manifest_dir.join("artifacts"); + if package_artifacts.join("manifest.json").is_file() { + return Some(package_artifacts); + } + + if let Some(path) = env::var_os("OLIPHAUNT_WASM_GENERATED_AOT_DIR") { + let path = PathBuf::from(path); + let candidate = if path.ends_with(target) { + path + } else { + path.join(target) + }; + if candidate.join("manifest.json").is_file() { + return Some(candidate); + } + } + + if let Some(repo_root) = repo_root_from_manifest_dir(&manifest_dir) { + let target_artifacts = repo_root.join("target/oliphaunt-wasix/aot").join(target); + if target_artifacts.join("manifest.json").is_file() { + return Some(target_artifacts); + } + } + + None +} + +fn repo_root_from_manifest_dir(manifest_dir: &Path) -> Option<&Path> { + manifest_dir.ancestors().find(|candidate| { + candidate.join("Cargo.toml").is_file() + && candidate + .join("src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml") + .is_file() + }) +} + +fn emit_rerun_directives(artifact_dir: &Path) { + println!("cargo:rerun-if-changed={}", artifact_dir.display()); + if let Ok(entries) = fs::read_dir(artifact_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_file() { + println!("cargo:rerun-if-changed={}", path.display()); + } + } + } +} + +fn write_generated_aot(out: &Path, target: &str, artifact_dir: &Path) { + let manifest = artifact_dir.join("manifest.json"); + let generated_manifest = out + .parent() + .expect("generated AOT output has parent") + .join("manifest.json"); + let retained_paths = write_core_aot_manifest(&manifest, &generated_manifest); + let mut cases = String::new(); + if let Ok(entries) = fs::read_dir(artifact_dir) { + let mut files = entries + .flatten() + .map(|entry| entry.path()) + .filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("zst")) + .collect::>(); + files.sort(); + for file in files { + let Some(file_name) = file.file_name().and_then(|name| name.to_str()) else { + continue; + }; + let Some(stem) = file_name.strip_suffix("-llvm-opta.bin.zst") else { + continue; + }; + let artifact_name = artifact_name_from_file_stem(stem); + if !artifact_belongs_to_crate(&artifact_name) { + continue; + } + cases.push_str(&format!( + " {:?} => Some(include_bytes!({})),\n", + artifact_name, + rust_string_literal(&file) + )); + } + } + cases.push_str(" _ => None,\n"); + + let text = format!( + "pub const TARGET_TRIPLE: &str = {:?};\n\ + pub const ENGINE: &str = \"llvm-opta\";\n\ + pub const HAS_EMBEDDED_AOT: bool = true;\n\ + pub const MANIFEST_JSON: &str = include_str!({});\n\ + #[rustfmt::skip]\n\ + pub fn artifact_bytes(name: &str) -> Option<&'static [u8]> {{\n\ + match name {{\n\ + {cases} }}\n\ + }}\n", + target, + rust_string_literal(&generated_manifest) + ); + fs::write(out, text).expect("write generated AOT include module"); + let mut manifest_files = vec![generated_manifest]; + for relative in retained_paths { + manifest_files.push(artifact_dir.join(relative)); + } + emit_artifact_manifest( + out.parent().expect("generated AOT output has parent"), + target, + artifact_dir, + &manifest_files, + ); +} + +fn write_source_only_aot(out: &Path, target: &str) { + let manifest = format!( + "{{\"format-version\":1,\"target-triple\":{target:?},\"engine\":\"llvm-opta\",\"wasmer-version\":\"7.2.0-alpha.3\",\"wasmer-wasix-version\":\"0.702.0-alpha.3\",\"artifacts\":[]}}" + ); + let text = format!( + "pub const TARGET_TRIPLE: &str = {target:?};\n\ + pub const ENGINE: &str = \"llvm-opta\";\n\ + pub const HAS_EMBEDDED_AOT: bool = false;\n\ + pub const MANIFEST_JSON: &str = r#\"{manifest}\"#;\n\ + pub fn artifact_bytes(_name: &str) -> Option<&'static [u8]> {{ None }}\n" + ); + fs::write(out, text).expect("write source-only AOT include module"); +} + +fn artifact_name_from_file_stem(stem: &str) -> String { + match stem { + "oliphaunt" => "runtime:oliphaunt".to_owned(), + "pg_dump" => "tool:pg_dump".to_owned(), + "psql" => "tool:psql".to_owned(), + "initdb" => "tool:initdb".to_owned(), + "plpgsql" => "runtime-support:plpgsql".to_owned(), + "dict_snowball" => "runtime-support:dict_snowball".to_owned(), + extension_support if extension_support.ends_with("_deps") => { + let sql_name = extension_support.trim_end_matches("_deps"); + format!("extension:{sql_name}:{extension_support}") + } + extension => format!("extension:{extension}"), + } +} + +fn rust_string_literal(path: &Path) -> String { + format!("{:?}", path.to_string_lossy()) +} + +fn artifact_belongs_to_crate(name: &str) -> bool { + match ARTIFACT_KIND { + "wasix-tools-aot" => matches!(name, "tool:pg_dump" | "tool:psql"), + _ => !name.starts_with("extension:") && !matches!(name, "tool:pg_dump" | "tool:psql"), + } +} + +fn write_core_aot_manifest(source: &Path, destination: &Path) -> Vec { + let text = fs::read_to_string(source).expect("read generated WASIX AOT manifest"); + let mut manifest: serde_json::Value = + serde_json::from_str(&text).expect("parse generated WASIX AOT manifest"); + let artifacts = manifest + .get_mut("artifacts") + .and_then(|value| value.as_array_mut()) + .expect("generated WASIX AOT manifest has artifacts array"); + let mut retained = Vec::new(); + let mut paths = Vec::new(); + for artifact in artifacts.drain(..) { + let name = artifact + .get("name") + .and_then(|value| value.as_str()) + .expect("AOT artifact has name") + .to_owned(); + if !artifact_belongs_to_crate(&name) { + continue; + } + let path = artifact + .get("path") + .and_then(|value| value.as_str()) + .expect("AOT artifact has path") + .to_owned(); + paths.push(path); + retained.push(artifact); + } + *artifacts = retained; + let rendered = + serde_json::to_string_pretty(&manifest).expect("serialize core WASIX AOT manifest"); + fs::write(destination, format!("{rendered}\n")).expect("write core WASIX AOT manifest"); + paths +} + +fn emit_artifact_manifest(out_dir: &Path, target: &str, artifact_dir: &Path, files: &[PathBuf]) { + let version = env::var("CARGO_PKG_VERSION").expect("CARGO_PKG_VERSION is set by Cargo"); + let manifest_path = out_dir.join("oliphaunt-artifact.toml"); + let mut text = format!( + "schema = {ARTIFACT_SCHEMA:?}\nproduct = {ARTIFACT_PRODUCT:?}\nversion = {version:?}\nkind = {ARTIFACT_KIND:?}\ntarget = {target:?}\n" + ); + for file in files { + if !file.is_file() { + continue; + } + let relative = file + .strip_prefix(artifact_dir) + .ok() + .map(|path| path.to_string_lossy().replace('\\', "/")) + .unwrap_or_else(|| "manifest.json".to_owned()); + let sha256 = sha256_file(file).expect("hash WASIX AOT artifact file"); + text.push_str(&format!( + "\n[[files]]\nsource = {:?}\nrelative = {:?}\nsha256 = {:?}\nexecutable = false\n", + file.display().to_string(), + relative, + sha256, + )); + } + fs::write(&manifest_path, text).expect("write WASIX AOT Cargo artifact manifest"); + println!("cargo::metadata=manifest={}", manifest_path.display()); +} + +fn sha256_file(path: &Path) -> io::Result { + let mut file = fs::File::open(path)?; + let mut hasher = Sha256::new(); + let mut buffer = [0u8; 128 * 1024]; + loop { + let read = file.read(&mut buffer)?; + if read == 0 { + break; + } + hasher.update(&buffer[..read]); + } + Ok(format!("{:x}", hasher.finalize())) +} diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/src/lib.rs b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/src/lib.rs new file mode 100644 index 00000000..edcddc24 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/src/lib.rs @@ -0,0 +1,3 @@ +#![deny(unsafe_code)] + +include!(concat!(env!("OUT_DIR"), "/generated_aot.rs")); diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml b/src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml new file mode 100644 index 00000000..d9c4c6ad --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "oliphaunt-wasix-tools" +version = "0.1.0" +edition = "2024" +rust-version = "1.93" +description = "Internal Oliphaunt WASIX PostgreSQL tool assets" +repository = "https://github.com/f0rr0/oliphaunt" +homepage = "https://oliphaunt.dev" +documentation = "https://docs.rs/oliphaunt-wasix-tools" +license = "MIT AND Apache-2.0 AND PostgreSQL" +publish = false +links = "oliphaunt_artifact_oliphaunt_wasix_tools" +include = [ + "Cargo.toml", + "build.rs", + "README.md", + "src/**", + "payload/**", +] + +[lib] +path = "src/lib.rs" + +[build-dependencies] +sha2 = "0.10" diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools/README.md b/src/runtimes/liboliphaunt/wasix/crates/tools/README.md new file mode 100644 index 00000000..7f1ceb6f --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix/crates/tools/README.md @@ -0,0 +1,5 @@ +# oliphaunt-wasix-tools + +Cargo artifact crate for Oliphaunt WASIX PostgreSQL command-line tools. +Applications do not depend on this crate directly; SDK crates select it when +they need the WASIX `pg_dump` or `psql` modules. diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools/build.rs b/src/runtimes/liboliphaunt/wasix/crates/tools/build.rs new file mode 100644 index 00000000..460854b9 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix/crates/tools/build.rs @@ -0,0 +1,169 @@ +use std::env; +use std::fs; +use std::io::{self, Read}; +use std::path::{Path, PathBuf}; + +use sha2::{Digest, Sha256}; + +const ARTIFACT_SCHEMA: &str = "oliphaunt-artifact-manifest-v1"; +const ARTIFACT_PRODUCT: &str = "oliphaunt-wasix-tools"; +const ARTIFACT_KIND: &str = "wasix-tools"; +const ARTIFACT_TARGET: &str = "portable"; + +fn main() { + println!("cargo:rerun-if-env-changed=OLIPHAUNT_WASM_GENERATED_ASSETS_DIR"); + + let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set by Cargo")); + let out = out_dir.join("generated_tools.rs"); + if let Some(asset_dir) = find_asset_dir() { + emit_rerun_directives(&asset_dir); + write_generated_tools(&out, &asset_dir); + } else if env::var_os("OLIPHAUNT_ARTIFACT_CRATE_REQUIRE_PAYLOAD").is_some() { + panic!("release packaging requires package-local WASIX tools payload"); + } else { + write_source_only_tools(&out); + } +} + +fn find_asset_dir() -> Option { + let manifest_dir = + PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set")); + let package_payload = manifest_dir.join("payload"); + if package_payload.join("bin/pg_dump.wasix.wasm").is_file() + && package_payload.join("bin/psql.wasix.wasm").is_file() + { + return Some(package_payload); + } + + if let Some(path) = env::var_os("OLIPHAUNT_WASM_GENERATED_ASSETS_DIR") { + let path = PathBuf::from(path); + if path.join("bin/pg_dump.wasix.wasm").is_file() + && path.join("bin/psql.wasix.wasm").is_file() + { + return Some(path); + } + } + + if let Some(repo_root) = repo_root_from_manifest_dir(&manifest_dir) { + let target_assets = repo_root.join("target/oliphaunt-wasix/assets"); + if target_assets.join("bin/pg_dump.wasix.wasm").is_file() + && target_assets.join("bin/psql.wasix.wasm").is_file() + { + return Some(target_assets); + } + } + None +} + +fn repo_root_from_manifest_dir(manifest_dir: &Path) -> Option { + for ancestor in manifest_dir.ancestors() { + if ancestor.join(".git").exists() && ancestor.join("Cargo.toml").is_file() { + return Some(ancestor.to_path_buf()); + } + } + None +} + +fn emit_rerun_directives(asset_dir: &Path) { + println!("cargo:rerun-if-changed={}", asset_dir.display()); + visit_files(asset_dir, &mut |path| { + println!("cargo:rerun-if-changed={}", path.display()); + }); +} + +fn visit_files(path: &Path, f: &mut impl FnMut(&Path)) { + let Ok(entries) = fs::read_dir(path) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + visit_files(&path, f); + } else if path.is_file() { + f(&path); + } + } +} + +fn write_generated_tools(out: &Path, asset_dir: &Path) { + let pg_dump = asset_dir.join("bin/pg_dump.wasix.wasm"); + let psql = asset_dir.join("bin/psql.wasix.wasm"); + for required in [&pg_dump, &psql] { + assert!( + required.is_file(), + "generated WASIX tools directory {} is missing required file {}", + asset_dir.display(), + required.display() + ); + } + let text = format!( + "pub const HAS_EMBEDDED_TOOLS: bool = true;\n\ + pub fn pg_dump_wasm() -> Option<&'static [u8]> {{ Some(include_bytes!({pg_dump})) }}\n\ + pub fn psql_wasm() -> Option<&'static [u8]> {{ Some(include_bytes!({psql})) }}\n", + pg_dump = rust_string_literal(&pg_dump), + psql = rust_string_literal(&psql), + ); + fs::write(out, text).expect("write generated WASIX tool include module"); + emit_artifact_manifest( + out.parent().expect("generated tool output has parent"), + asset_dir, + &[&pg_dump, &psql], + ); +} + +fn write_source_only_tools(out: &Path) { + fs::write( + out, + "pub const HAS_EMBEDDED_TOOLS: bool = false;\n\ + pub fn pg_dump_wasm() -> Option<&'static [u8]> { None }\n\ + pub fn psql_wasm() -> Option<&'static [u8]> { None }\n", + ) + .expect("write source-only WASIX tool include module"); +} + +fn rust_string_literal(path: &Path) -> String { + format!("{:?}", path.to_string_lossy()) +} + +fn emit_artifact_manifest(out_dir: &Path, asset_dir: &Path, files: &[&Path]) { + let version = env::var("CARGO_PKG_VERSION").expect("CARGO_PKG_VERSION is set by Cargo"); + let manifest_path = out_dir.join("oliphaunt-artifact.toml"); + let mut text = format!( + "schema = {ARTIFACT_SCHEMA:?}\nproduct = {ARTIFACT_PRODUCT:?}\nversion = {version:?}\nkind = {ARTIFACT_KIND:?}\ntarget = {ARTIFACT_TARGET:?}\n" + ); + for file in files { + let relative = file + .strip_prefix(asset_dir) + .ok() + .map(|path| path.to_string_lossy().replace('\\', "/")) + .unwrap_or_else(|| { + file.file_name() + .unwrap_or_default() + .to_string_lossy() + .into_owned() + }); + let sha256 = sha256_file(file).expect("hash WASIX tools artifact file"); + text.push_str(&format!( + "\n[[files]]\nsource = {:?}\nrelative = {:?}\nsha256 = {:?}\nexecutable = false\n", + file.display().to_string(), + relative, + sha256, + )); + } + fs::write(&manifest_path, text).expect("write WASIX tools Cargo artifact manifest"); + println!("cargo::metadata=manifest={}", manifest_path.display()); +} + +fn sha256_file(path: &Path) -> io::Result { + let mut file = fs::File::open(path)?; + let mut hasher = Sha256::new(); + let mut buffer = [0u8; 128 * 1024]; + loop { + let read = file.read(&mut buffer)?; + if read == 0 { + break; + } + hasher.update(&buffer[..read]); + } + Ok(format!("{:x}", hasher.finalize())) +} diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools/src/lib.rs b/src/runtimes/liboliphaunt/wasix/crates/tools/src/lib.rs new file mode 100644 index 00000000..a159d584 --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix/crates/tools/src/lib.rs @@ -0,0 +1,3 @@ +#![deny(unsafe_code)] + +include!(concat!(env!("OUT_DIR"), "/generated_tools.rs")); diff --git a/src/runtimes/liboliphaunt/wasix/release.toml b/src/runtimes/liboliphaunt/wasix/release.toml index 38e916e9..a286b4f2 100644 --- a/src/runtimes/liboliphaunt/wasix/release.toml +++ b/src/runtimes/liboliphaunt/wasix/release.toml @@ -5,10 +5,15 @@ publish_targets = ["github-release-assets", "crates-io"] registry_packages = [ "crates:oliphaunt-icu", "crates:liboliphaunt-wasix-portable", + "crates:oliphaunt-wasix-tools", "crates:liboliphaunt-wasix-aot-aarch64-apple-darwin", "crates:liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu", "crates:liboliphaunt-wasix-aot-x86_64-pc-windows-msvc", "crates:liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", + "crates:oliphaunt-wasix-tools-aot-aarch64-apple-darwin", + "crates:oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu", + "crates:oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc", + "crates:oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu", ] release_artifacts = [ "release-assets", diff --git a/src/sdks/rust/crates/oliphaunt-build/src/lib.rs b/src/sdks/rust/crates/oliphaunt-build/src/lib.rs index 6b91db34..8065fea3 100644 --- a/src/sdks/rust/crates/oliphaunt-build/src/lib.rs +++ b/src/sdks/rust/crates/oliphaunt-build/src/lib.rs @@ -227,6 +227,14 @@ fn select_artifacts( target, "selected native runtime", )?); + selected.push(require_artifact( + artifacts, + "oliphaunt-tools", + Some(&metadata.runtime_version), + ArtifactKind::NativeTools, + target, + "selected native tools", + )?); selected.push(require_artifact( artifacts, "oliphaunt-broker", @@ -245,6 +253,14 @@ fn select_artifacts( "portable", "selected WASIX portable runtime", )?); + selected.push(require_artifact( + artifacts, + "oliphaunt-wasix-tools", + Some(&metadata.runtime_version), + ArtifactKind::WasixTools, + "portable", + "selected WASIX tools", + )?); selected.push(require_artifact( artifacts, "liboliphaunt-wasix", @@ -253,6 +269,14 @@ fn select_artifacts( target, "selected WASIX AOT runtime", )?); + selected.push(require_artifact( + artifacts, + "oliphaunt-wasix-tools", + Some(&metadata.runtime_version), + ArtifactKind::WasixToolsAot, + target, + "selected WASIX tools AOT runtime", + )?); } other => { return Err(Error::new(format!( @@ -585,8 +609,11 @@ impl ArtifactManifest { #[serde(rename_all = "kebab-case")] enum ArtifactKind { NativeRuntime, + NativeTools, WasixRuntime, + WasixTools, WasixAot, + WasixToolsAot, BrokerHelper, IcuData, Extension, @@ -596,8 +623,11 @@ impl ArtifactKind { fn as_str(self) -> &'static str { match self { Self::NativeRuntime => "native-runtime", + Self::NativeTools => "native-tools", Self::WasixRuntime => "wasix-runtime", + Self::WasixTools => "wasix-tools", Self::WasixAot => "wasix-aot", + Self::WasixToolsAot => "wasix-tools-aot", Self::BrokerHelper => "broker-helper", Self::IcuData => "icu-data", Self::Extension => "extension", @@ -752,6 +782,16 @@ icu = true None, "runtime/bin/postgres", ); + let tools_manifest = write_artifact_manifest( + &temp, + "tools.toml", + "oliphaunt-tools", + "0.1.0", + "native-tools", + "x86_64-unknown-linux-gnu", + None, + "runtime/bin/pg_dump", + ); let broker_manifest = write_artifact_manifest( &temp, "broker.toml", @@ -766,7 +806,7 @@ icu = true manifest_dir: temp.path().to_path_buf(), out_dir: temp.path().join("out"), target: "x86_64-unknown-linux-gnu".to_owned(), - artifact_manifest_paths: vec![runtime_manifest, broker_manifest], + artifact_manifest_paths: vec![runtime_manifest, tools_manifest, broker_manifest], }; let error = context .configure() @@ -794,11 +834,21 @@ runtime-version = "0.1.0" None, "runtime/bin/postgres", ); + let tools_manifest = write_artifact_manifest( + &temp, + "tools.toml", + "oliphaunt-tools", + "0.1.0", + "native-tools", + "x86_64-unknown-linux-gnu", + None, + "runtime/bin/pg_dump", + ); let context = BuildContext { manifest_dir: temp.path().to_path_buf(), out_dir: temp.path().join("out"), target: "x86_64-unknown-linux-gnu".to_owned(), - artifact_manifest_paths: vec![runtime_manifest], + artifact_manifest_paths: vec![runtime_manifest, tools_manifest], }; let error = context .configure() @@ -828,6 +878,16 @@ icu = true None, "runtime/bin/postgres", ); + let tools_manifest = write_artifact_manifest( + &temp, + "tools.toml", + "oliphaunt-tools", + "1.2.0", + "native-tools", + "x86_64-unknown-linux-gnu", + None, + "runtime/bin/pg_dump", + ); let broker_manifest = write_artifact_manifest( &temp, "broker.toml", @@ -864,6 +924,7 @@ icu = true target: "x86_64-unknown-linux-gnu".to_owned(), artifact_manifest_paths: vec![ runtime_manifest, + tools_manifest, broker_manifest, icu_manifest, extension_manifest, @@ -877,6 +938,7 @@ icu = true let lock = fs::read_to_string(output.lock_file).unwrap(); assert!(lock.contains("product = \"liboliphaunt-native\"")); assert!(lock.contains("version = \"1.2.0\"")); + assert!(lock.contains("product = \"oliphaunt-tools\"")); assert!(lock.contains("product = \"oliphaunt-broker\"")); assert!(lock.contains("version = \"2.0.0\"")); assert!(lock.contains("product = \"oliphaunt-icu\"")); @@ -904,6 +966,16 @@ runtime-version = "0.1.0" None, "runtime/bin/postgres", ); + let tools_manifest = write_artifact_manifest( + &temp, + "tools.toml", + "oliphaunt-tools", + "0.1.0", + "native-tools", + "x86_64-unknown-linux-gnu", + None, + "runtime/bin/pg_dump", + ); let broker_manifest = write_artifact_manifest( &temp, "broker.toml", @@ -928,7 +1000,12 @@ runtime-version = "0.1.0" manifest_dir: temp.path().to_path_buf(), out_dir: temp.path().join("out"), target: "x86_64-unknown-linux-gnu".to_owned(), - artifact_manifest_paths: vec![runtime_manifest, broker_manifest, extension_manifest], + artifact_manifest_paths: vec![ + runtime_manifest, + tools_manifest, + broker_manifest, + extension_manifest, + ], }; let error = context .configure() @@ -956,6 +1033,16 @@ extensions = ["vector"] None, "runtime/bin/postgres", ); + let tools_manifest = write_artifact_manifest( + &temp, + "tools.toml", + "oliphaunt-tools", + "0.1.0", + "native-tools", + "x86_64-unknown-linux-gnu", + None, + "runtime/bin/pg_dump", + ); let broker_manifest = write_artifact_manifest( &temp, "broker.toml", @@ -980,7 +1067,12 @@ extensions = ["vector"] manifest_dir: temp.path().to_path_buf(), out_dir: temp.path().join("out"), target: "x86_64-unknown-linux-gnu".to_owned(), - artifact_manifest_paths: vec![runtime_manifest, broker_manifest, extension_manifest], + artifact_manifest_paths: vec![ + runtime_manifest, + tools_manifest, + broker_manifest, + extension_manifest, + ], }; let output = context @@ -993,6 +1085,12 @@ extensions = ["vector"] .join("native-runtime/liboliphaunt-native/runtime/bin/postgres") .is_file() ); + assert!( + output + .resources_dir + .join("native-tools/oliphaunt-tools/runtime/bin/pg_dump") + .is_file() + ); assert!( output .resources_dir @@ -1033,6 +1131,16 @@ runtime-version = "0.1.0" None, "runtime/bin/postgres", ); + let tools_manifest = write_artifact_manifest( + &temp, + "tools.toml", + "oliphaunt-tools", + "0.1.0", + "native-tools", + "x86_64-unknown-linux-gnu", + None, + "runtime/bin/pg_dump", + ); let broker_manifest = write_artifact_manifest( &temp, "broker.toml", @@ -1051,7 +1159,7 @@ runtime-version = "0.1.0" manifest_dir: temp.path().to_path_buf(), out_dir, target: "x86_64-unknown-linux-gnu".to_owned(), - artifact_manifest_paths: vec![runtime_manifest, broker_manifest], + artifact_manifest_paths: vec![runtime_manifest, tools_manifest, broker_manifest], }; let output = context.configure().expect("selected runtime should stage"); diff --git a/src/sdks/rust/src/liboliphaunt/root.rs b/src/sdks/rust/src/liboliphaunt/root.rs index bb1012fa..e04dc017 100644 --- a/src/sdks/rust/src/liboliphaunt/root.rs +++ b/src/sdks/rust/src/liboliphaunt/root.rs @@ -22,8 +22,8 @@ use crate::extension::Extension; use crate::storage::DatabaseRoot; static ACTIVE_ROOTS: OnceLock>> = OnceLock::new(); -pub(super) const NATIVE_RUNTIME_TOOLS: [&str; 5] = - ["postgres", "initdb", "pg_ctl", "pg_dump", "psql"]; +pub(super) const NATIVE_RUNTIME_TOOLS: [&str; 3] = ["postgres", "initdb", "pg_ctl"]; +pub(super) const NATIVE_TOOLS_PACKAGE_TOOLS: [&str; 2] = ["pg_dump", "psql"]; pub(crate) struct MaterializedNativeResources { pub(crate) runtime_dir: PathBuf, diff --git a/src/sdks/rust/src/liboliphaunt/root/runtime.rs b/src/sdks/rust/src/liboliphaunt/root/runtime.rs index 272fd9ad..4b9a8eeb 100644 --- a/src/sdks/rust/src/liboliphaunt/root/runtime.rs +++ b/src/sdks/rust/src/liboliphaunt/root/runtime.rs @@ -12,7 +12,9 @@ use fs2::FileExt; use cache_key::{cached_runtime_is_valid, runtime_cache_key, runtime_cache_manifest}; use install::install_cached_runtime; -use locate::{locate_native_embedded_modules_dir, locate_native_install_dir}; +use locate::{ + locate_native_embedded_modules_dir, locate_native_install_dir, locate_native_tools_dir, +}; use super::NativeRuntimeProfile; use crate::error::{Error, Result}; @@ -25,6 +27,7 @@ pub(super) fn materialize_runtime( extensions: &[Extension], ) -> Result { let install_dir = locate_native_install_dir()?; + let tools_dir = locate_native_tools_dir(&install_dir); let embedded_modules = if profile.needs_embedded_modules() { Some(locate_native_embedded_modules_dir(&install_dir)?) } else { @@ -33,6 +36,7 @@ pub(super) fn materialize_runtime( let key = runtime_cache_key( profile, &install_dir, + tools_dir.as_deref(), embedded_modules.as_deref(), extensions, )?; @@ -96,6 +100,7 @@ pub(super) fn materialize_runtime( let build_result = install_cached_runtime( profile, &install_dir, + tools_dir.as_deref(), embedded_modules.as_deref(), &build_dir, extensions, diff --git a/src/sdks/rust/src/liboliphaunt/root/runtime/cache_key.rs b/src/sdks/rust/src/liboliphaunt/root/runtime/cache_key.rs index d4111791..7b131dbe 100644 --- a/src/sdks/rust/src/liboliphaunt/root/runtime/cache_key.rs +++ b/src/sdks/rust/src/liboliphaunt/root/runtime/cache_key.rs @@ -12,15 +12,18 @@ use super::super::fingerprint::{ fingerprint_named_extension_sql_files, fingerprint_optional_file, hash_path, hash_str, new_state, }; -use super::super::{NATIVE_RUNTIME_TOOLS, existing_native_tool_path, native_tool_path}; +use super::super::{ + NATIVE_RUNTIME_TOOLS, NATIVE_TOOLS_PACKAGE_TOOLS, existing_native_tool_path, native_tool_path, +}; use crate::error::{Error, Result}; use crate::extension::Extension; -const RUNTIME_CACHE_VERSION: &str = "pg18-runtime-cache-v4"; +const RUNTIME_CACHE_VERSION: &str = "pg18-runtime-cache-v5"; pub(super) fn runtime_cache_key( profile: NativeRuntimeProfile, install_dir: &Path, + tools_dir: Option<&Path>, embedded_modules: Option<&Path>, extensions: &[Extension], ) -> Result { @@ -28,6 +31,12 @@ pub(super) fn runtime_cache_key( hash_str(&mut state, RUNTIME_CACHE_VERSION); hash_str(&mut state, profile.cache_id()); hash_path(&mut state, &canonical_or_original(install_dir)); + if let Some(tools_dir) = tools_dir { + hash_str(&mut state, "native-tools"); + hash_path(&mut state, &canonical_or_original(tools_dir)); + } else { + hash_str(&mut state, "native-tools:none"); + } if let Some(embedded_modules) = embedded_modules { hash_path(&mut state, &canonical_or_original(embedded_modules)); } @@ -44,6 +53,14 @@ pub(super) fn runtime_cache_key( &existing_native_tool_path(install_dir, tool), )?; } + let tools_dir = tools_dir.unwrap_or(install_dir); + for tool in NATIVE_TOOLS_PACKAGE_TOOLS { + fingerprint_optional_file( + &mut state, + tools_dir, + &existing_native_tool_path(tools_dir, tool), + )?; + } let source_share = install_dir.join("share/postgresql"); fingerprint_directory_filtered(&mut state, &source_share, &source_share, core_share_file)?; @@ -114,6 +131,7 @@ pub(super) fn cached_runtime_is_valid( if !cache_dir.join(".complete").is_file() || !native_tool_path(cache_dir, "postgres").is_file() || !native_tool_path(cache_dir, "initdb").is_file() + || !native_tool_path(cache_dir, "pg_ctl").is_file() || !cache_dir .join("share/postgresql/postgresql.conf.sample") .is_file() @@ -232,6 +250,7 @@ mod tests { NativeRuntimeProfile::PostgresServer, &install_dir, None, + None, &[Extension::Hstore], ) .expect("create first runtime cache key"); @@ -244,6 +263,7 @@ mod tests { NativeRuntimeProfile::PostgresServer, &install_dir, None, + None, &[Extension::Hstore], ) .expect("create SQL-mutated runtime cache key"); @@ -262,6 +282,7 @@ mod tests { NativeRuntimeProfile::PostgresServer, &install_dir, None, + None, &[Extension::Hstore], ) .expect("create module-mutated runtime cache key"); @@ -281,6 +302,7 @@ mod tests { NativeRuntimeProfile::PostgresServer, &install_dir, None, + None, &[], ) .expect("create first runtime cache key"); @@ -304,6 +326,7 @@ mod tests { NativeRuntimeProfile::PostgresServer, &install_dir, None, + None, &[], ) .expect("create second runtime cache key"); @@ -331,6 +354,7 @@ mod tests { NativeRuntimeProfile::PostgresServer, &install_dir, None, + None, &[], ) .expect("create first ICU runtime cache key"); @@ -343,6 +367,7 @@ mod tests { NativeRuntimeProfile::PostgresServer, &install_dir, None, + None, &[], ) .expect("create changed ICU runtime cache key"); @@ -450,6 +475,7 @@ mod tests { ); write_file(&cache_dir.join("bin/postgres"), b"postgres"); write_file(&cache_dir.join("bin/initdb"), b"initdb"); + write_file(&cache_dir.join("bin/pg_ctl"), b"pg_ctl"); write_file( &cache_dir.join("share/postgresql/postgresql.conf.sample"), b"# sample\n", diff --git a/src/sdks/rust/src/liboliphaunt/root/runtime/install.rs b/src/sdks/rust/src/liboliphaunt/root/runtime/install.rs index e9bfc458..1c6ac577 100644 --- a/src/sdks/rust/src/liboliphaunt/root/runtime/install.rs +++ b/src/sdks/rust/src/liboliphaunt/root/runtime/install.rs @@ -10,13 +10,16 @@ use super::super::extensions::{ use super::super::files::{ copy_directory_filtered, copy_file_preserving_permissions, remove_file_if_exists, }; -use super::super::{NATIVE_RUNTIME_TOOLS, existing_native_tool_path, native_tool_path}; +use super::super::{ + NATIVE_RUNTIME_TOOLS, NATIVE_TOOLS_PACKAGE_TOOLS, existing_native_tool_path, native_tool_path, +}; use crate::error::{Error, Result}; use crate::extension::Extension; pub(super) fn install_cached_runtime( profile: NativeRuntimeProfile, install_dir: &Path, + tools_dir: Option<&Path>, embedded_modules: Option<&Path>, runtime_dir: &Path, extensions: &[Extension], @@ -34,6 +37,13 @@ pub(super) fn install_cached_runtime( install_runtime_tool(&source, &native_tool_path(runtime_dir, tool))?; } } + let tools_dir = tools_dir.unwrap_or(install_dir); + for tool in NATIVE_TOOLS_PACKAGE_TOOLS { + let source = existing_native_tool_path(tools_dir, tool); + if source.is_file() { + install_runtime_tool(&source, &native_tool_path(runtime_dir, tool))?; + } + } install_native_share_tree(install_dir, runtime_dir, extensions)?; install_native_library_tree( @@ -132,6 +142,7 @@ mod tests { NativeRuntimeProfile::PostgresServer, &install_dir, None, + None, &temp.path().join("runtime"), &extensions, ) @@ -159,6 +170,7 @@ mod tests { NativeRuntimeProfile::PostgresServer, &install_dir, None, + None, &runtime_dir, &[Extension::Vector], ) @@ -200,7 +212,10 @@ mod tests { let runtime_dir = temp.path().join("runtime"); write_minimal_install(&install_dir); write_file(&install_dir.join("bin/initdb"), b"initdb"); - for tool in ["postgres", "initdb"] { + write_file(&install_dir.join("bin/pg_ctl"), b"pg_ctl"); + write_file(&install_dir.join("bin/pg_dump"), b"pg_dump"); + write_file(&install_dir.join("bin/psql"), b"psql"); + for tool in ["postgres", "initdb", "pg_ctl", "pg_dump", "psql"] { fs::set_permissions( install_dir.join("bin").join(tool), fs::Permissions::from_mode(0o644), @@ -212,12 +227,13 @@ mod tests { NativeRuntimeProfile::PostgresServer, &install_dir, None, + None, &runtime_dir, &[], ) .unwrap(); - for tool in ["postgres", "initdb"] { + for tool in ["postgres", "initdb", "pg_ctl", "pg_dump", "psql"] { let mode = fs::metadata(runtime_dir.join("bin").join(tool)) .expect("stat copied runtime tool") .permissions() @@ -249,6 +265,7 @@ mod tests { NativeRuntimeProfile::PostgresServer, &install_dir, None, + None, &runtime_dir, &[], ) @@ -275,6 +292,7 @@ mod tests { NativeRuntimeProfile::PostgresServer, &install_dir, None, + None, &runtime_dir, &[], ) @@ -282,6 +300,38 @@ mod tests { assert!(!runtime_dir.join("share/icu").exists()); } + #[test] + fn install_copies_sidecar_native_tools_into_runtime_cache() { + let temp = TempTree::new("sidecar-tools"); + let install_dir = temp.path().join("install"); + let tools_dir = temp.path().join("tools"); + let runtime_dir = temp.path().join("runtime"); + write_minimal_install(&install_dir); + write_file(&install_dir.join("bin/initdb"), b"initdb"); + write_file(&install_dir.join("bin/pg_ctl"), b"pg_ctl"); + write_file(&tools_dir.join("bin/pg_dump"), b"pg_dump-from-tools"); + write_file(&tools_dir.join("bin/psql"), b"psql-from-tools"); + + install_cached_runtime( + NativeRuntimeProfile::PostgresServer, + &install_dir, + Some(&tools_dir), + None, + &runtime_dir, + &[], + ) + .unwrap(); + + assert_eq!( + fs::read(runtime_dir.join("bin/pg_dump")).unwrap(), + b"pg_dump-from-tools" + ); + assert_eq!( + fs::read(runtime_dir.join("bin/psql")).unwrap(), + b"psql-from-tools" + ); + } + struct TempTree { path: PathBuf, } @@ -313,6 +363,8 @@ mod tests { fn write_minimal_install(install_dir: &Path) { write_file(&install_dir.join("bin/postgres"), b"postgres"); + write_file(&install_dir.join("bin/initdb"), b"initdb"); + write_file(&install_dir.join("bin/pg_ctl"), b"pg_ctl"); write_file( &install_dir.join("share/postgresql/postgresql.conf.sample"), b"# sample\n", diff --git a/src/sdks/rust/src/liboliphaunt/root/runtime/locate.rs b/src/sdks/rust/src/liboliphaunt/root/runtime/locate.rs index 9f388bac..1913eeb9 100644 --- a/src/sdks/rust/src/liboliphaunt/root/runtime/locate.rs +++ b/src/sdks/rust/src/liboliphaunt/root/runtime/locate.rs @@ -6,9 +6,15 @@ use super::super::super::ffi::{ }; use crate::error::{Error, Result}; +const ENV_RESOURCES_DIR: &str = "OLIPHAUNT_RESOURCES_DIR"; +const ENV_TOOLS_DIR: &str = "OLIPHAUNT_TOOLS_DIR"; + pub(super) fn locate_native_install_dir() -> Result { let mut candidates = Vec::new(); candidates.extend(env_path_candidates([ENV_INSTALL_DIR])); + if let Some(path) = std::env::var_os(ENV_RESOURCES_DIR) { + candidates.push(PathBuf::from(path).join("native-runtime/liboliphaunt-native/runtime")); + } for env_name in [ENV_POSTGRES, ENV_INITDB] { if let Some(path) = std::env::var_os(env_name) { let path = PathBuf::from(path); @@ -40,6 +46,18 @@ pub(super) fn locate_native_install_dir() -> Result { ))) } +pub(super) fn locate_native_tools_dir(install_dir: &Path) -> Option { + let mut candidates = Vec::new(); + candidates.extend(env_path_candidates([ENV_TOOLS_DIR])); + if let Some(path) = std::env::var_os(ENV_RESOURCES_DIR) { + candidates.push(PathBuf::from(path).join("native-tools/oliphaunt-tools/runtime")); + } + candidates.push(install_dir.to_path_buf()); + candidates + .into_iter() + .find(|candidate| native_tools_dir_is_valid(candidate)) +} + pub(super) fn locate_native_embedded_modules_dir(install_dir: &Path) -> Result { locate_native_embedded_modules_dir_from_libraries( install_dir, @@ -85,12 +103,18 @@ fn locate_native_embedded_modules_dir_from_libraries( fn native_install_dir_is_valid(path: &Path) -> bool { native_tool_is_file(path, "postgres") + && native_tool_is_file(path, "initdb") + && native_tool_is_file(path, "pg_ctl") && path .join("share/postgresql/postgresql.conf.sample") .is_file() && path.join("lib/postgresql").is_dir() } +fn native_tools_dir_is_valid(path: &Path) -> bool { + native_tool_is_file(path, "pg_dump") && native_tool_is_file(path, "psql") +} + fn native_tool_is_file(path: &Path, tool: &str) -> bool { path.join("bin").join(tool).is_file() || path.join("bin").join(format!("{tool}.exe")).is_file() } @@ -110,12 +134,20 @@ fn native_host_target_id() -> Option<&'static str> { mod tests { use std::fs; use std::path::{Path, PathBuf}; + use std::sync::{Mutex, OnceLock}; use std::time::{SystemTime, UNIX_EPOCH}; use super::*; + static ENV_LOCK: OnceLock> = OnceLock::new(); + #[test] fn embedded_modules_locator_accepts_release_lib_modules_next_to_dll() { + let _guard = ENV_LOCK.get_or_init(|| Mutex::new(())).lock().unwrap(); + let previous = std::env::var_os(ENV_EMBEDDED_MODULE_DIR); + unsafe { + std::env::remove_var(ENV_EMBEDDED_MODULE_DIR); + } let temp = TempTree::new("release-lib-modules"); let release_root = temp.path().join("liboliphaunt-0.0.0-windows-x64-msvc"); let install_dir = release_root.join("runtime"); @@ -130,11 +162,13 @@ mod tests { ) .expect("locate release modules"); + restore_env(ENV_EMBEDDED_MODULE_DIR, previous); assert_eq!(located, modules_dir); } #[test] fn embedded_modules_locator_prefers_explicit_environment_dir() { + let _guard = ENV_LOCK.get_or_init(|| Mutex::new(())).lock().unwrap(); let temp = TempTree::new("explicit-env-modules"); let install_dir = temp.path().join("runtime"); let modules_dir = temp.path().join("registry/modules"); @@ -151,15 +185,19 @@ mod tests { ) .expect("locate env modules"); + restore_env(ENV_EMBEDDED_MODULE_DIR, previous); + assert_eq!(located, modules_dir); + } + + fn restore_env(name: &str, previous: Option) { match previous { Some(value) => unsafe { - std::env::set_var(ENV_EMBEDDED_MODULE_DIR, value); + std::env::set_var(name, value); }, None => unsafe { - std::env::remove_var(ENV_EMBEDDED_MODULE_DIR); + std::env::remove_var(name); }, } - assert_eq!(located, modules_dir); } struct TempTree { diff --git a/src/sdks/rust/tools/check-sdk.sh b/src/sdks/rust/tools/check-sdk.sh index eb784d43..95521be8 100755 --- a/src/sdks/rust/tools/check-sdk.sh +++ b/src/sdks/rust/tools/check-sdk.sh @@ -239,9 +239,12 @@ fn main() { let lock = fs::read_to_string(&output.lock_file).expect("staged Oliphaunt lockfile is readable"); assert!(lock.contains("product = \"liboliphaunt-native\"")); assert!(lock.contains("kind = \"native-runtime\"")); + assert!(lock.contains("product = \"oliphaunt-tools\"")); + assert!(lock.contains("kind = \"native-tools\"")); assert!(lock.contains("product = \"oliphaunt-broker\"")); assert!(lock.contains("kind = \"broker-helper\"")); assert!(output.resources_dir.join("native-runtime/liboliphaunt-native").is_dir()); + assert!(output.resources_dir.join("native-tools/oliphaunt-tools").is_dir()); assert!(output.resources_dir.join("broker-helper/oliphaunt-broker").is_dir()); for instruction in output.cargo_instructions { println!("{instruction}"); diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 7f581347..bb99e112 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -336,6 +336,10 @@ def check_liboliphaunt(findings: list[Finding]) -> None: "crates:liboliphaunt-native-linux-x64-gnu", "crates:liboliphaunt-native-macos-arm64", "crates:liboliphaunt-native-windows-x64-msvc", + "crates:oliphaunt-tools-linux-arm64-gnu", + "crates:oliphaunt-tools-linux-x64-gnu", + "crates:oliphaunt-tools-macos-arm64", + "crates:oliphaunt-tools-windows-x64-msvc", "npm:@oliphaunt/icu", "npm:@oliphaunt/liboliphaunt-darwin-arm64", "npm:@oliphaunt/liboliphaunt-linux-x64-gnu", @@ -1372,6 +1376,7 @@ def check_wasm(findings: list[Finding]) -> None: dependencies = manifest.get("dependencies", {}) target_tables = manifest.get("target", {}) expected_runtime_dependency = dependencies.get("liboliphaunt-wasix-portable") + expected_tools_dependency = dependencies.get("oliphaunt-wasix-tools") require( findings, product, @@ -1382,14 +1387,30 @@ def check_wasm(findings: list[Finding]) -> None: f"liboliphaunt-wasix-portable dependency={expected_runtime_dependency!r}", severity="P0", ) + require( + findings, + product, + "wasm-tools-artifact-dependency", + isinstance(expected_tools_dependency, dict) + and expected_tools_dependency.get("version") == f"={runtime_version}", + "WASM crate must depend on the public WASIX tools artifact crate at the liboliphaunt-wasix version.", + f"oliphaunt-wasix-tools dependency={expected_tools_dependency!r}", + severity="P0", + ) expected_aot_dependencies = { 'cfg(all(target_os = "macos", target_arch = "aarch64"))': "liboliphaunt-wasix-aot-aarch64-apple-darwin", 'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))': "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", 'cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))': "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu", 'cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))': "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc", } + expected_tools_aot_dependencies = { + 'cfg(all(target_os = "macos", target_arch = "aarch64"))': "oliphaunt-wasix-tools-aot-aarch64-apple-darwin", + 'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))': "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu", + 'cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))': "oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu", + 'cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))': "oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc", + } missing_aot_dependencies = [] - for cfg, crate in expected_aot_dependencies.items(): + for cfg, crate in {**expected_aot_dependencies, **expected_tools_aot_dependencies}.items(): target = target_tables.get(cfg) target_dependencies = target.get("dependencies", {}) if isinstance(target, dict) else {} dependency = target_dependencies.get(crate) @@ -1400,7 +1421,7 @@ def check_wasm(findings: list[Finding]) -> None: product, "wasm-aot-artifact-dependencies", not missing_aot_dependencies, - "WASM crate must depend on every public target-specific AOT artifact crate behind exact Cargo target cfgs.", + "WASM crate must depend on every public target-specific root/tools AOT artifact crate behind exact Cargo target cfgs.", missing_aot_dependencies or "src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml", severity="P0", ) @@ -1428,7 +1449,7 @@ def check_wasm(findings: list[Finding]) -> None: and package.get("build") == "build.rs" and "DEP_OLIPHAUNT_ARTIFACT_" in relay_source and "cargo::metadata=" in relay_source, - "WASM crate must relay Cargo-resolved runtime/AOT artifact manifests through Cargo links metadata.", + "WASM crate must relay Cargo-resolved runtime/tool/AOT artifact manifests through Cargo links metadata.", "src/bindings/wasix-rust/crates/oliphaunt-wasix/build.rs", severity="P0", ) @@ -1484,6 +1505,8 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: ) asset_manifest = read_toml("src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml") asset_package = asset_manifest.get("package", {}) + tools_manifest = read_toml("src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml") + tools_package = tools_manifest.get("package", {}) require( findings, product, @@ -1494,6 +1517,16 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: f"src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml package={asset_package!r}", severity="P0", ) + require( + findings, + product, + "wasix-tools-crate", + tools_package.get("name") == "oliphaunt-wasix-tools" + and tools_package.get("version") == product_metadata.read_current_version(product), + "WASIX tools asset crate must publish under the runtime product version.", + f"src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml package={tools_package!r}", + severity="P0", + ) require( findings, product, @@ -1507,17 +1540,22 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: expected_registry_packages = { "crates:oliphaunt-icu", "crates:liboliphaunt-wasix-portable", + "crates:oliphaunt-wasix-tools", "crates:liboliphaunt-wasix-aot-aarch64-apple-darwin", "crates:liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu", "crates:liboliphaunt-wasix-aot-x86_64-pc-windows-msvc", "crates:liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", + "crates:oliphaunt-wasix-tools-aot-aarch64-apple-darwin", + "crates:oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu", + "crates:oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc", + "crates:oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu", } require( findings, product, "wasix-registry-packages", registry_packages == expected_registry_packages, - "WASIX runtime release metadata must expose the public portable runtime, target-specific AOT, and ICU data artifact crates.", + "WASIX runtime release metadata must expose the public portable runtime, tools, target-specific root/tools AOT, and ICU data artifact crates.", f"src/runtimes/liboliphaunt/wasix/release.toml registry_packages={sorted(registry_packages)!r}", severity="P0", ) diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 4802677e..302832c1 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -146,10 +146,10 @@ def validate_platform_npm_packages( if metadata.get("runtimeRelativePath") != "runtime": fail(f"{target.npm_package} runtimeRelativePath must be runtime") files = ["bin", "runtime", "README.md"] if target.target == "windows-x64-msvc" else ["lib", "runtime", "README.md"] - executable_files = optimize_native_runtime_payload.required_runtime_member_paths( - target.target, - prefix="./runtime/bin", - ) + executable_files = [ + f"./runtime/bin/{tool}" + for tool in sorted(optimize_native_runtime_payload.packaged_runtime_tools(target.target)) + ] elif product == "oliphaunt-broker": if target.executable_relative_path is None: fail(f"{target.id} must declare executable_relative_path") @@ -1024,14 +1024,23 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None runtime_dependency = dependencies.get("liboliphaunt-wasix-portable") if not isinstance(runtime_dependency, dict) or runtime_dependency.get("version") != f"={wasix_runtime_version}": fail("oliphaunt-wasix must depend on liboliphaunt-wasix-portable at the exact liboliphaunt-wasix runtime version") + tools_dependency = dependencies.get("oliphaunt-wasix-tools") + if not isinstance(tools_dependency, dict) or tools_dependency.get("version") != f"={wasix_runtime_version}": + fail("oliphaunt-wasix must depend on oliphaunt-wasix-tools at the exact liboliphaunt-wasix runtime version") expected_aot_dependencies = { 'cfg(all(target_os = "macos", target_arch = "aarch64"))': "liboliphaunt-wasix-aot-aarch64-apple-darwin", 'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))': "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", 'cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))': "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu", 'cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))': "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc", } + expected_tools_aot_dependencies = { + 'cfg(all(target_os = "macos", target_arch = "aarch64"))': "oliphaunt-wasix-tools-aot-aarch64-apple-darwin", + 'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))': "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu", + 'cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))': "oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu", + 'cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))': "oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc", + } target_tables = manifest.get("target", {}) - for cfg, crate in expected_aot_dependencies.items(): + for cfg, crate in {**expected_aot_dependencies, **expected_tools_aot_dependencies}.items(): target = target_tables.get(cfg) target_dependencies = target.get("dependencies", {}) if isinstance(target, dict) else {} dependency = target_dependencies.get(crate) @@ -1063,14 +1072,19 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None expected_registry_packages = { "crates:oliphaunt-icu", "crates:liboliphaunt-wasix-portable", + "crates:oliphaunt-wasix-tools", "crates:liboliphaunt-wasix-aot-aarch64-apple-darwin", "crates:liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu", "crates:liboliphaunt-wasix-aot-x86_64-pc-windows-msvc", "crates:liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", + "crates:oliphaunt-wasix-tools-aot-aarch64-apple-darwin", + "crates:oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu", + "crates:oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc", + "crates:oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu", } if registry_packages != expected_registry_packages: fail( - "liboliphaunt-wasix crates.io registry packages must match public WASIX runtime, AOT, and ICU data artifact crates: " + "liboliphaunt-wasix crates.io registry packages must match public WASIX runtime, tools, AOT, and ICU data artifact crates: " + ", ".join(sorted(registry_packages)) ) features = manifest.get("features", {}) diff --git a/tools/release/optimize_native_runtime_payload.py b/tools/release/optimize_native_runtime_payload.py index e2066ba1..51f85759 100644 --- a/tools/release/optimize_native_runtime_payload.py +++ b/tools/release/optimize_native_runtime_payload.py @@ -17,7 +17,9 @@ ROOT = Path(__file__).resolve().parents[2] -NATIVE_RUNTIME_TOOL_STEMS = ("initdb", "pg_ctl", "pg_dump", "postgres", "psql") +NATIVE_RUNTIME_TOOL_STEMS = ("initdb", "pg_ctl", "postgres") +NATIVE_TOOLS_TOOL_STEMS = ("pg_dump", "psql") +NATIVE_PACKAGED_TOOL_STEMS = (*NATIVE_RUNTIME_TOOL_STEMS, *NATIVE_TOOLS_TOOL_STEMS) ELF_DEBUG_SECTION = re.compile(r"\]\s+\.(debug_[^\s]+|symtab|strtab)\s") MACHO_MAGICS = { b"\xfe\xed\xfa\xce", @@ -82,7 +84,7 @@ def is_windows_target(target: str | None, runtime_dir: Path | None = None) -> bo if runtime_dir is None: return False bin_dir = runtime_dir / "bin" - return any((bin_dir / f"{stem}.exe").exists() for stem in NATIVE_RUNTIME_TOOL_STEMS) + return any((bin_dir / f"{stem}.exe").exists() for stem in NATIVE_PACKAGED_TOOL_STEMS) def required_runtime_tools(target: str | None, runtime_dir: Path | None = None) -> tuple[str, ...]: @@ -91,10 +93,28 @@ def required_runtime_tools(target: str | None, runtime_dir: Path | None = None) return NATIVE_RUNTIME_TOOL_STEMS +def required_tools_package_tools( + target: str | None, runtime_dir: Path | None = None +) -> tuple[str, ...]: + if is_windows_target(target, runtime_dir): + return tuple(f"{stem}.exe" for stem in NATIVE_TOOLS_TOOL_STEMS) + return NATIVE_TOOLS_TOOL_STEMS + + +def packaged_runtime_tools(target: str | None, runtime_dir: Path | None = None) -> tuple[str, ...]: + if is_windows_target(target, runtime_dir): + return tuple(f"{stem}.exe" for stem in NATIVE_PACKAGED_TOOL_STEMS) + return NATIVE_PACKAGED_TOOL_STEMS + + def required_runtime_member_paths(target: str | None, *, prefix: str) -> list[str]: return [f"{prefix.rstrip('/')}/{tool}" for tool in required_runtime_tools(target)] +def required_tools_member_paths(target: str | None, *, prefix: str) -> list[str]: + return [f"{prefix.rstrip('/')}/{tool}" for tool in required_tools_package_tools(target)] + + def runtime_dir_for(root: Path) -> Path | None: for candidate in [ root / "runtime", @@ -139,7 +159,7 @@ def prune_runtime_payload(root: Path, target: str | None = None) -> None: return windows = is_windows_target(target, runtime_dir) - required_tools = set(required_runtime_tools(target, runtime_dir)) + required_tools = set(packaged_runtime_tools(target, runtime_dir)) bin_dir = runtime_dir / "bin" if bin_dir.is_dir(): for path in sorted(bin_dir.iterdir()): @@ -250,7 +270,7 @@ def validate_runtime_tree(root: Path, target: str | None, require_runtime: bool) return errors windows = is_windows_target(target, runtime_dir) - required_tools = set(required_runtime_tools(target, runtime_dir)) + required_tools = set(packaged_runtime_tools(target, runtime_dir)) bin_dir = runtime_dir / "bin" if require_runtime and not bin_dir.is_dir(): errors.append(f"{rel(runtime_dir)} is missing bin") diff --git a/tools/release/package_liboliphaunt_cargo_artifacts.py b/tools/release/package_liboliphaunt_cargo_artifacts.py index f69e7f8e..be2711a6 100644 --- a/tools/release/package_liboliphaunt_cargo_artifacts.py +++ b/tools/release/package_liboliphaunt_cargo_artifacts.py @@ -24,6 +24,8 @@ ROOT = Path(__file__).resolve().parents[2] PRODUCT = "liboliphaunt-native" KIND = "native-runtime" +TOOLS_PRODUCT = "oliphaunt-tools" +TOOLS_KIND = "native-tools" SURFACE = "rust-native-direct" CRATES_IO_MAX_BYTES = 10 * 1024 * 1024 DEFAULT_PART_BYTES = 7 * 1024 * 1024 @@ -35,6 +37,8 @@ class GeneratedPackage: manifest_path: Path crate_path: Path | None target: str + product: str + kind: str role: str index: int | None = None @@ -66,20 +70,22 @@ def sha256_file(path: Path) -> str: return digest.hexdigest() -def cargo_package_name(target_id: str) -> str: - return f"liboliphaunt-native-{target_id}" +def cargo_package_name(target_id: str, *, package_base: str = PRODUCT) -> str: + return f"{package_base}-{target_id}" -def cargo_links_name(target_id: str) -> str: - return f"oliphaunt_artifact_liboliphaunt_native_{target_id.replace('-', '_')}" +def cargo_links_name(target_id: str, *, artifact_product: str = PRODUCT) -> str: + product = artifact_product.replace("-", "_") + return f"oliphaunt_artifact_{product}_{target_id.replace('-', '_')}" -def part_package_name(target_id: str, index: int) -> str: - return f"{cargo_package_name(target_id)}-part-{index:03d}" +def part_package_name(target_id: str, index: int, *, package_base: str = PRODUCT) -> str: + return f"{cargo_package_name(target_id, package_base=package_base)}-part-{index:03d}" -def part_links_name(target_id: str, index: int) -> str: - return f"oliphaunt_artifact_part_liboliphaunt_native_{target_id.replace('-', '_')}_{index:03d}" +def part_links_name(target_id: str, index: int, *, artifact_product: str = PRODUCT) -> str: + product = artifact_product.replace("-", "_") + return f"oliphaunt_artifact_part_{product}_{target_id.replace('-', '_')}_{index:03d}" def rust_crate_ident(crate_name: str) -> str: @@ -134,9 +140,18 @@ def extract_archive(archive_path: Path, destination: Path) -> None: fail(f"{rel(archive_path)} is not a readable tar archive: {error}") -def write_part_crate(crate_dir: Path, *, target_id: str, index: int, version: str) -> None: - name = part_package_name(target_id, index) - links = part_links_name(target_id, index) +def write_part_crate( + crate_dir: Path, + *, + target_id: str, + index: int, + version: str, + package_base: str, + artifact_product: str, + artifact_label: str, +) -> None: + name = part_package_name(target_id, index, package_base=package_base) + links = part_links_name(target_id, index, artifact_product=artifact_product) (crate_dir / "src").mkdir(parents=True, exist_ok=True) (crate_dir / "Cargo.toml").write_text( f"""[package] @@ -144,7 +159,7 @@ def write_part_crate(crate_dir: Path, *, target_id: str, index: int, version: st version = "{version}" edition = "2024" rust-version = "1.93" -description = "Cargo payload part {index:03d} for the {target_id} liboliphaunt native runtime." +description = "Cargo payload part {index:03d} for the {target_id} {artifact_label}." readme = "README.md" repository = "https://github.com/f0rr0/oliphaunt" homepage = "https://oliphaunt.dev" @@ -163,7 +178,7 @@ def write_part_crate(crate_dir: Path, *, target_id: str, index: int, version: st (crate_dir / "README.md").write_text( f"""# {name} -Cargo payload part for the `{target_id}` liboliphaunt native runtime. +Cargo payload part for the `{target_id}` {artifact_label}. Applications do not depend on this crate directly. """, encoding="utf-8", @@ -186,7 +201,7 @@ def write_part_crate(crate_dir: Path, *, target_id: str, index: int, version: st println!("cargo::rerun-if-changed={}", root.display()); if !root.is_dir() { if env::var_os("OLIPHAUNT_ARTIFACT_CRATE_REQUIRE_PAYLOAD").is_some() { - panic!("missing packaged liboliphaunt native payload under {}", root.display()); + panic!("missing packaged Oliphaunt artifact payload under {}", root.display()); } return; } @@ -207,27 +222,32 @@ def write_aggregator_crate( target: artifact_targets.ArtifactTarget, version: str, part_count: int, + package_base: str, + artifact_product: str, + artifact_kind: str, + artifact_label: str, ) -> None: - if target.triple is None or target.library_relative_path is None: - fail(f"{target.id} must declare Cargo target triple and library path") - name = cargo_package_name(target.target) - links = cargo_links_name(target.target) + if target.triple is None: + fail(f"{target.id} must declare Cargo target triple") + name = cargo_package_name(target.target, package_base=package_base) + links = cargo_links_name(target.target, artifact_product=artifact_product) (crate_dir / "src").mkdir(parents=True, exist_ok=True) dependency_lines = [ - f'{part_package_name(target.target, index)} = {{ version = "={version}" }}' + f'{part_package_name(target.target, index, package_base=package_base)} = {{ version = "={version}" }}' for index in range(part_count) ] part_roots = [ - f" {rust_crate_ident(part_package_name(target.target, index))}::PAYLOAD_ROOT," + f" {rust_crate_ident(part_package_name(target.target, index, package_base=package_base))}::PAYLOAD_ROOT," for index in range(part_count) ] + library_relative_path = target.library_relative_path or "" (crate_dir / "Cargo.toml").write_text( f"""[package] name = "{name}" version = "{version}" edition = "2024" rust-version = "1.93" -description = "Cargo artifact crate for the {target.target} liboliphaunt native runtime." +description = "Cargo artifact crate for the {target.target} {artifact_label}." readme = "README.md" repository = "https://github.com/f0rr0/oliphaunt" homepage = "https://oliphaunt.dev" @@ -250,27 +270,27 @@ def write_aggregator_crate( (crate_dir / "README.md").write_text( f"""# {name} -Cargo artifact crate for the `{target.target}` liboliphaunt native runtime. +Cargo artifact crate for the `{target.target}` {artifact_label}. Applications do not depend on this crate directly; `oliphaunt` selects it for matching Cargo targets. """, encoding="utf-8", ) (crate_dir / "src" / "lib.rs").write_text( - f"""pub const PRODUCT: &str = "liboliphaunt-native"; -pub const KIND: &str = "native-runtime"; + f"""pub const PRODUCT: &str = "{artifact_product}"; +pub const KIND: &str = "{artifact_kind}"; pub const RELEASE_TARGET: &str = "{target.target}"; pub const CARGO_TARGET: &str = "{target.triple}"; -pub const LIBRARY_RELATIVE_PATH: &str = "{target.library_relative_path}"; +pub const LIBRARY_RELATIVE_PATH: &str = "{library_relative_path}"; """, encoding="utf-8", ) build_rs = ( AGGREGATOR_BUILD_RS .replace("__SCHEMA__", toml_string("oliphaunt-artifact-manifest-v1")) - .replace("__PRODUCT__", toml_string(PRODUCT)) + .replace("__PRODUCT__", toml_string(artifact_product)) .replace("__VERSION__", toml_string(version)) - .replace("__KIND__", toml_string(KIND)) + .replace("__KIND__", toml_string(artifact_kind)) .replace("__TARGET__", toml_string(target.triple)) .replace("__PART_ROOTS__", "\n".join(part_roots)) ) @@ -488,9 +508,26 @@ def payload_files(source_root: Path) -> list[Path]: return sorted(path for path in source_root.rglob("*") if path.is_file()) -def next_part_dir(source_root: Path, target_id: str, index: int, version: str) -> Path: - crate_dir = source_root / part_package_name(target_id, index) - write_part_crate(crate_dir, target_id=target_id, index=index, version=version) +def next_part_dir( + source_root: Path, + target_id: str, + index: int, + version: str, + *, + package_base: str, + artifact_product: str, + artifact_label: str, +) -> Path: + crate_dir = source_root / part_package_name(target_id, index, package_base=package_base) + write_part_crate( + crate_dir, + target_id=target_id, + index=index, + version=version, + package_base=package_base, + artifact_product=artifact_product, + artifact_label=artifact_label, + ) return crate_dir @@ -511,6 +548,9 @@ def build_part_crates( target_id: str, version: str, part_bytes: int, + package_base: str, + artifact_product: str, + artifact_label: str, ) -> list[Path]: part_dirs: list[Path] = [] current_dir: Path | None = None @@ -518,7 +558,15 @@ def build_part_crates( def start_part() -> Path: index = len(part_dirs) - part_dir = next_part_dir(source_root, target_id, index, version) + part_dir = next_part_dir( + source_root, + target_id, + index, + version, + package_base=package_base, + artifact_product=artifact_product, + artifact_label=artifact_label, + ) part_dirs.append(part_dir) return part_dir @@ -547,7 +595,7 @@ def start_part() -> Path: copy_payload_file(source, current_dir / "payload" / "files" / relative) current_size += size if not part_dirs: - fail(f"{target_id} generated no liboliphaunt native part crates") + fail(f"{target_id} generated no {artifact_label} part crates") return part_dirs @@ -587,35 +635,61 @@ def validate_crate_size(crate_path: Path) -> None: fail(f"{rel(crate_path)} is {size} bytes, above the crates.io 10 MiB package limit") -def package_target( - target: artifact_targets.ArtifactTarget, - *, - version: str, - asset_dir: Path, +def copy_tools_payload(extracted_root: Path, tools_root: Path, target_id: str) -> None: + shutil.rmtree(tools_root, ignore_errors=True) + required = optimize_native_runtime_payload.required_tools_member_paths( + target_id, + prefix="runtime/bin", + ) + missing: list[str] = [] + for member in required: + source = extracted_root / member + if not source.is_file(): + missing.append(member) + continue + destination = tools_root / member + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, destination) + source.unlink() + if missing: + fail(f"{target_id} optimized payload is missing native tools: {', '.join(missing)}") + optimize_native_runtime_payload.prune_empty_dirs(extracted_root) + + +def package_payload( + payload_root: Path, source_root: Path, output_dir: Path, cargo_target_dir: Path, + *, + target: artifact_targets.ArtifactTarget, + version: str, part_bytes: int, + package_base: str, + artifact_product: str, + artifact_kind: str, + artifact_label: str, ) -> list[GeneratedPackage]: - archive = asset_dir / target.asset_name(version) - if not archive.is_file(): - fail(f"missing liboliphaunt native release asset: {rel(archive)}") - extracted_root = source_root / f"{target.target}-extracted" - extract_archive(archive, extracted_root) - optimize_native_runtime_payload.optimize_payload(extracted_root, target.target) part_dirs = build_part_crates( - extracted_root, + payload_root, source_root, target_id=target.target, version=version, part_bytes=part_bytes, + package_base=package_base, + artifact_product=artifact_product, + artifact_label=artifact_label, ) - aggregator_dir = source_root / cargo_package_name(target.target) + aggregator_dir = source_root / cargo_package_name(target.target, package_base=package_base) write_aggregator_crate( aggregator_dir, target=target, version=version, part_count=len(part_dirs), + package_base=package_base, + artifact_product=artifact_product, + artifact_kind=artifact_kind, + artifact_label=artifact_label, ) packages: list[GeneratedPackage] = [] @@ -626,10 +700,12 @@ def package_target( shutil.copy2(crate_path, output) packages.append( GeneratedPackage( - name=part_package_name(target.target, index), + name=part_package_name(target.target, index, package_base=package_base), manifest_path=part_dir / "Cargo.toml", crate_path=output, target=target.target, + product=artifact_product, + kind=artifact_kind, role="part", index=index, ) @@ -637,16 +713,66 @@ def package_target( packages.append( GeneratedPackage( - name=cargo_package_name(target.target), + name=cargo_package_name(target.target, package_base=package_base), manifest_path=aggregator_dir / "Cargo.toml", crate_path=None, target=target.target, + product=artifact_product, + kind=artifact_kind, role="aggregator", ) ) return packages +def package_target( + target: artifact_targets.ArtifactTarget, + *, + version: str, + asset_dir: Path, + source_root: Path, + output_dir: Path, + cargo_target_dir: Path, + part_bytes: int, +) -> list[GeneratedPackage]: + archive = asset_dir / target.asset_name(version) + if not archive.is_file(): + fail(f"missing liboliphaunt native release asset: {rel(archive)}") + extracted_root = source_root / f"{target.target}-extracted" + extract_archive(archive, extracted_root) + optimize_native_runtime_payload.optimize_payload(extracted_root, target.target) + tools_root = source_root / f"{target.target}-tools-extracted" + copy_tools_payload(extracted_root, tools_root, target.target) + return [ + *package_payload( + extracted_root, + source_root, + output_dir, + cargo_target_dir, + target=target, + version=version, + part_bytes=part_bytes, + package_base=PRODUCT, + artifact_product=PRODUCT, + artifact_kind=KIND, + artifact_label="liboliphaunt native runtime", + ), + *package_payload( + tools_root, + source_root, + output_dir, + cargo_target_dir, + target=target, + version=version, + part_bytes=part_bytes, + package_base=TOOLS_PRODUCT, + artifact_product=TOOLS_PRODUCT, + artifact_kind=TOOLS_KIND, + artifact_label="Oliphaunt native tools", + ), + ] + + def write_packages_manifest(packages: list[GeneratedPackage], output_dir: Path) -> None: data = { "schema": "oliphaunt-liboliphaunt-cargo-artifacts-v1", @@ -655,6 +781,8 @@ def write_packages_manifest(packages: list[GeneratedPackage], output_dir: Path) { "name": package.name, "target": package.target, + "product": package.product, + "kind": package.kind, "role": package.role, "index": package.index, "manifestPath": rel(package.manifest_path), diff --git a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py index 0ad5204b..e79287f3 100644 --- a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py +++ b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py @@ -24,14 +24,26 @@ SCHEMA = "oliphaunt-liboliphaunt-wasix-cargo-artifacts-v2" CRATES_IO_MAX_BYTES = 10 * 1024 * 1024 RUNTIME_PACKAGE = "liboliphaunt-wasix-portable" +TOOLS_PACKAGE = "oliphaunt-wasix-tools" ICU_PACKAGE = "oliphaunt-icu" ICU_PAYLOAD_ARCHIVE = "icu-data.tar.zst" +TOOLS_PAYLOAD_FILES = ( + "bin/pg_dump.wasix.wasm", + "bin/psql.wasix.wasm", +) +TOOLS_AOT_ARTIFACTS = {"tool:pg_dump", "tool:psql"} AOT_PACKAGES = { "macos-arm64": "liboliphaunt-wasix-aot-aarch64-apple-darwin", "linux-arm64-gnu": "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu", "linux-x64-gnu": "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", "windows-x64-msvc": "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc", } +TOOLS_AOT_PACKAGES = { + "macos-arm64": "oliphaunt-wasix-tools-aot-aarch64-apple-darwin", + "linux-arm64-gnu": "oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu", + "linux-x64-gnu": "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu", + "windows-x64-msvc": "oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc", +} AOT_TARGET_TRIPLES = { "macos-arm64": "aarch64-apple-darwin", "linux-arm64-gnu": "aarch64-unknown-linux-gnu", @@ -209,6 +221,48 @@ def validate_runtime_payload(root: Path) -> None: "WASIX runtime Cargo payload must not bundle ICU data; " f"found {bundled_icu[0]} in oliphaunt.wasix.tar.zst" ) + bundled_tools = sorted( + member + for member in runtime_members + if member in {"oliphaunt/bin/pg_dump", "oliphaunt/bin/psql"} + ) + if bundled_tools: + fail( + "WASIX runtime Cargo payload must not bundle standalone tools inside " + f"oliphaunt.wasix.tar.zst; found {bundled_tools[0]}" + ) + + +def split_runtime_tools_payload(runtime_root: Path, extract_root: Path) -> tuple[Path, Path]: + core_root = extract_root / "runtime-core-payload" + tools_root = extract_root / "tools-payload" + shutil.rmtree(core_root, ignore_errors=True) + shutil.rmtree(tools_root, ignore_errors=True) + shutil.copytree(runtime_root, core_root) + missing: list[str] = [] + for relative in TOOLS_PAYLOAD_FILES: + source = runtime_root / relative + if not source.is_file(): + missing.append(relative) + continue + destination = tools_root / relative + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, destination) + core_file = core_root / relative + if core_file.exists(): + core_file.unlink() + if missing: + fail("WASIX tools Cargo payload is missing " + ", ".join(missing)) + prune_empty_dirs(core_root) + return core_root, tools_root + + +def prune_empty_dirs(root: Path) -> None: + for path in sorted((item for item in root.rglob("*") if item.is_dir()), reverse=True): + try: + path.rmdir() + except OSError: + pass def icu_root_contains_data(root: Path) -> bool: @@ -308,6 +362,87 @@ def validate_aot_payload(root: Path) -> None: fail(f"WASIX AOT Cargo payload file set mismatch for {rel(root)}: expected {sorted(expected)}, got {sorted(actual)}") +def split_aot_tools_payload(aot_root: Path, extract_root: Path, target_id: str) -> tuple[Path, Path]: + manifest_path = aot_root / "manifest.json" + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + artifacts = manifest.get("artifacts") + if not isinstance(artifacts, list): + fail(f"{rel(manifest_path)} must contain an artifacts array") + + core_root = extract_root / f"{target_id}-aot-core-payload" + tools_root = extract_root / f"{target_id}-aot-tools-payload" + shutil.rmtree(core_root, ignore_errors=True) + shutil.rmtree(tools_root, ignore_errors=True) + core_artifacts: list[dict[str, object]] = [] + tools_artifacts: list[dict[str, object]] = [] + + for artifact in artifacts: + if not isinstance(artifact, dict): + fail(f"{rel(manifest_path)} contains a non-object artifact") + name = artifact.get("name") + path = artifact.get("path") + if not isinstance(name, str) or not isinstance(path, str): + fail(f"{rel(manifest_path)} contains an artifact without name/path") + target_root = tools_root if name in TOOLS_AOT_ARTIFACTS else core_root + target_artifacts = tools_artifacts if name in TOOLS_AOT_ARTIFACTS else core_artifacts + source = aot_root / path + if not source.is_file(): + fail(f"{rel(manifest_path)} references missing AOT artifact {path}") + destination = target_root / path + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, destination) + target_artifacts.append(artifact) + + missing = sorted(TOOLS_AOT_ARTIFACTS - {str(item.get("name")) for item in tools_artifacts}) + if missing: + fail(f"{rel(manifest_path)} is missing WASIX tools AOT artifacts: {', '.join(missing)}") + if not core_artifacts: + fail(f"{rel(manifest_path)} generated no core WASIX AOT artifacts") + + for target_root, target_artifacts in [(core_root, core_artifacts), (tools_root, tools_artifacts)]: + target_manifest = {**manifest, "artifacts": target_artifacts} + target_root.mkdir(parents=True, exist_ok=True) + (target_root / "manifest.json").write_text( + json.dumps(target_manifest, indent=2) + "\n", + encoding="utf-8", + ) + return core_root, tools_root + + +def patch_tools_aot_template(crate_dir: Path, target: str) -> None: + manifest = crate_dir / "Cargo.toml" + text = manifest.read_text(encoding="utf-8") + links = "oliphaunt_artifact_oliphaunt_wasix_tools_aot_" + target.replace("-", "_") + text = re.sub(r'(?m)^links = "[^"]+"$', f'links = "{links}"', text, count=1) + text = re.sub( + r'(?m)^description = "[^"]+"$', + f'description = "Internal Wasmer AOT artifacts for oliphaunt-wasix tools on {target}"', + text, + count=1, + ) + manifest.write_text(text, encoding="utf-8") + + build_rs = crate_dir / "build.rs" + text = build_rs.read_text(encoding="utf-8") + text = text.replace( + 'const ARTIFACT_PRODUCT: &str = "liboliphaunt-wasix";', + 'const ARTIFACT_PRODUCT: &str = "oliphaunt-wasix-tools";', + ) + text = text.replace( + 'const ARTIFACT_KIND: &str = "wasix-aot";', + 'const ARTIFACT_KIND: &str = "wasix-tools-aot";', + ) + text = text.replace( + '.strip_prefix("liboliphaunt-wasix-aot-")', + '.strip_prefix("oliphaunt-wasix-tools-aot-")', + ) + text = text.replace( + "AOT crate name starts with liboliphaunt-wasix-aot-", + "AOT crate name starts with oliphaunt-wasix-tools-aot-", + ) + build_rs.write_text(text, encoding="utf-8") + + def rewrite_cargo_manifest( manifest: Path, *, @@ -317,6 +452,7 @@ def rewrite_cargo_manifest( extension_aot_sources: list[ExtensionAotCargoSource], ) -> None: text = manifest.read_text(encoding="utf-8") + text = re.sub(r'(?m)^name = "[^"]+"$', f'name = "{package_name}"', text, count=1) text = re.sub(r'(?m)^version = "[^"]+"$', f'version = "{version}"', text, count=1) text = re.sub(r'(?m)^publish = false\n?', "", text) if package_name == RUNTIME_PACKAGE and extension_sources: @@ -389,6 +525,8 @@ def copy_package_source( crate_dir, ignore=shutil.ignore_patterns("target", "payload", "artifacts"), ) + if spec.kind == "wasix-tools-aot": + patch_tools_aot_template(crate_dir, spec.target) shutil.copytree(spec.payload_root, crate_dir / spec.payload_dir_name) rewrite_cargo_manifest( crate_dir / "Cargo.toml", @@ -835,13 +973,24 @@ def package_specs(asset_dir: Path, extract_root: Path, version: str) -> list[Pac extract_tar_zstd(runtime_archive, runtime_extract) runtime_root = target_asset_root(runtime_extract) validate_runtime_payload(runtime_root) + runtime_core_root, tools_root = split_runtime_tools_payload(runtime_root, extract_root) specs.append( PackageSpec( name=RUNTIME_PACKAGE, target="portable", kind="wasix-runtime", template_dir=ROOT / "src/runtimes/liboliphaunt/wasix/crates/assets", - payload_root=runtime_root, + payload_root=runtime_core_root, + payload_dir_name="payload", + ) + ) + specs.append( + PackageSpec( + name=TOOLS_PACKAGE, + target="portable", + kind="wasix-tools", + template_dir=ROOT / "src/runtimes/liboliphaunt/wasix/crates/tools", + payload_root=tools_root, payload_dir_name="payload", ) ) @@ -873,13 +1022,24 @@ def package_specs(asset_dir: Path, extract_root: Path, version: str) -> list[Pac triple = AOT_TARGET_TRIPLES[target_id] aot_root = target_aot_root(extracted, triple) validate_aot_payload(aot_root) + aot_core_root, tools_aot_root = split_aot_tools_payload(aot_root, extract_root, target_id) specs.append( PackageSpec( name=package_name, target=triple, kind="wasix-aot", template_dir=ROOT / "src/runtimes/liboliphaunt/wasix/crates/aot" / triple, - payload_root=aot_root, + payload_root=aot_core_root, + payload_dir_name="artifacts", + ) + ) + specs.append( + PackageSpec( + name=TOOLS_AOT_PACKAGES[target_id], + target=triple, + kind="wasix-tools-aot", + template_dir=ROOT / "src/runtimes/liboliphaunt/wasix/crates/tools-aot" / triple, + payload_root=tools_aot_root, payload_dir_name="artifacts", ) ) diff --git a/tools/release/release.py b/tools/release/release.py index a7e96510..17352501 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -597,8 +597,13 @@ def render_oliphaunt_release_cargo_toml(source: str, native_version: str, broker published_only=True, ): crate = package_liboliphaunt_cargo_artifacts.cargo_package_name(target.target) + tools_crate = package_liboliphaunt_cargo_artifacts.cargo_package_name( + target.target, + package_base=package_liboliphaunt_cargo_artifacts.TOOLS_PRODUCT, + ) cfg = rust_artifact_cargo_target_cfg(target) target_dependencies.setdefault(cfg, []).append(f'{crate} = {{ version = "={native_version}" }}') + target_dependencies.setdefault(cfg, []).append(f'{tools_crate} = {{ version = "={native_version}" }}') for target in artifact_targets.artifact_targets( product="oliphaunt-broker", kind="broker-helper", @@ -684,6 +689,12 @@ def prepare_oliphaunt_release_source(version: str) -> Path: crate = package_liboliphaunt_cargo_artifacts.cargo_package_name(target.target) if f'{crate} = {{ version = "={native_version}" }}' not in rendered: fail(f"generated oliphaunt release source is missing native runtime artifact dependency {crate}") + tools_crate = package_liboliphaunt_cargo_artifacts.cargo_package_name( + target.target, + package_base=package_liboliphaunt_cargo_artifacts.TOOLS_PRODUCT, + ) + if f'{tools_crate} = {{ version = "={native_version}" }}' not in rendered: + fail(f"generated oliphaunt release source is missing native tools artifact dependency {tools_crate}") for target in artifact_targets.artifact_targets( product="oliphaunt-broker", kind="broker-helper", @@ -892,6 +903,16 @@ def validate_wasix_portable_release_asset(archive: Path) -> None: f"{archive.relative_to(ROOT)} must not bundle ICU data inside target/oliphaunt-wasix/assets/oliphaunt.wasix.tar.zst: " + ", ".join(bundled_icu[:5]) ) + bundled_tools = sorted( + member + for member in runtime_members + if member in {"oliphaunt/bin/pg_dump", "oliphaunt/bin/psql"} + ) + if bundled_tools: + fail( + f"{archive.relative_to(ROOT)} must not bundle standalone tools inside target/oliphaunt-wasix/assets/oliphaunt.wasix.tar.zst: " + + ", ".join(bundled_tools) + ) def validate_wasix_icu_release_asset(archive: Path) -> None: @@ -2290,10 +2311,10 @@ def liboliphaunt_npm_tarballs(version: str) -> list[tuple[str, Path]]: ): if target.library_relative_path is None: fail(f"{target.id} must declare library_relative_path for npm artifact package publication") - runtime_members = optimize_native_runtime_payload.required_runtime_member_paths( - target.target, - prefix="package/runtime/bin", - ) + runtime_members = [ + f"package/runtime/bin/{tool}" + for tool in sorted(optimize_native_runtime_payload.packaged_runtime_tools(target.target)) + ] required_members = [f"package/{target.library_relative_path}", *runtime_members] package_dir = stages[package_name] tarball = npm_pack_and_validate( @@ -2409,19 +2430,26 @@ def liboliphaunt_cargo_artifact_crates(version: str) -> list[tuple[str, Path | N fail(f"{manifest_path.relative_to(ROOT)} has an invalid schema") packages: list[tuple[str, Path | None, Path, str]] = [] + native_targets = artifact_targets.artifact_targets( + product="liboliphaunt-native", + kind="native-runtime", + surface="rust-native-direct", + published_only=True, + ) expected_aggregators = { package_liboliphaunt_cargo_artifacts.cargo_package_name(target.target) - for target in artifact_targets.artifact_targets( - product="liboliphaunt-native", - kind="native-runtime", - surface="rust-native-direct", - published_only=True, + for target in native_targets + } | { + package_liboliphaunt_cargo_artifacts.cargo_package_name( + target.target, + package_base=package_liboliphaunt_cargo_artifacts.TOOLS_PRODUCT, ) + for target in native_targets } configured_crates = set(check_cratesio_publication.product_crates("liboliphaunt-native")) if configured_crates != expected_aggregators: fail( - "liboliphaunt-native crates.io packages must match native Rust artifact targets: " + "liboliphaunt-native crates.io packages must match native Rust runtime/tool artifact targets: " f"expected={sorted(expected_aggregators)}, configured={sorted(configured_crates)}" ) @@ -2446,16 +2474,16 @@ def liboliphaunt_cargo_artifact_crates(version: str) -> list[tuple[str, Path | N expected_part_crates.add(crate_path) elif role == "aggregator": if name not in expected_aggregators: - fail(f"unexpected liboliphaunt native aggregator crate {name}") + fail(f"unexpected liboliphaunt native artifact aggregator crate {name}") if crate_path is not None: - fail(f"liboliphaunt native aggregator {name} must publish from source after part crates") + fail(f"liboliphaunt native artifact aggregator {name} must publish from source after part crates") seen_aggregators.add(name) else: fail(f"unsupported liboliphaunt generated Cargo artifact role {role!r}") packages.append((name, crate_path, source_manifest, role)) if seen_aggregators != expected_aggregators: fail( - "generated liboliphaunt native aggregators do not match configured crates: " + "generated liboliphaunt native artifact aggregators do not match configured crates: " f"expected={sorted(expected_aggregators)}, generated={sorted(seen_aggregators)}" ) unexpected = sorted( @@ -2464,7 +2492,7 @@ def liboliphaunt_cargo_artifact_crates(version: str) -> list[tuple[str, Path | N if path not in expected_part_crates ) if unexpected: - fail("unexpected liboliphaunt native Cargo artifact crate(s): " + ", ".join(unexpected)) + fail("unexpected liboliphaunt native Cargo artifact part crate(s): " + ", ".join(unexpected)) return packages @@ -2492,7 +2520,9 @@ def liboliphaunt_wasix_cargo_artifact_crates(version: str) -> list[tuple[str, Pa expected_base_crates = { package_liboliphaunt_wasix_cargo_artifacts.ICU_PACKAGE, package_liboliphaunt_wasix_cargo_artifacts.RUNTIME_PACKAGE, + package_liboliphaunt_wasix_cargo_artifacts.TOOLS_PACKAGE, *package_liboliphaunt_wasix_cargo_artifacts.AOT_PACKAGES.values(), + *package_liboliphaunt_wasix_cargo_artifacts.TOOLS_AOT_PACKAGES.values(), } configured_crates = set(check_cratesio_publication.product_crates("liboliphaunt-wasix")) if configured_crates != expected_base_crates: @@ -2523,7 +2553,7 @@ def liboliphaunt_wasix_cargo_artifact_crates(version: str) -> list[tuple[str, Pa and any(name.startswith(f"{product}-wasix-aot-") for product in product_metadata.extension_product_ids()) ): fail(f"unexpected liboliphaunt-wasix Cargo artifact crate {name}") - if kind not in {"wasix-runtime", "wasix-aot", "icu-data", "wasix-extension", "wasix-extension-aot"}: + if kind not in {"wasix-runtime", "wasix-tools", "wasix-aot", "wasix-tools-aot", "icu-data", "wasix-extension", "wasix-extension-aot"}: fail(f"{manifest_path.relative_to(ROOT)} has unsupported WASIX Cargo artifact kind {kind!r}") source_manifest = ROOT / raw_manifest if not source_manifest.is_file(): diff --git a/tools/xtask/src/asset_checks.rs b/tools/xtask/src/asset_checks.rs index 08ecba18..3d45f2fa 100644 --- a/tools/xtask/src/asset_checks.rs +++ b/tools/xtask/src/asset_checks.rs @@ -376,6 +376,10 @@ pub(crate) fn verify_asset_manifest_hashes() -> Result<()> { "pg_dump module sha256", )?; } + if let Some(psql) = &manifest.psql { + verify_file_sha256(&base.join(&psql.path), &psql.sha256, "psql wasm")?; + ensure_eq(&psql.sha256, &psql.module_sha256, "psql module sha256")?; + } if let Some(initdb) = &manifest.initdb { verify_file_sha256(&base.join(&initdb.path), &initdb.sha256, "initdb wasm")?; ensure_eq( @@ -508,6 +512,9 @@ fn verify_root_asset_metadata( if let Some(pg_dump) = &manifest.pg_dump { verify_metadata_value("pg-dump-wasix-sha256", &pg_dump.sha256, "pg_dump metadata")?; } + if let Some(psql) = &manifest.psql { + verify_metadata_value("psql-wasix-sha256", &psql.sha256, "psql metadata")?; + } if let Some(initdb) = &manifest.initdb { verify_metadata_value("initdb-wasix-sha256", &initdb.sha256, "initdb metadata")?; } @@ -1373,7 +1380,7 @@ pub(crate) fn check_canonical_asset_layout_in(asset_dir: &Path, strict: bool) -> } let runtime_entries = archive_entries(&runtime_archive)?; - let mut required_paths = vec![ + let required_paths = vec![ "oliphaunt/bin/oliphaunt", "oliphaunt/bin/postgres", "oliphaunt/bin/initdb", @@ -1383,9 +1390,6 @@ pub(crate) fn check_canonical_asset_layout_in(asset_dir: &Path, strict: bool) -> "oliphaunt/share/postgresql/timezone/America/New_York", "oliphaunt/share/postgresql/timezonesets/Default", ]; - if !skip_extensions_for_perf_probe() { - required_paths.push("oliphaunt/bin/pg_dump"); - } for required in required_paths { if !runtime_entries.contains(required) { bail!( diff --git a/tools/xtask/src/asset_manifest.rs b/tools/xtask/src/asset_manifest.rs index 842db634..7fcf46ef 100644 --- a/tools/xtask/src/asset_manifest.rs +++ b/tools/xtask/src/asset_manifest.rs @@ -214,11 +214,13 @@ pub(super) struct AssetManifestOut { pub(super) source_fingerprint: Option, pub(super) runtime: RuntimeAssetOut, pub(super) runtime_support: Vec, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub(super) pg_dump: Option, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub(super) psql: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub(super) initdb: Option, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub(super) pgdata_template: Option, pub(super) extensions: Vec, pub(super) sources: Vec, diff --git a/tools/xtask/src/asset_pipeline.rs b/tools/xtask/src/asset_pipeline.rs index 1203046d..f25f3e05 100644 --- a/tools/xtask/src/asset_pipeline.rs +++ b/tools/xtask/src/asset_pipeline.rs @@ -281,6 +281,13 @@ impl BuildOutputs { aot_file: "pg_dump-llvm-opta.bin.zst".to_owned(), requires_aot: true, }); + modules.push(BuildModuleOutput { + name: "tool:psql".to_owned(), + kind: "tool".to_owned(), + path: build_dir.join("src/bin/psql/psql"), + aot_file: "psql-llvm-opta.bin.zst".to_owned(), + requires_aot: true, + }); } if !skip_extensions_for_perf_probe() { for extension in extension_catalog::promoted_build_specs()? { @@ -392,6 +399,17 @@ impl BuildOutputs { requires_aot: true, }); } + if let Some(psql) = &manifest.psql { + let path = base.join("tools/psql"); + copy_file(&assets_base.join(&psql.path), &path)?; + modules.push(BuildModuleOutput { + name: "tool:psql".to_owned(), + kind: "tool".to_owned(), + path, + aot_file: "psql-llvm-opta.bin.zst".to_owned(), + requires_aot: true, + }); + } if let Some(initdb) = &manifest.initdb { let path = base.join("tools/initdb"); copy_file(&assets_base.join(&initdb.path), &path)?; @@ -981,6 +999,15 @@ fn build_output_modules_from_asset_manifest( link: pg_dump.link.clone(), }); } + if let Some(psql) = &manifest.psql { + modules.push(BuildModuleManifestOut { + name: "tool:psql".to_owned(), + kind: "tool".to_owned(), + path: psql.path.clone(), + sha256: psql.module_sha256.clone(), + link: psql.link.clone(), + }); + } if let Some(initdb) = &manifest.initdb { modules.push(BuildModuleManifestOut { name: "tool:initdb".to_owned(), @@ -1281,6 +1308,10 @@ fn asset_build_commands(backend_script: &str) -> Result> script: "src/runtimes/liboliphaunt/wasix/assets/build/docker_pgdump.sh".to_owned(), skip_for_core_probe: true, }); + commands.push(AssetBuildCommand { + script: "src/runtimes/liboliphaunt/wasix/assets/build/docker_psql.sh".to_owned(), + skip_for_core_probe: true, + }); Ok(commands) } @@ -1520,6 +1551,13 @@ fn package_assets_with_options( copy_file(outputs.module_path("tool:pg_dump")?, &pg_dump)?; Some(pg_dump) }; + let psql = if skip_extensions_for_perf_probe() { + None + } else { + let psql = assets_dir.join("bin/psql.wasix.wasm"); + copy_file(outputs.module_path("tool:psql")?, &psql)?; + Some(psql) + }; let initdb = assets_dir.join("bin/initdb.wasix.wasm"); copy_file(outputs.module_path("tool:initdb")?, &initdb)?; @@ -1550,6 +1588,7 @@ fn package_assets_with_options( outputs.module_path("runtime:oliphaunt")?, &runtime_archive, pg_dump.as_deref(), + psql.as_deref(), &initdb, &[ BinaryPackage { @@ -2003,9 +2042,6 @@ fn stage_runtime_tree(build: &Path, source: &Path, runtime: &Path) -> Result<()> copy_file(&build.join("src/backend/oliphaunt"), &bin.join("oliphaunt"))?; copy_file(&build.join("src/backend/oliphaunt"), &bin.join("postgres"))?; - if !skip_extensions_for_perf_probe() { - copy_file(&build.join("src/bin/pg_dump/pg_dump"), &bin.join("pg_dump"))?; - } copy_file(&build.join("src/bin/initdb/initdb"), &bin.join("initdb"))?; fs::write(runtime.join("password"), b"password\n") .with_context(|| format!("write {}", runtime.join("password").display()))?; @@ -2482,6 +2518,7 @@ fn write_asset_manifest( runtime_module: &Path, runtime_archive: &Path, pg_dump: Option<&Path>, + psql: Option<&Path>, initdb: &Path, runtime_support: &[BinaryPackage<'_>], extensions: &[ExtensionArtifact<'_>], @@ -2531,6 +2568,20 @@ fn write_asset_manifest( }) }) .transpose()?, + psql: psql + .map(|psql| { + Ok::<_, anyhow::Error>(BinaryAssetOut { + name: "psql".to_owned(), + path: "bin/psql.wasix.wasm".to_owned(), + sha256: sha256_file(psql)?, + module_sha256: sha256_file(psql)?, + size: fs::metadata(psql) + .with_context(|| format!("metadata {}", psql.display()))? + .len(), + link: read_wasm_link_metadata(psql)?, + }) + }) + .transpose()?, initdb: Some(BinaryAssetOut { name: "initdb".to_owned(), path: "bin/initdb.wasix.wasm".to_owned(), @@ -3201,6 +3252,9 @@ fn update_root_asset_metadata_in( if let Some(pg_dump) = &manifest.pg_dump { text = replace_metadata_value(text, "pg-dump-wasix-sha256", &pg_dump.sha256); } + if let Some(psql) = &manifest.psql { + text = replace_metadata_value(text, "psql-wasix-sha256", &psql.sha256); + } if let Some(initdb) = &manifest.initdb { text = replace_metadata_value(text, "initdb-wasix-sha256", &initdb.sha256); } diff --git a/tools/xtask/src/postgres_guard.rs b/tools/xtask/src/postgres_guard.rs index 2b0ee2bb..6b86f25f 100644 --- a/tools/xtask/src/postgres_guard.rs +++ b/tools/xtask/src/postgres_guard.rs @@ -1108,13 +1108,22 @@ pub(crate) fn check_source_lane_isolation() -> Result<()> { "manifest_dir.join(\"payload\")", "write_source_only_assets", "source-only-template", - "optional_include_bytes_body(&pg_dump)", ] { ensure!( asset_build_rs.contains(marker), "asset crate source-only build script guard is missing marker {marker:?}" ); } + ensure_file_contains_all( + "src/runtimes/liboliphaunt/wasix/crates/tools/build.rs", + &[ + "oliphaunt-wasix-tools", + "pg_dump_wasm", + "psql_wasm", + "bin/pg_dump.wasix.wasm", + "bin/psql.wasix.wasm", + ], + )?; for marker in [ "OLIPHAUNT_WASM_SOURCE_LANE", "validate_asset_manifest_source_lane", From 3cea532a95e9413a841eeb694c2ad64e0beec77f Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Thu, 25 Jun 2026 19:34:43 +0000 Subject: [PATCH 012/137] fix: split native tools package surfaces --- docs/architecture/native-liboliphaunt.md | 3 +- .../examples-ci-release-validation.md | 87 +++++++++ examples/README.md | 16 +- examples/electron-wasix/src-wasix/Cargo.toml | 5 + examples/electron-wasix/src-wasix/src/main.rs | 18 +- examples/electron/README.md | 2 +- examples/electron/package.json | 4 +- examples/electron/src/oliphaunt-kysely.ts | 135 -------------- examples/electron/src/todos.ts | 29 ++- examples/tauri-wasix/src-tauri/Cargo.toml | 5 + examples/tauri-wasix/src-tauri/src/lib.rs | 23 ++- examples/tauri/src-tauri/Cargo.lock | 122 +++---------- examples/tauri/src-tauri/Cargo.toml | 1 + examples/tauri/src-tauri/src/lib.rs | 55 +++++- examples/tools/check-examples.sh | 21 +++ examples/tools/check-lockfiles.sh | 27 ++- pnpm-lock.yaml | 26 +++ pnpm-workspace.yaml | 1 + release-please-config.json | 20 +++ .../tauri-sqlx-vanilla/src-tauri/Cargo.toml | 7 +- .../tauri-sqlx-vanilla/src-tauri/src/bench.rs | 19 +- .../native/packages/darwin-arm64/package.json | 4 +- .../packages/linux-arm64-gnu/package.json | 4 +- .../packages/linux-x64-gnu/package.json | 4 +- .../packages/win32-x64-msvc/package.json | 4 +- src/runtimes/liboliphaunt/native/release.toml | 4 + .../tools-packages/darwin-arm64/README.md | 5 + .../tools-packages/darwin-arm64/package.json | 40 +++++ .../tools-packages/linux-arm64-gnu/README.md | 5 + .../linux-arm64-gnu/package.json | 43 +++++ .../tools-packages/linux-x64-gnu/README.md | 5 + .../tools-packages/linux-x64-gnu/package.json | 43 +++++ .../tools-packages/win32-x64-msvc/README.md | 5 + .../win32-x64-msvc/package.json | 40 +++++ src/sdks/js/ARCHITECTURE.md | 6 +- src/sdks/js/README.md | 8 +- src/sdks/js/package.json | 6 +- .../js/src/__tests__/asset-resolver.test.ts | 27 ++- src/sdks/js/src/__tests__/client.test.ts | 3 +- .../js/src/__tests__/native-bindings.test.ts | 4 +- src/sdks/js/src/native/assets-node.ts | 131 +++++++++++++- src/sdks/js/src/native/common.ts | 10 ++ src/sdks/js/src/runtime/server.ts | 33 +++- src/sdks/js/tools/check-sdk.sh | 13 ++ src/sdks/rust/src/backup.rs | 10 +- src/sdks/rust/src/build_resources.rs | 62 +++++++ src/sdks/rust/src/lib.rs | 2 + src/sdks/rust/src/liboliphaunt/mod.rs | 4 +- src/sdks/rust/src/liboliphaunt/root.rs | 45 +++++ .../rust/src/liboliphaunt/root/runtime.rs | 35 +++- .../liboliphaunt/root/runtime/cache_key.rs | 137 ++++++++++++++- .../src/liboliphaunt/root/runtime/install.rs | 166 +++++++++++++++--- .../src/liboliphaunt/root/runtime/locate.rs | 39 +++- .../rust/src/liboliphaunt/root/template.rs | 3 +- .../rust/src/runtime_resources/package.rs | 4 + src/sdks/rust/src/server.rs | 7 +- tools/release/artifact_targets.py | 24 +++ tools/release/check_consumer_shape.py | 8 + tools/release/check_release_metadata.py | 27 ++- tools/release/local_registry_publish.py | 36 ++-- .../optimize_native_runtime_payload.py | 60 ++++++- .../package_liboliphaunt_cargo_artifacts.py | 13 +- tools/release/release.py | 65 ++++++- tools/release/sync-example-lockfiles.py | 37 ++-- 64 files changed, 1483 insertions(+), 374 deletions(-) create mode 100644 docs/maintainers/examples-ci-release-validation.md delete mode 100644 examples/electron/src/oliphaunt-kysely.ts create mode 100644 src/runtimes/liboliphaunt/native/tools-packages/darwin-arm64/README.md create mode 100644 src/runtimes/liboliphaunt/native/tools-packages/darwin-arm64/package.json create mode 100644 src/runtimes/liboliphaunt/native/tools-packages/linux-arm64-gnu/README.md create mode 100644 src/runtimes/liboliphaunt/native/tools-packages/linux-arm64-gnu/package.json create mode 100644 src/runtimes/liboliphaunt/native/tools-packages/linux-x64-gnu/README.md create mode 100644 src/runtimes/liboliphaunt/native/tools-packages/linux-x64-gnu/package.json create mode 100644 src/runtimes/liboliphaunt/native/tools-packages/win32-x64-msvc/README.md create mode 100644 src/runtimes/liboliphaunt/native/tools-packages/win32-x64-msvc/package.json create mode 100644 src/sdks/rust/src/build_resources.rs diff --git a/docs/architecture/native-liboliphaunt.md b/docs/architecture/native-liboliphaunt.md index 151b8400..b8b5d550 100644 --- a/docs/architecture/native-liboliphaunt.md +++ b/docs/architecture/native-liboliphaunt.md @@ -448,7 +448,8 @@ OLIPHAUNT_TRACK_BUILD=never src/runtimes/liboliphaunt/native/tools/check-track.s - Server mode starts a local PostgreSQL process and exposes a connection string; SDK-owned protocol traffic uses a short Unix-domain socket on Unix by default with buffered frame reads, while the public connection string remains - PostgreSQL-compatible TCP. The runtime cache includes `pg_dump` and `psql`, + PostgreSQL-compatible TCP. Package-managed installs materialize the root + runtime together with split `pg_dump`/`psql` tools into the runtime cache, while broader ORM/pool parity tests are still release gates. - The latest complete source-current native matrix is `target/perf/native-liboliphaunt-20260524T090412Z/report.md`, with verified diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md new file mode 100644 index 00000000..c0916ab3 --- /dev/null +++ b/docs/maintainers/examples-ci-release-validation.md @@ -0,0 +1,87 @@ +# Examples, CI, Release, and SDK Validation Tracker + +This is the working checklist for validating the registry-first example flow and +the release/tooling surface after the runtime tool crate split. + +## P0: Registry-First Example Validation + +- [ ] Rebuild or stage current local registry artifacts from the active branch. +- [ ] Publish local Cargo crates into `target/local-registries/cargo`, including: + - `liboliphaunt-native-linux-x64-gnu` + - `oliphaunt-tools-linux-x64-gnu` + - `oliphaunt-broker-linux-x64-gnu` + - selected native extension crates + - `liboliphaunt-wasix-portable` + - `oliphaunt-wasix-tools` + - host WASIX AOT and tools-AOT crates + - selected WASIX extension crates and extension-AOT crates +- [ ] Publish local npm packages to Verdaccio for root desktop examples. +- [ ] Update root examples so their manifests model the registry install path: + - native Tauri explicitly resolves the native tools artifact crate + - WASIX examples explicitly resolve the WASIX tools and tools-AOT artifact crates + - product-local WASIX example no longer uses path dependencies +- [ ] Exercise tool paths in example code, not only in dependency manifests: + - native example should execute a flow that requires packaged `pg_dump` + - WASIX example should execute a flow that requires packaged `pg_dump` + - WASIX example should compile with `psql` available from `oliphaunt-wasix-tools` +- [ ] Run `examples/tools/with-local-registries.sh` installs/builds for each root example. +- [ ] Run native and WASIX app smoke flows where available. + +## P1: CI and Release Shape + +- [ ] Verify CI lanes build and upload the artifact families now expected by examples: + - native runtime Cargo crates + - native tools Cargo crates + - broker Cargo crates + - WASIX runtime Cargo crates + - WASIX tools Cargo crates + - WASIX AOT crates + - WASIX tools-AOT crates + - extension runtime/AOT crates +- [ ] Verify release dry-runs publish the same package families to local registries. +- [ ] Keep release checks DRY: generation, validation, and publication should share one + package-family model per ecosystem. +- [ ] Validate local Linux CI lanes with a local GitHub Actions runner when practical. +- [ ] Document local runner limitations instead of pretending macOS, Windows, iOS, or + Android lanes were validated on Linux. + +## P1: SDK Consistency + +- [ ] Compare native runtime/tool/extension/ICU resolution across Rust, JS, React + Native, Swift, and Kotlin. +- [ ] Compare WASIX runtime/tool/AOT/extension/ICU resolution across Rust and JS-facing + examples. +- [ ] Remove subtle duplicate logic where one SDK has a stronger resolver or validator + than another. +- [ ] Ensure examples exercise the same control flows the SDKs document. + +## P2: Dead Code and Tooling Cleanup + +- [ ] Run dead-code scans for Rust, TypeScript, shell, and release scripts. +- [ ] Remove generated or stale example build outputs if they are tracked accidentally. +- [ ] Identify Python release scripts that can be moved to Bun without losing the + ecosystem fit or making release behavior harder to validate. +- [ ] Identify Rust xtask code that is not performance-sensitive or domain-critical and + can be moved to Bun without compiling unnecessary crates. +- [ ] Keep build/runtime-critical Rust and platform shell where they remain idiomatic. + +## Current Evidence + +- Native Linux x64 Cargo artifact generation now emits split payloads: + `liboliphaunt-native-linux-x64-gnu-part-000` through `part-006` contain the + root runtime, and `oliphaunt-tools-linux-x64-gnu-part-000` contains + `pg_dump` and `psql`. The generated `.crate` files are all below 10 MiB. +- Generated root native payload content has `postgres`, `initdb`, and `pg_ctl` + only; `pg_dump` and `psql` are present only in `oliphaunt-tools-*`. +- The local Cargo registry was refreshed from the split artifacts. The native + Tauri example regenerated its lockfile through `examples/tools/with-local-registries.sh`, + `cargo check` passed, and `startup_smoke_runs_sql_dump` passed through packaged + `pg_dump`. +- JS package-manager shape now mirrors Rust: `@oliphaunt/liboliphaunt-*` + packages carry the root native runtime, while `@oliphaunt/tools-*` packages + carry `pg_dump` and `psql`. `@oliphaunt/ts` keeps the user install path + unchanged by selecting both package families as optional dependencies. +- Current local WASIX release assets are stale: the new WASIX packager rejects + them because `oliphaunt.wasix.tar.zst` still contains `oliphaunt/bin/pg_dump`. + A fresh WASIX release asset build is required before WASIX example e2e can be + claimed. diff --git a/examples/README.md b/examples/README.md index fbe03eca..808df27f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -4,11 +4,15 @@ These examples keep the same todo schema across desktop shells: - `tauri`: Tauri v2 with the native Rust SDK. - `tauri-wasix`: Tauri v2 with `oliphaunt-wasix` and SQLx. -- `electron`: Electron with the TypeScript SDK and native broker mode. +- `electron`: Electron with the TypeScript SDK and native server mode. - `electron-wasix`: Electron with a Rust WASIX sidecar exposing a PostgreSQL URL. Each app opts into `hstore`, `pg_trgm`, and `unaccent`, then uses `hstore` -tags plus trigram/accent-insensitive search for the todo list. +tags plus trigram/accent-insensitive search for the todo list. Native examples +load `postgres`, `initdb`, and `pg_ctl` from `liboliphaunt-native-*`, while +`pg_dump` and `psql` come from `oliphaunt-tools-*`. WASIX examples load +`postgres` and `initdb` from the runtime crates and `pg_dump`/`psql` from +`oliphaunt-wasix-tools`; WASIX intentionally has no `pg_ctl`. Local registry artifacts for Linux x64 from CI run `28049923289` can be staged with: @@ -34,6 +38,11 @@ python3 tools/release/local_registry_publish.py publish \ --artifact-root target/local-registry-artifacts/oliphaunt-extension-package-artifacts ``` +The native packaging step emits both `liboliphaunt-native-linux-x64-gnu` and +`oliphaunt-tools-linux-x64-gnu`. The WASIX packaging step emits +`liboliphaunt-wasix-portable`, `oliphaunt-wasix-tools`, +`liboliphaunt-wasix-aot-*`, and `oliphaunt-wasix-tools-aot-*`. + Run examples through the local registry helper so Cargo resolves `registry = "oliphaunt-local"` and pnpm reads the local Verdaccio registry: @@ -42,5 +51,8 @@ examples/tools/with-local-registries.sh pnpm --dir examples/electron install examples/tools/with-local-registries.sh pnpm --dir examples/electron start ``` +The native examples run a SQL backup smoke through `pg_dump` during startup. +The WASIX examples run `dump_sql("--schema-only")` during startup. + On Linux, SwiftPM artifacts are staged for inspection and skipped for registry publish when `swift` is not installed. diff --git a/examples/electron-wasix/src-wasix/Cargo.toml b/examples/electron-wasix/src-wasix/Cargo.toml index 96558521..7ceaeee2 100644 --- a/examples/electron-wasix/src-wasix/Cargo.toml +++ b/examples/electron-wasix/src-wasix/Cargo.toml @@ -13,4 +13,9 @@ oliphaunt-wasix = { version = "=0.1.0", registry = "oliphaunt-local", features = "extension-pg-trgm", "extension-unaccent", ] } +oliphaunt-wasix-tools = { version = "=0.1.0", registry = "oliphaunt-local" } serde_json = "1" + +[target.'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))'.dependencies] +liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu = { version = "=0.1.0", registry = "oliphaunt-local" } +oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu = { version = "=0.1.0", registry = "oliphaunt-local" } diff --git a/examples/electron-wasix/src-wasix/src/main.rs b/examples/electron-wasix/src-wasix/src/main.rs index 632cb4e6..ff163fe5 100644 --- a/examples/electron-wasix/src-wasix/src/main.rs +++ b/examples/electron-wasix/src-wasix/src/main.rs @@ -4,16 +4,21 @@ use std::path::PathBuf; use std::thread; use anyhow::{Context, Result, bail}; -use oliphaunt_wasix::{extensions, OliphauntServer}; +use oliphaunt_wasix::{OliphauntServer, PgDumpOptions, extensions}; use serde_json::json; fn main() -> Result<()> { let root = parse_root()?; let server = OliphauntServer::builder() .path(root) - .extensions([extensions::HSTORE, extensions::PG_TRGM, extensions::UNACCENT]) + .extensions([ + extensions::HSTORE, + extensions::PG_TRGM, + extensions::UNACCENT, + ]) .start() .context("start oliphaunt-wasix server")?; + validate_wasix_tools(&server)?; println!("{}", json!({ "databaseUrl": server.connection_uri() })); io::stdout().flush()?; let _server = server; @@ -22,6 +27,15 @@ fn main() -> Result<()> { } } +fn validate_wasix_tools(server: &OliphauntServer) -> Result<()> { + let dump = server.dump_sql(PgDumpOptions::new().arg("--schema-only"))?; + anyhow::ensure!( + dump.contains("PostgreSQL database dump"), + "pg_dump SQL backup smoke did not look like a PostgreSQL dump" + ); + Ok(()) +} + fn parse_root() -> Result { let mut args = env::args().skip(1); while let Some(arg) = args.next() { diff --git a/examples/electron/README.md b/examples/electron/README.md index f8acfe37..dbf5cebe 100644 --- a/examples/electron/README.md +++ b/examples/electron/README.md @@ -1,7 +1,7 @@ # Electron Native Todo Electron owns the Oliphaunt TypeScript SDK in the main process and exposes a -small IPC surface to the renderer through preload. The app uses `nativeBroker` +small IPC surface to the renderer through preload. The app uses `nativeServer` mode with a persistent root under Electron's user data directory. ```sh diff --git a/examples/electron/package.json b/examples/electron/package.json index c0a18a08..dc687cb1 100644 --- a/examples/electron/package.json +++ b/examples/electron/package.json @@ -13,10 +13,12 @@ "@oliphaunt/extension-pg-trgm": "0.1.0", "@oliphaunt/extension-unaccent": "0.1.0", "@oliphaunt/ts": "0.1.0", - "kysely": "^0.29.2" + "kysely": "^0.29.2", + "pg": "^8.16.3" }, "devDependencies": { "@types/node": "^24.10.1", + "@types/pg": "^8.15.6", "electron": "^39.2.5", "typescript": "catalog:", "vite": "^6.0.3" diff --git a/examples/electron/src/oliphaunt-kysely.ts b/examples/electron/src/oliphaunt-kysely.ts deleted file mode 100644 index 071ca89d..00000000 --- a/examples/electron/src/oliphaunt-kysely.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { - CompiledQuery, - PostgresAdapter, - PostgresIntrospector, - PostgresQueryCompiler, - type AbortableOperationOptions, - type DatabaseConnection, - type DatabaseIntrospector, - type Dialect, - type DialectAdapter, - type Driver, - type Kysely, - type QueryCompiler, - type QueryResult as KyselyQueryResult, - type TransactionSettings, -} from "kysely"; - -import type { OliphauntDatabase, QueryParam } from "@oliphaunt/ts"; - -export class OliphauntDialect implements Dialect { - constructor(private readonly db: OliphauntDatabase) {} - - createDriver(): Driver { - return new OliphauntDriver(this.db); - } - - createQueryCompiler(): QueryCompiler { - return new PostgresQueryCompiler(); - } - - createAdapter(): DialectAdapter { - return new PostgresAdapter(); - } - - createIntrospector(db: Kysely): DatabaseIntrospector { - return new PostgresIntrospector(db); - } -} - -class OliphauntDriver implements Driver { - private readonly connection: OliphauntConnection; - - constructor(db: OliphauntDatabase) { - this.connection = new OliphauntConnection(db); - } - - async init(_options?: AbortableOperationOptions): Promise {} - - async acquireConnection(_options?: AbortableOperationOptions): Promise { - return this.connection; - } - - async beginTransaction( - connection: DatabaseConnection, - settings: TransactionSettings, - ): Promise { - let statement = "begin"; - if (settings.isolationLevel || settings.accessMode) { - statement = "start transaction"; - if (settings.isolationLevel) statement += ` isolation level ${settings.isolationLevel}`; - if (settings.accessMode) statement += ` ${settings.accessMode}`; - } - await connection.executeQuery(CompiledQuery.raw(statement)); - } - - async commitTransaction(connection: DatabaseConnection): Promise { - await connection.executeQuery(CompiledQuery.raw("commit")); - } - - async rollbackTransaction(connection: DatabaseConnection): Promise { - await connection.executeQuery(CompiledQuery.raw("rollback")); - } - - async releaseConnection( - _connection: DatabaseConnection, - _options?: AbortableOperationOptions, - ): Promise {} - - async destroy(_options?: AbortableOperationOptions): Promise {} -} - -class OliphauntConnection implements DatabaseConnection { - constructor(private readonly db: OliphauntDatabase) {} - - async executeQuery(compiledQuery: CompiledQuery): Promise> { - const result = await this.db.query( - compiledQuery.sql, - compiledQuery.parameters.map(toQueryParam), - ); - const rows = result.rows.map((_, rowIndex) => { - const row: Record = {}; - for (const field of result.fields) { - row[field.name] = result.getText(rowIndex, field.name); - } - return row as R; - }); - return { - numAffectedRows: affectedRows(result.commandTag), - rows, - }; - } - - async *streamQuery( - _compiledQuery: CompiledQuery, - _chunkSize: number, - _options?: AbortableOperationOptions, - ): AsyncIterableIterator> { - throw new Error("Streaming is not supported by the Oliphaunt Kysely example dialect."); - } -} - -function toQueryParam(value: unknown): QueryParam { - if ( - value === null || - typeof value === "string" || - typeof value === "number" || - typeof value === "boolean" - ) { - return value; - } - if (value instanceof Uint8Array || value instanceof ArrayBuffer || ArrayBuffer.isView(value)) { - return value; - } - throw new Error(`unsupported Oliphaunt query parameter: ${typeof value}`); -} - -function affectedRows(commandTag: string | undefined): bigint | undefined { - if (!commandTag) return undefined; - const command = commandTag.split(/\s+/, 1)[0]; - if (command !== "INSERT" && command !== "UPDATE" && command !== "DELETE" && command !== "MERGE") { - return undefined; - } - const count = Number(commandTag.trim().split(/\s+/).at(-1)); - return Number.isFinite(count) ? BigInt(count) : undefined; -} diff --git a/examples/electron/src/todos.ts b/examples/electron/src/todos.ts index adaa5e2f..117a5e9d 100644 --- a/examples/electron/src/todos.ts +++ b/examples/electron/src/todos.ts @@ -1,11 +1,13 @@ import { join } from "node:path"; import { Oliphaunt, type OliphauntDatabase } from "@oliphaunt/ts"; -import { Kysely, sql, type Generated } from "kysely"; +import { Kysely, PostgresDialect, sql, type Generated } from "kysely"; +import pg from "pg"; -import { OliphauntDialect } from "./oliphaunt-kysely.js"; import type { CreateTodoInput, StatusFilter, Todo } from "./types.js"; +const { Pool } = pg; + type TodoTable = { id: Generated; title: string; @@ -64,19 +66,38 @@ export function getDatabase(userData: string) { async function openDatabase(userData: string): Promise { const native = await Oliphaunt.open({ - engine: "nativeBroker", + engine: "nativeServer", root: join(userData, "oliphaunt-native-todos"), extensions: ["hstore", "pg_trgm", "unaccent"], + maxClientSessions: 4, }); + const connectionString = await native.connectionString(); + if (!connectionString) { + throw new Error("nativeServer did not expose a PostgreSQL connection string"); + } const db = new Kysely({ - dialect: new OliphauntDialect(native), + dialect: new PostgresDialect({ + pool: new Pool({ + connectionString, + max: 2, + }), + }), }); for (const statement of schemaStatements) { await sql.raw(statement).execute(db); } + await validateSqlBackup(native); return { native, db }; } +async function validateSqlBackup(native: OliphauntDatabase) { + const backup = await native.backup("sql"); + const dump = Buffer.from(backup.bytes).toString("utf8"); + if (!dump.includes("PostgreSQL database dump")) { + throw new Error("pg_dump SQL backup smoke did not look like a PostgreSQL dump"); + } +} + export async function listTodos( userData: string, filter: { search: string; status: StatusFilter }, diff --git a/examples/tauri-wasix/src-tauri/Cargo.toml b/examples/tauri-wasix/src-tauri/Cargo.toml index a0d3acd7..6662b1a1 100644 --- a/examples/tauri-wasix/src-tauri/Cargo.toml +++ b/examples/tauri-wasix/src-tauri/Cargo.toml @@ -21,8 +21,13 @@ oliphaunt-wasix = { version = "=0.1.0", registry = "oliphaunt-local", features = "extension-pg-trgm", "extension-unaccent", ] } +oliphaunt-wasix-tools = { version = "=0.1.0", registry = "oliphaunt-local" } serde = { version = "1", features = ["derive"] } sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio-rustls", "postgres"] } tauri = { version = "2", features = [] } thiserror = "2" tokio = { version = "1", features = ["sync"] } + +[target.'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))'.dependencies] +liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu = { version = "=0.1.0", registry = "oliphaunt-local" } +oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu = { version = "=0.1.0", registry = "oliphaunt-local" } diff --git a/examples/tauri-wasix/src-tauri/src/lib.rs b/examples/tauri-wasix/src-tauri/src/lib.rs index 777060d2..deedbe90 100644 --- a/examples/tauri-wasix/src-tauri/src/lib.rs +++ b/examples/tauri-wasix/src-tauri/src/lib.rs @@ -2,9 +2,9 @@ use std::path::PathBuf; use std::time::Duration; use anyhow::{Context, Result}; -use oliphaunt_wasix::{extensions, OliphauntServer}; -use serde::{Deserialize, Serialize}; +use oliphaunt_wasix::{OliphauntServer, PgDumpOptions, extensions}; use serde::ser::Serializer; +use serde::{Deserialize, Serialize}; use sqlx::postgres::PgPoolOptions; use sqlx::{PgPool, Row}; use tauri::Manager; @@ -29,7 +29,8 @@ CREATE TABLE IF NOT EXISTS todos ( ) "#; -const CREATE_INDEX: &str = "CREATE INDEX IF NOT EXISTS todos_title_trgm ON todos USING gin (title gin_trgm_ops)"; +const CREATE_INDEX: &str = + "CREATE INDEX IF NOT EXISTS todos_title_trgm ON todos USING gin (title gin_trgm_ops)"; const SELECT_TODOS: &str = r#" SELECT @@ -135,9 +136,14 @@ impl From for CommandError { async fn open_database(root: PathBuf) -> Result { let server = OliphauntServer::builder() .path(root) - .extensions([extensions::HSTORE, extensions::PG_TRGM, extensions::UNACCENT]) + .extensions([ + extensions::HSTORE, + extensions::PG_TRGM, + extensions::UNACCENT, + ]) .start() .context("start oliphaunt-wasix server")?; + validate_wasix_tools(&server)?; let pool = PgPoolOptions::new() .max_connections(1) .acquire_timeout(Duration::from_secs(30)) @@ -160,6 +166,15 @@ async fn init_schema(pool: &PgPool) -> Result<()> { Ok(()) } +fn validate_wasix_tools(server: &OliphauntServer) -> Result<()> { + let dump = server.dump_sql(PgDumpOptions::new().arg("--schema-only"))?; + anyhow::ensure!( + dump.contains("PostgreSQL database dump"), + "pg_dump SQL backup smoke did not look like a PostgreSQL dump" + ); + Ok(()) +} + #[tauri::command] async fn list_todos( state: tauri::State<'_, TodoStore>, diff --git a/examples/tauri/src-tauri/Cargo.lock b/examples/tauri/src-tauri/Cargo.lock index 826a857d..97735068 100644 --- a/examples/tauri/src-tauri/Cargo.lock +++ b/examples/tauri/src-tauri/Cargo.lock @@ -1714,7 +1714,7 @@ dependencies = [ name = "liboliphaunt-native-linux-x64-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "a20540ee7e2178c23667bf8fef269a6bcadadf5906a899aa413a6c7880d48987" +checksum = "dbbed43b4d8c1a57433def7020f33c01a2b10eba72edfad7b77c80be516e8eb8" dependencies = [ "liboliphaunt-native-linux-x64-gnu-part-000", "liboliphaunt-native-linux-x64-gnu-part-001", @@ -1723,18 +1723,6 @@ dependencies = [ "liboliphaunt-native-linux-x64-gnu-part-004", "liboliphaunt-native-linux-x64-gnu-part-005", "liboliphaunt-native-linux-x64-gnu-part-006", - "liboliphaunt-native-linux-x64-gnu-part-007", - "liboliphaunt-native-linux-x64-gnu-part-008", - "liboliphaunt-native-linux-x64-gnu-part-009", - "liboliphaunt-native-linux-x64-gnu-part-010", - "liboliphaunt-native-linux-x64-gnu-part-011", - "liboliphaunt-native-linux-x64-gnu-part-012", - "liboliphaunt-native-linux-x64-gnu-part-013", - "liboliphaunt-native-linux-x64-gnu-part-014", - "liboliphaunt-native-linux-x64-gnu-part-015", - "liboliphaunt-native-linux-x64-gnu-part-016", - "liboliphaunt-native-linux-x64-gnu-part-017", - "liboliphaunt-native-linux-x64-gnu-part-018", "sha2", ] @@ -1742,115 +1730,43 @@ dependencies = [ name = "liboliphaunt-native-linux-x64-gnu-part-000" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "ce8496e2a86e7f70827318ce04432103d707394ca181b0dcc72f8a4852546ba2" +checksum = "520041a055281a65b0e300ea4d6c8113a2bcd08f4c9ef95393342ffbf1232351" [[package]] name = "liboliphaunt-native-linux-x64-gnu-part-001" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "062b06f96ae1eaf3deacf1f862937c60f5db2443a231d291cc778d225b69d9e8" +checksum = "4f38eeb858943d8587fbf9dc4ad6d86f3b993eb4154c50135c2f22378285373e" [[package]] name = "liboliphaunt-native-linux-x64-gnu-part-002" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "00a4200ba9455997a383d791554f4972765967b5f4695e6a5d10eb341d28a62e" +checksum = "b6ba9d8dbd493f4ca293108a70344154d188073b287a51defd0ff4b6e59217de" [[package]] name = "liboliphaunt-native-linux-x64-gnu-part-003" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "2b13d309c1c023db07edc2da1c8690b48e7950c680b8e5bdbd41749e6ac22e49" +checksum = "74f1d81d6d570a5cf189b816e503dc2087d107675bb6137b388322bd8f35fd9e" [[package]] name = "liboliphaunt-native-linux-x64-gnu-part-004" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "c374711cef7606ea3901a8a27530dbb101dbcb640ff3650ee624462be87bed99" +checksum = "043608eb604121ea3201a4a24825d95d4205808b7ff933bf94b5e02eae5842c4" [[package]] name = "liboliphaunt-native-linux-x64-gnu-part-005" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "aff19d3622276f94e1c6e5185cff6d50c1465e6dc7549a8a5b3d4c6d1f6e49b8" +checksum = "bc5c16a1cd47f5f90bb94288d3c9ff6f201139a98a31571aa0479308d9884b6a" [[package]] name = "liboliphaunt-native-linux-x64-gnu-part-006" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "b3868b1cc083adba8bc3e1f4a88b3e00dfb2a41b238faca0c33d19bb8b65085c" - -[[package]] -name = "liboliphaunt-native-linux-x64-gnu-part-007" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "359c929c00e676da0cd10c221736582a9d929a58a7d77ee8dacd348fc0d9fbb4" - -[[package]] -name = "liboliphaunt-native-linux-x64-gnu-part-008" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "1dc32627ee552289518c60d9dd4afa992b2828c2cfc112a3f2f4f6947142974a" - -[[package]] -name = "liboliphaunt-native-linux-x64-gnu-part-009" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "9f72606f36ebcf593d762a0bc3739d2d2ce35f8beb6ddbbd136666de5be9872a" - -[[package]] -name = "liboliphaunt-native-linux-x64-gnu-part-010" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "39d44995ec52d4c297d59c9d7ea3279448996dc499ef1fe9819824c8b625747f" - -[[package]] -name = "liboliphaunt-native-linux-x64-gnu-part-011" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "bdd00410be5ddb58573acdcfef27485f7c9bf2e629f0fb423ed99c24f5e3cef4" - -[[package]] -name = "liboliphaunt-native-linux-x64-gnu-part-012" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "9f00bc29f1ed6220a7b036ea2c94eedc6c4342af83de54936a50e515869ffbd4" - -[[package]] -name = "liboliphaunt-native-linux-x64-gnu-part-013" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "b28ba903db0cbc308c50bb86cbd896b273a39f78d508b8da05f3f23f90414831" - -[[package]] -name = "liboliphaunt-native-linux-x64-gnu-part-014" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "eb5787d47861b6daee4435c67004ca2d9e272230ada3c43a1988f359573199b4" - -[[package]] -name = "liboliphaunt-native-linux-x64-gnu-part-015" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "13f81bec6d95b4d032969687881419dcb49621b153478f7b80509207be10730c" - -[[package]] -name = "liboliphaunt-native-linux-x64-gnu-part-016" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "2a2ffe641f6f1651c908d9996430bcd1ae844bbf4d85773163c227ca53ee0705" - -[[package]] -name = "liboliphaunt-native-linux-x64-gnu-part-017" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "719bb0e2c435631a3b449818b3495689be6b9ab96cbcd402c2f7beb64581ebbf" - -[[package]] -name = "liboliphaunt-native-linux-x64-gnu-part-018" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "09b4016ecd42044254f8707343d6ad9b1fd68bad81861d00a43dde2cdf29cce4" +checksum = "6560450839c262fa76b36ab35e8eb4d84e1a736c49f77bf4f6bd57114eb5772a" [[package]] name = "libredox" @@ -2228,7 +2144,7 @@ dependencies = [ name = "oliphaunt" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "60d9438f9208c76d8c5da49e450d76fe8829d3739562c710ec14ed8bfef6a790" +checksum = "b7037b1836ef8e0cda38807c553d54ba3f40de2c4054c1c99a02ca4b124af12d" dependencies = [ "crossbeam-channel", "flate2", @@ -2237,6 +2153,7 @@ dependencies = [ "libloading 0.8.9", "liboliphaunt-native-linux-x64-gnu", "oliphaunt-broker-linux-x64-gnu", + "oliphaunt-tools-linux-x64-gnu", "serde", "sha2", "tar", @@ -2255,7 +2172,7 @@ checksum = "e8789d11e7ee362e2dce2cdf0487cc5a06a3e58441761c02b8f0ba2e27c95765" name = "oliphaunt-build" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "6c342a63fd9162f1594885093d164e275cfed43a5b8af49f831a40d498286d9c" +checksum = "486249fc71f0087353b0fa81e3f3a07007fb8eab33e7586f3de6283b3b16662d" dependencies = [ "serde", "sha2", @@ -2274,6 +2191,7 @@ dependencies = [ "oliphaunt-extension-hstore-linux-x64-gnu", "oliphaunt-extension-pg-trgm-linux-x64-gnu", "oliphaunt-extension-unaccent-linux-x64-gnu", + "oliphaunt-tools-linux-x64-gnu", "serde", "tauri", "tauri-build", @@ -2308,6 +2226,22 @@ dependencies = [ "sha2", ] +[[package]] +name = "oliphaunt-tools-linux-x64-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "e742596e96c3ee6f4b78774497fbfdc2dfb87c5474f336f3f999c25ce95f2c38" +dependencies = [ + "oliphaunt-tools-linux-x64-gnu-part-000", + "sha2", +] + +[[package]] +name = "oliphaunt-tools-linux-x64-gnu-part-000" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "dba5682416ca2fb0ed7ea5d36cad304962f064898469211ef5c1b1063159f6b7" + [[package]] name = "once_cell" version = "1.21.4" diff --git a/examples/tauri/src-tauri/Cargo.toml b/examples/tauri/src-tauri/Cargo.toml index af6c3a19..82d75d0b 100644 --- a/examples/tauri/src-tauri/Cargo.toml +++ b/examples/tauri/src-tauri/Cargo.toml @@ -31,6 +31,7 @@ tokio = { version = "1", features = ["sync"] } [target.'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))'.dependencies] liboliphaunt-native-linux-x64-gnu = { version = "=0.1.0", registry = "oliphaunt-local" } oliphaunt-broker-linux-x64-gnu = { version = "=0.1.0", registry = "oliphaunt-local" } +oliphaunt-tools-linux-x64-gnu = { version = "=0.1.0", registry = "oliphaunt-local" } oliphaunt-extension-hstore-linux-x64-gnu = { version = "=0.1.0", registry = "oliphaunt-local" } oliphaunt-extension-pg-trgm-linux-x64-gnu = { version = "=0.1.0", registry = "oliphaunt-local" } oliphaunt-extension-unaccent-linux-x64-gnu = { version = "=0.1.0", registry = "oliphaunt-local" } diff --git a/examples/tauri/src-tauri/src/lib.rs b/examples/tauri/src-tauri/src/lib.rs index d1de354b..d9721966 100644 --- a/examples/tauri/src-tauri/src/lib.rs +++ b/examples/tauri/src-tauri/src/lib.rs @@ -1,8 +1,8 @@ use std::path::PathBuf; -use oliphaunt::{Extension, Oliphaunt, QueryResult}; -use serde::{Deserialize, Serialize}; +use oliphaunt::{BackupRequest, Extension, Oliphaunt, QueryResult}; use serde::ser::Serializer; +use serde::{Deserialize, Serialize}; use tauri::Manager; use tokio::sync::Mutex; @@ -123,16 +123,29 @@ impl From for CommandError { } async fn open_database(root: PathBuf) -> anyhow::Result { + oliphaunt::register_build_resources!()?; let db = Oliphaunt::builder() .path(root) - .native_direct() + .native_server() + .max_client_sessions(4) .extensions([Extension::Hstore, Extension::PgTrgm, Extension::Unaccent]) .open() .await?; db.execute(SCHEMA).await?; + validate_sql_dump(&db).await?; Ok(db) } +async fn validate_sql_dump(db: &Oliphaunt) -> anyhow::Result<()> { + let backup = db.backup(BackupRequest::sql()).await?; + let sql = std::str::from_utf8(&backup.bytes)?; + anyhow::ensure!( + sql.contains("PostgreSQL database dump"), + "pg_dump SQL backup smoke did not look like a PostgreSQL dump" + ); + Ok(()) +} + #[tauri::command] async fn list_todos( state: tauri::State<'_, TodoStore>, @@ -159,7 +172,13 @@ async fn create_todo( let result = db .query_params( &sql, - [input.title, input.notes, input.area, input.context, priority], + [ + input.title, + input.notes, + input.area, + input.context, + priority, + ], ) .await?; one_todo(&result).map_err(CommandError::from) @@ -181,13 +200,18 @@ async fn toggle_todo(state: tauri::State<'_, TodoStore>, id: i64) -> Result, id: i64) -> Result<(), CommandError> { let db = state.db.lock().await; - db.query_params("DELETE FROM todos WHERE id = $1 RETURNING id::text AS id", [id]) - .await?; + db.query_params( + "DELETE FROM todos WHERE id = $1 RETURNING id::text AS id", + [id], + ) + .await?; Ok(()) } fn todos_from_result(result: &QueryResult) -> anyhow::Result> { - (0..result.row_count()).map(|row| todo_from_result(result, row)).collect() + (0..result.row_count()) + .map(|row| todo_from_result(result, row)) + .collect() } fn one_todo(result: &QueryResult) -> anyhow::Result { @@ -232,3 +256,20 @@ pub fn run() { .run(tauri::generate_context!()) .expect("error while running tauri application"); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn startup_smoke_runs_sql_dump() { + let root = std::env::temp_dir().join(format!( + "oliphaunt-example-tauri-smoke-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + let db = tauri::async_runtime::block_on(open_database(root.clone())).unwrap(); + tauri::async_runtime::block_on(db.close()).unwrap(); + let _ = std::fs::remove_dir_all(root); + } +} diff --git a/examples/tools/check-examples.sh b/examples/tools/check-examples.sh index 91467234..856740c2 100755 --- a/examples/tools/check-examples.sh +++ b/examples/tools/check-examples.sh @@ -59,6 +59,14 @@ reject_text() { fi } +reject_file() { + local path="$1" + if [[ -e "$path" ]]; then + echo "forbidden stale example file: $path" >&2 + exit 1 + fi +} + require_file "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/package.json" require_file "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml" require_text "src/bindings/wasix-rust/moon.yml" '^ example-check:$' @@ -80,16 +88,29 @@ require_text "examples/electron/package.json" '"@oliphaunt/ts": "0\.1\.0"' require_text "examples/electron/package.json" '"@oliphaunt/extension-hstore": "0\.1\.0"' require_text "examples/electron/package.json" '"@oliphaunt/extension-pg-trgm": "0\.1\.0"' require_text "examples/electron/package.json" '"@oliphaunt/extension-unaccent": "0\.1\.0"' +require_text "examples/electron/package.json" '"pg": "\^8\.16\.3"' +reject_file "examples/electron/src/oliphaunt-kysely.ts" require_text "examples/tauri/src-tauri/Cargo.toml" 'registry = "oliphaunt-local"' +require_text "examples/tauri/src-tauri/Cargo.toml" 'oliphaunt-tools-linux-x64-gnu' require_text "examples/tauri/src-tauri/Cargo.toml" 'oliphaunt-extension-hstore-linux-x64-gnu' require_text "examples/tauri/src-tauri/Cargo.toml" 'oliphaunt-extension-pg-trgm-linux-x64-gnu' require_text "examples/tauri/src-tauri/Cargo.toml" 'oliphaunt-extension-unaccent-linux-x64-gnu' require_text "examples/tauri-wasix/src-tauri/Cargo.toml" 'registry = "oliphaunt-local"' +require_text "examples/tauri-wasix/src-tauri/Cargo.toml" 'oliphaunt-wasix-tools' +require_text "examples/tauri-wasix/src-tauri/Cargo.toml" 'liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu' +require_text "examples/tauri-wasix/src-tauri/Cargo.toml" 'oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu' require_text "examples/electron-wasix/src-wasix/Cargo.toml" 'registry = "oliphaunt-local"' +require_text "examples/electron-wasix/src-wasix/Cargo.toml" 'oliphaunt-wasix-tools' +require_text "examples/electron-wasix/src-wasix/Cargo.toml" 'liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu' +require_text "examples/electron-wasix/src-wasix/Cargo.toml" 'oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu' +require_text "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml" 'registry = "oliphaunt-local"' +require_text "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml" 'oliphaunt-wasix-tools' +require_text "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml" 'oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu' reject_text "examples/electron/package.json" '"@oliphaunt/ts": "workspace:\*"' reject_text "examples/tauri/src-tauri/Cargo.toml" 'path = "../../../src/sdks/rust' reject_text "examples/tauri-wasix/src-tauri/Cargo.toml" 'path = "../../../src/bindings/wasix-rust' reject_text "examples/electron-wasix/src-wasix/Cargo.toml" 'path = "../../../src/bindings/wasix-rust' +reject_text "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml" 'path = "../../../crates/oliphaunt-wasix"' require_file "src/sdks/react-native/examples/expo/package.json" require_file "src/sdks/react-native/examples/expo/maestro/installed-smoke.yaml" diff --git a/examples/tools/check-lockfiles.sh b/examples/tools/check-lockfiles.sh index 2a4183b2..54f68a25 100755 --- a/examples/tools/check-lockfiles.sh +++ b/examples/tools/check-lockfiles.sh @@ -22,15 +22,24 @@ if ! git rev-parse --verify -q "${base_ref}^{commit}" >/dev/null; then fi changed="$( - git diff --name-only "${base_ref}...HEAD" -- \ - Cargo.toml \ - Cargo.lock \ - src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml \ - src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml \ - src/runtimes/liboliphaunt/wasix/crates/aot \ - src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock \ - examples/tools/check-lockfiles.sh \ - tools/release/sync-example-lockfiles.py + git diff --name-only "${base_ref}...HEAD" -- \ + Cargo.toml \ + Cargo.lock \ + src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml \ + src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml \ + src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml \ + src/runtimes/liboliphaunt/wasix/crates/aot \ + src/runtimes/liboliphaunt/wasix/crates/tools-aot \ + src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml \ + src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock \ + examples/tauri/src-tauri/Cargo.toml \ + examples/tauri/src-tauri/Cargo.lock \ + examples/tauri-wasix/src-tauri/Cargo.toml \ + examples/tauri-wasix/src-tauri/Cargo.lock \ + examples/electron-wasix/src-wasix/Cargo.toml \ + examples/electron-wasix/src-wasix/Cargo.lock \ + examples/tools/check-lockfiles.sh \ + tools/release/sync-example-lockfiles.py )" if [[ -z "$changed" ]]; then diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dbea3ee7..dcae7227 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,10 +43,16 @@ importers: kysely: specifier: ^0.29.2 version: 0.29.2 + pg: + specifier: ^8.16.3 + version: 8.22.0 devDependencies: '@types/node': specifier: ^24.10.1 version: 24.12.4 + '@types/pg': + specifier: ^8.15.6 + version: 8.20.0 electron: specifier: ^39.2.5 version: 39.8.10 @@ -221,6 +227,14 @@ importers: src/runtimes/liboliphaunt/native/packages/win32-x64-msvc: {} + src/runtimes/liboliphaunt/native/tools-packages/darwin-arm64: {} + + src/runtimes/liboliphaunt/native/tools-packages/linux-arm64-gnu: {} + + src/runtimes/liboliphaunt/native/tools-packages/linux-x64-gnu: {} + + src/runtimes/liboliphaunt/native/tools-packages/win32-x64-msvc: {} + src/runtimes/node-direct: devDependencies: node-api-headers: @@ -295,6 +309,18 @@ importers: '@oliphaunt/node-direct-win32-x64-msvc': specifier: workspace:0.1.0 version: link:../../runtimes/node-direct/packages/win32-x64-msvc + '@oliphaunt/tools-darwin-arm64': + specifier: workspace:0.1.0 + version: link:../../runtimes/liboliphaunt/native/tools-packages/darwin-arm64 + '@oliphaunt/tools-linux-arm64-gnu': + specifier: workspace:0.1.0 + version: link:../../runtimes/liboliphaunt/native/tools-packages/linux-arm64-gnu + '@oliphaunt/tools-linux-x64-gnu': + specifier: workspace:0.1.0 + version: link:../../runtimes/liboliphaunt/native/tools-packages/linux-x64-gnu + '@oliphaunt/tools-win32-x64-msvc': + specifier: workspace:0.1.0 + version: link:../../runtimes/liboliphaunt/native/tools-packages/win32-x64-msvc src/sdks/react-native: devDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d9f8d951..eb499fc9 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,6 +3,7 @@ packages: - "src/sdks/js" - "src/runtimes/liboliphaunt/native/icu-npm" - "src/runtimes/liboliphaunt/native/packages/*" + - "src/runtimes/liboliphaunt/native/tools-packages/*" - "src/runtimes/broker/packages/*" - "src/runtimes/node-direct" - "src/runtimes/node-direct/packages/*" diff --git a/release-please-config.json b/release-please-config.json index 77a8dcbe..b7d7e1ba 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -38,6 +38,26 @@ "path": "packages/win32-x64-msvc/package.json", "jsonpath": "$.version" }, + { + "type": "json", + "path": "tools-packages/darwin-arm64/package.json", + "jsonpath": "$.version" + }, + { + "type": "json", + "path": "tools-packages/linux-arm64-gnu/package.json", + "jsonpath": "$.version" + }, + { + "type": "json", + "path": "tools-packages/linux-x64-gnu/package.json", + "jsonpath": "$.version" + }, + { + "type": "json", + "path": "tools-packages/win32-x64-msvc/package.json", + "jsonpath": "$.version" + }, { "type": "json", "path": "icu-npm/package.json", diff --git a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml index 982a9393..717f6f9c 100644 --- a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml +++ b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml @@ -17,7 +17,8 @@ tauri-build = { version = "2", features = [] } [dependencies] anyhow = "1" -oliphaunt-wasix = { path = "../../../crates/oliphaunt-wasix" } +oliphaunt-wasix = { version = "=0.1.0", registry = "oliphaunt-local", features = ["extensions"] } +oliphaunt-wasix-tools = { version = "=0.1.0", registry = "oliphaunt-local" } sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio-rustls", "postgres"] } tauri = { version = "2", features = [] } tauri-plugin-opener = "2" @@ -25,3 +26,7 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" thiserror = "2" tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync"] } + +[target.'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))'.dependencies] +liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu = { version = "=0.1.0", registry = "oliphaunt-local" } +oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu = { version = "=0.1.0", registry = "oliphaunt-local" } diff --git a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs index 997584ea..4f4401da 100644 --- a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs +++ b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs @@ -3,8 +3,10 @@ use std::future::Future; use std::path::PathBuf; use std::time::{Duration, Instant}; -use anyhow::{anyhow, bail, Context, Result}; -use oliphaunt_wasix::{install_into, preload_runtime_module, OliphauntPaths, OliphauntServer}; +use anyhow::{Context, Result, anyhow, bail}; +use oliphaunt_wasix::{ + OliphauntPaths, OliphauntServer, PgDumpOptions, install_into, preload_runtime_module, +}; use serde::Serialize; use sqlx::postgres::{PgConnectOptions, PgPoolOptions, PgSslMode}; use sqlx::{PgPool, Row}; @@ -120,6 +122,7 @@ impl DatabaseHarness { preferred_server(server_root) }) .await?; + validate_wasix_tools(&server)?; let database_url = server.connection_uri(); let pool = time_async(&mut startup, "sqlx pool connect", async { @@ -329,6 +332,18 @@ impl DatabaseHarness { } } +fn validate_wasix_tools(server: &OliphauntServer) -> Result<()> { + if server.tcp_addr().is_none() { + return Ok(()); + } + let dump = server.dump_sql(PgDumpOptions::new().arg("--schema-only"))?; + anyhow::ensure!( + dump.contains("PostgreSQL database dump"), + "pg_dump SQL backup smoke did not look like a PostgreSQL dump" + ); + Ok(()) +} + fn preferred_server(root: PathBuf) -> Result { let builder = OliphauntServer::builder().path(&root); #[cfg(unix)] diff --git a/src/runtimes/liboliphaunt/native/packages/darwin-arm64/package.json b/src/runtimes/liboliphaunt/native/packages/darwin-arm64/package.json index e23753aa..5d22f566 100644 --- a/src/runtimes/liboliphaunt/native/packages/darwin-arm64/package.json +++ b/src/runtimes/liboliphaunt/native/packages/darwin-arm64/package.json @@ -27,9 +27,7 @@ "executableFiles": [ "./runtime/bin/initdb", "./runtime/bin/pg_ctl", - "./runtime/bin/pg_dump", - "./runtime/bin/postgres", - "./runtime/bin/psql" + "./runtime/bin/postgres" ] }, "files": [ diff --git a/src/runtimes/liboliphaunt/native/packages/linux-arm64-gnu/package.json b/src/runtimes/liboliphaunt/native/packages/linux-arm64-gnu/package.json index 18f6a926..5931eac3 100644 --- a/src/runtimes/liboliphaunt/native/packages/linux-arm64-gnu/package.json +++ b/src/runtimes/liboliphaunt/native/packages/linux-arm64-gnu/package.json @@ -30,9 +30,7 @@ "executableFiles": [ "./runtime/bin/initdb", "./runtime/bin/pg_ctl", - "./runtime/bin/pg_dump", - "./runtime/bin/postgres", - "./runtime/bin/psql" + "./runtime/bin/postgres" ] }, "files": [ diff --git a/src/runtimes/liboliphaunt/native/packages/linux-x64-gnu/package.json b/src/runtimes/liboliphaunt/native/packages/linux-x64-gnu/package.json index 016ca1eb..5e9bd4c0 100644 --- a/src/runtimes/liboliphaunt/native/packages/linux-x64-gnu/package.json +++ b/src/runtimes/liboliphaunt/native/packages/linux-x64-gnu/package.json @@ -30,9 +30,7 @@ "executableFiles": [ "./runtime/bin/initdb", "./runtime/bin/pg_ctl", - "./runtime/bin/pg_dump", - "./runtime/bin/postgres", - "./runtime/bin/psql" + "./runtime/bin/postgres" ] }, "files": [ diff --git a/src/runtimes/liboliphaunt/native/packages/win32-x64-msvc/package.json b/src/runtimes/liboliphaunt/native/packages/win32-x64-msvc/package.json index e476f80c..db5a62fc 100644 --- a/src/runtimes/liboliphaunt/native/packages/win32-x64-msvc/package.json +++ b/src/runtimes/liboliphaunt/native/packages/win32-x64-msvc/package.json @@ -27,9 +27,7 @@ "executableFiles": [ "./runtime/bin/initdb.exe", "./runtime/bin/pg_ctl.exe", - "./runtime/bin/pg_dump.exe", - "./runtime/bin/postgres.exe", - "./runtime/bin/psql.exe" + "./runtime/bin/postgres.exe" ] }, "files": [ diff --git a/src/runtimes/liboliphaunt/native/release.toml b/src/runtimes/liboliphaunt/native/release.toml index b2744667..8d0edc76 100644 --- a/src/runtimes/liboliphaunt/native/release.toml +++ b/src/runtimes/liboliphaunt/native/release.toml @@ -16,6 +16,10 @@ registry_packages = [ "npm:@oliphaunt/liboliphaunt-linux-x64-gnu", "npm:@oliphaunt/liboliphaunt-linux-arm64-gnu", "npm:@oliphaunt/liboliphaunt-win32-x64-msvc", + "npm:@oliphaunt/tools-darwin-arm64", + "npm:@oliphaunt/tools-linux-x64-gnu", + "npm:@oliphaunt/tools-linux-arm64-gnu", + "npm:@oliphaunt/tools-win32-x64-msvc", "maven:dev.oliphaunt.runtime:oliphaunt-icu", "maven:dev.oliphaunt.runtime:liboliphaunt-runtime-resources", "maven:dev.oliphaunt.runtime:liboliphaunt-android-arm64-v8a", diff --git a/src/runtimes/liboliphaunt/native/tools-packages/darwin-arm64/README.md b/src/runtimes/liboliphaunt/native/tools-packages/darwin-arm64/README.md new file mode 100644 index 00000000..c6fb6848 --- /dev/null +++ b/src/runtimes/liboliphaunt/native/tools-packages/darwin-arm64/README.md @@ -0,0 +1,5 @@ +# @oliphaunt/tools-darwin-arm64 + +Platform PostgreSQL client tools for Oliphaunt on macOS arm64. +Applications do not depend on this package directly; `@oliphaunt/ts` selects it +as an optional package for the current platform. diff --git a/src/runtimes/liboliphaunt/native/tools-packages/darwin-arm64/package.json b/src/runtimes/liboliphaunt/native/tools-packages/darwin-arm64/package.json new file mode 100644 index 00000000..8d374a78 --- /dev/null +++ b/src/runtimes/liboliphaunt/native/tools-packages/darwin-arm64/package.json @@ -0,0 +1,40 @@ +{ + "name": "@oliphaunt/tools-darwin-arm64", + "version": "0.1.0", + "description": "macOS arm64 PostgreSQL client tools for Oliphaunt.", + "license": "MIT AND Apache-2.0 AND PostgreSQL", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/f0rr0/oliphaunt.git", + "directory": "src/runtimes/liboliphaunt/native/tools-packages/darwin-arm64" + }, + "os": [ + "darwin" + ], + "cpu": [ + "arm64" + ], + "optional": true, + "oliphaunt": { + "product": "oliphaunt-tools", + "kind": "native-tools", + "target": "macos-arm64", + "runtimeRelativePath": "runtime" + }, + "publishConfig": { + "access": "public", + "provenance": true, + "executableFiles": [ + "./runtime/bin/pg_dump", + "./runtime/bin/psql" + ] + }, + "files": [ + "runtime", + "README.md" + ], + "exports": { + "./package.json": "./package.json" + } +} diff --git a/src/runtimes/liboliphaunt/native/tools-packages/linux-arm64-gnu/README.md b/src/runtimes/liboliphaunt/native/tools-packages/linux-arm64-gnu/README.md new file mode 100644 index 00000000..d83e6349 --- /dev/null +++ b/src/runtimes/liboliphaunt/native/tools-packages/linux-arm64-gnu/README.md @@ -0,0 +1,5 @@ +# @oliphaunt/tools-linux-arm64-gnu + +Platform PostgreSQL client tools for Oliphaunt on Linux arm64 glibc. +Applications do not depend on this package directly; `@oliphaunt/ts` selects it +as an optional package for the current platform. diff --git a/src/runtimes/liboliphaunt/native/tools-packages/linux-arm64-gnu/package.json b/src/runtimes/liboliphaunt/native/tools-packages/linux-arm64-gnu/package.json new file mode 100644 index 00000000..69f88c84 --- /dev/null +++ b/src/runtimes/liboliphaunt/native/tools-packages/linux-arm64-gnu/package.json @@ -0,0 +1,43 @@ +{ + "name": "@oliphaunt/tools-linux-arm64-gnu", + "version": "0.1.0", + "description": "Linux arm64 glibc PostgreSQL client tools for Oliphaunt.", + "license": "MIT AND Apache-2.0 AND PostgreSQL", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/f0rr0/oliphaunt.git", + "directory": "src/runtimes/liboliphaunt/native/tools-packages/linux-arm64-gnu" + }, + "os": [ + "linux" + ], + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "optional": true, + "oliphaunt": { + "product": "oliphaunt-tools", + "kind": "native-tools", + "target": "linux-arm64-gnu", + "runtimeRelativePath": "runtime" + }, + "publishConfig": { + "access": "public", + "provenance": true, + "executableFiles": [ + "./runtime/bin/pg_dump", + "./runtime/bin/psql" + ] + }, + "files": [ + "runtime", + "README.md" + ], + "exports": { + "./package.json": "./package.json" + } +} diff --git a/src/runtimes/liboliphaunt/native/tools-packages/linux-x64-gnu/README.md b/src/runtimes/liboliphaunt/native/tools-packages/linux-x64-gnu/README.md new file mode 100644 index 00000000..eb08f03c --- /dev/null +++ b/src/runtimes/liboliphaunt/native/tools-packages/linux-x64-gnu/README.md @@ -0,0 +1,5 @@ +# @oliphaunt/tools-linux-x64-gnu + +Platform PostgreSQL client tools for Oliphaunt on Linux x64 glibc. +Applications do not depend on this package directly; `@oliphaunt/ts` selects it +as an optional package for the current platform. diff --git a/src/runtimes/liboliphaunt/native/tools-packages/linux-x64-gnu/package.json b/src/runtimes/liboliphaunt/native/tools-packages/linux-x64-gnu/package.json new file mode 100644 index 00000000..bab423d9 --- /dev/null +++ b/src/runtimes/liboliphaunt/native/tools-packages/linux-x64-gnu/package.json @@ -0,0 +1,43 @@ +{ + "name": "@oliphaunt/tools-linux-x64-gnu", + "version": "0.1.0", + "description": "Linux x64 glibc PostgreSQL client tools for Oliphaunt.", + "license": "MIT AND Apache-2.0 AND PostgreSQL", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/f0rr0/oliphaunt.git", + "directory": "src/runtimes/liboliphaunt/native/tools-packages/linux-x64-gnu" + }, + "os": [ + "linux" + ], + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "optional": true, + "oliphaunt": { + "product": "oliphaunt-tools", + "kind": "native-tools", + "target": "linux-x64-gnu", + "runtimeRelativePath": "runtime" + }, + "publishConfig": { + "access": "public", + "provenance": true, + "executableFiles": [ + "./runtime/bin/pg_dump", + "./runtime/bin/psql" + ] + }, + "files": [ + "runtime", + "README.md" + ], + "exports": { + "./package.json": "./package.json" + } +} diff --git a/src/runtimes/liboliphaunt/native/tools-packages/win32-x64-msvc/README.md b/src/runtimes/liboliphaunt/native/tools-packages/win32-x64-msvc/README.md new file mode 100644 index 00000000..a55c684a --- /dev/null +++ b/src/runtimes/liboliphaunt/native/tools-packages/win32-x64-msvc/README.md @@ -0,0 +1,5 @@ +# @oliphaunt/tools-win32-x64-msvc + +Platform PostgreSQL client tools for Oliphaunt on Windows x64 MSVC. +Applications do not depend on this package directly; `@oliphaunt/ts` selects it +as an optional package for the current platform. diff --git a/src/runtimes/liboliphaunt/native/tools-packages/win32-x64-msvc/package.json b/src/runtimes/liboliphaunt/native/tools-packages/win32-x64-msvc/package.json new file mode 100644 index 00000000..7d4c9aaa --- /dev/null +++ b/src/runtimes/liboliphaunt/native/tools-packages/win32-x64-msvc/package.json @@ -0,0 +1,40 @@ +{ + "name": "@oliphaunt/tools-win32-x64-msvc", + "version": "0.1.0", + "description": "Windows x64 MSVC PostgreSQL client tools for Oliphaunt.", + "license": "MIT AND Apache-2.0 AND PostgreSQL", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/f0rr0/oliphaunt.git", + "directory": "src/runtimes/liboliphaunt/native/tools-packages/win32-x64-msvc" + }, + "os": [ + "win32" + ], + "cpu": [ + "x64" + ], + "optional": true, + "oliphaunt": { + "product": "oliphaunt-tools", + "kind": "native-tools", + "target": "windows-x64-msvc", + "runtimeRelativePath": "runtime" + }, + "publishConfig": { + "access": "public", + "provenance": true, + "executableFiles": [ + "./runtime/bin/pg_dump.exe", + "./runtime/bin/psql.exe" + ] + }, + "files": [ + "runtime", + "README.md" + ], + "exports": { + "./package.json": "./package.json" + } +} diff --git a/src/sdks/js/ARCHITECTURE.md b/src/sdks/js/ARCHITECTURE.md index 19a56083..73a8d0ee 100644 --- a/src/sdks/js/ARCHITECTURE.md +++ b/src/sdks/js/ARCHITECTURE.md @@ -263,8 +263,10 @@ server. 2. Prepare or validate `/pgdata`. Empty roots are initialized with matching `initdb`; initialized roots are reused after `PG_VERSION` validation by PostgreSQL startup. -3. Resolve `postgres`, `pg_ctl`, `pg_dump`, and `initdb` from - `serverToolDirectory`, `serverExecutable`, or the prepared runtime root. +3. Resolve `postgres`, `pg_ctl`, and `initdb` from `serverToolDirectory`, + `serverExecutable`, or the prepared root runtime. Package-managed installs + materialize the root runtime together with the `@oliphaunt/tools-*` + `pg_dump`/`psql` payload into one runtime directory before server startup. 4. Allocate a fixed or ephemeral loopback port. Retry ephemeral bind conflicts a bounded number of times, matching Rust's behavior. 5. On Unix, allocate a private mode `0700` socket directory and prefer it for diff --git a/src/sdks/js/README.md b/src/sdks/js/README.md index 6fb7bcd6..504deb50 100644 --- a/src/sdks/js/README.md +++ b/src/sdks/js/README.md @@ -33,10 +33,12 @@ artifact is `@oliphaunt/ts`; Deno native applications import is the native-runtime install path. JSR publishes protocol/query helpers only. On supported desktop targets, package managers install the matching -`@oliphaunt/liboliphaunt-*`, `@oliphaunt/broker-*`, and +`@oliphaunt/liboliphaunt-*`, `@oliphaunt/tools-*`, `@oliphaunt/broker-*`, and `@oliphaunt/node-direct-*` packages. Each `@oliphaunt/liboliphaunt-*` package -contains the matching native library and PostgreSQL runtime tree. Runtime -startup uses those installed packages and never downloads GitHub release assets. +contains the matching native library plus the root PostgreSQL runtime +(`postgres`, `initdb`, and `pg_ctl`), while `@oliphaunt/tools-*` carries +`pg_dump` and `psql`. Runtime startup uses those installed packages and never +downloads GitHub release assets. There is no `postinstall` native compilation step and no package-manager native addon approval in the normal path: Node, Bun, and Deno consumers do not install Rust, run Cargo, build PostgreSQL, or copy Oliphaunt native artifacts. The diff --git a/src/sdks/js/package.json b/src/sdks/js/package.json index 6c56ced5..d36e8eb8 100644 --- a/src/sdks/js/package.json +++ b/src/sdks/js/package.json @@ -34,7 +34,11 @@ "@oliphaunt/node-direct-darwin-arm64": "workspace:0.1.0", "@oliphaunt/node-direct-linux-arm64-gnu": "workspace:0.1.0", "@oliphaunt/node-direct-linux-x64-gnu": "workspace:0.1.0", - "@oliphaunt/node-direct-win32-x64-msvc": "workspace:0.1.0" + "@oliphaunt/node-direct-win32-x64-msvc": "workspace:0.1.0", + "@oliphaunt/tools-darwin-arm64": "workspace:0.1.0", + "@oliphaunt/tools-linux-arm64-gnu": "workspace:0.1.0", + "@oliphaunt/tools-linux-x64-gnu": "workspace:0.1.0", + "@oliphaunt/tools-win32-x64-msvc": "workspace:0.1.0" }, "publishConfig": { "access": "public", diff --git a/src/sdks/js/src/__tests__/asset-resolver.test.ts b/src/sdks/js/src/__tests__/asset-resolver.test.ts index d945f220..e0dea74a 100644 --- a/src/sdks/js/src/__tests__/asset-resolver.test.ts +++ b/src/sdks/js/src/__tests__/asset-resolver.test.ts @@ -76,21 +76,29 @@ function packageTargetsMatchLiboliphauntPackages(): void { assert.equal(target.packageName, '@oliphaunt/liboliphaunt-darwin-arm64'); assert.equal(target.libraryRelativePath, 'lib/liboliphaunt.dylib'); assert.equal(target.runtimeRelativePath, 'runtime'); + assert.equal(target.toolsPackageName, '@oliphaunt/tools-darwin-arm64'); + assert.equal(target.toolsRuntimeRelativePath, 'runtime'); const linuxTarget = liboliphauntPackageTarget('linux', 'x64'); assert.equal(linuxTarget.id, 'linux-x64-gnu'); assert.equal(linuxTarget.packageName, '@oliphaunt/liboliphaunt-linux-x64-gnu'); assert.equal(linuxTarget.libraryRelativePath, 'lib/liboliphaunt.so'); assert.equal(linuxTarget.runtimeRelativePath, 'runtime'); + assert.equal(linuxTarget.toolsPackageName, '@oliphaunt/tools-linux-x64-gnu'); + assert.equal(linuxTarget.toolsRuntimeRelativePath, 'runtime'); const linuxArmTarget = liboliphauntPackageTarget('linux', 'arm64'); assert.equal(linuxArmTarget.id, 'linux-arm64-gnu'); assert.equal(linuxArmTarget.packageName, '@oliphaunt/liboliphaunt-linux-arm64-gnu'); assert.equal(linuxArmTarget.libraryRelativePath, 'lib/liboliphaunt.so'); assert.equal(linuxArmTarget.runtimeRelativePath, 'runtime'); + assert.equal(linuxArmTarget.toolsPackageName, '@oliphaunt/tools-linux-arm64-gnu'); + assert.equal(linuxArmTarget.toolsRuntimeRelativePath, 'runtime'); const windowsTarget = liboliphauntPackageTarget('win32', 'x64'); assert.equal(windowsTarget.id, 'windows-x64-msvc'); assert.equal(windowsTarget.packageName, '@oliphaunt/liboliphaunt-win32-x64-msvc'); assert.equal(windowsTarget.libraryRelativePath, 'bin/oliphaunt.dll'); assert.equal(windowsTarget.runtimeRelativePath, 'runtime'); + assert.equal(windowsTarget.toolsPackageName, '@oliphaunt/tools-win32-x64-msvc'); + assert.equal(windowsTarget.toolsRuntimeRelativePath, 'runtime'); } async function tarExtractionRejectsTraversal(): Promise { @@ -160,6 +168,10 @@ async function typeScriptPackageMetadataMatchesRuntimePackages(): Promise '@oliphaunt/node-direct-linux-arm64-gnu', '@oliphaunt/node-direct-linux-x64-gnu', '@oliphaunt/node-direct-win32-x64-msvc', + '@oliphaunt/tools-darwin-arm64', + '@oliphaunt/tools-linux-arm64-gnu', + '@oliphaunt/tools-linux-x64-gnu', + '@oliphaunt/tools-win32-x64-msvc', ]; assert.deepEqual( Object.keys(packageJson.optionalDependencies ?? {}).sort(), @@ -174,9 +186,15 @@ async function typeScriptPackageMetadataMatchesRuntimePackages(): Promise `workspace:${liboliphauntVersion}`, ); } - for (const packageName of optionalDependencyNames.slice(8)) { + for (const packageName of optionalDependencyNames.slice(8, 12)) { assert.equal(packageJson.optionalDependencies?.[packageName], `workspace:${nodeDirectVersion}`); } + for (const packageName of optionalDependencyNames.slice(12)) { + assert.equal( + packageJson.optionalDependencies?.[packageName], + `workspace:${liboliphauntVersion}`, + ); + } await assertPlatformPackageTarget( '../../../../runtimes/liboliphaunt/native/packages/linux-x64-gnu/package.json', '@oliphaunt/liboliphaunt-linux-x64-gnu', @@ -184,6 +202,13 @@ async function typeScriptPackageMetadataMatchesRuntimePackages(): Promise 'linux-x64-gnu', 'runtime', ); + await assertPlatformPackageTarget( + '../../../../runtimes/liboliphaunt/native/tools-packages/linux-x64-gnu/package.json', + '@oliphaunt/tools-linux-x64-gnu', + liboliphauntVersion, + 'linux-x64-gnu', + 'runtime', + ); await assertPlatformPackageTarget( '../../../../runtimes/broker/packages/linux-x64-gnu/package.json', '@oliphaunt/broker-linux-x64-gnu', diff --git a/src/sdks/js/src/__tests__/client.test.ts b/src/sdks/js/src/__tests__/client.test.ts index fa2ff753..790efcc1 100644 --- a/src/sdks/js/src/__tests__/client.test.ts +++ b/src/sdks/js/src/__tests__/client.test.ts @@ -127,6 +127,7 @@ async function testOpenNormalizesNativeConfigAndUsesLibraryOverride(): Promise await assert.rejects( async () => client.open({ engine: 'nativeServer', root: '/tmp/oliphaunt-js-root' }), - /serverExecutable|OLIPHAUNT_POSTGRES/, + /serverExecutable|OLIPHAUNT_POSTGRES|@oliphaunt\/liboliphaunt-/, ); await assert.rejects( async () => client.open({ root: '/tmp/root', temporary: true }), diff --git a/src/sdks/js/src/__tests__/native-bindings.test.ts b/src/sdks/js/src/__tests__/native-bindings.test.ts index 452ae26a..a4673e8a 100644 --- a/src/sdks/js/src/__tests__/native-bindings.test.ts +++ b/src/sdks/js/src/__tests__/native-bindings.test.ts @@ -57,6 +57,7 @@ function testFfiLayoutPackingAndBounds(): void { runtimeDirectory: '/tmp/runtime', username: 'postgres', database: 'app', + extensions: [], startupArgs: ['-c', 'work_mem=8MB'], }, pointerOf, @@ -159,10 +160,11 @@ module.exports = { assert.equal(binding.version(), '18.4-test'); assert.equal(binding.capabilities(), 195n); - const handle = binding.open({ + const handle = await binding.open({ pgdata: join(root, 'pgdata'), username: 'postgres', database: 'postgres', + extensions: [], startupArgs: [], }); assert.equal(handle, 41n); diff --git a/src/sdks/js/src/native/assets-node.ts b/src/sdks/js/src/native/assets-node.ts index a4c77232..f5094e4c 100644 --- a/src/sdks/js/src/native/assets-node.ts +++ b/src/sdks/js/src/native/assets-node.ts @@ -38,6 +38,17 @@ type LiboliphauntPackageMetadata = { }; }; +type NativeToolsPackageMetadata = { + name?: string; + version?: string; + oliphaunt?: { + product?: string; + kind?: string; + target?: string; + runtimeRelativePath?: string; + }; +}; + type IcuPackageMetadata = { name?: string; version?: string; @@ -87,7 +98,7 @@ export async function resolveNodeNativeInstall( export async function materializeNodeExtensionInstall( install: ResolvedNativeInstall, - extensions: ReadonlyArray, + extensions: ReadonlyArray = [], ): Promise { const selected = selectedExtensionClosure(extensions); if (selected.length === 0) { @@ -397,7 +408,113 @@ async function resolvePackageNativeInstall( packageJson.oliphaunt?.runtimeRelativePath ?? target.runtimeRelativePath, ); await requireDirectory(runtimeDirectory, `${target.packageName} runtime directory`); - return { libraryPath, runtimeDirectory, icuDataDirectory }; + for (const tool of nativeRuntimeToolsForTarget(target.id)) { + await requireFile( + join(runtimeDirectory, 'bin', tool), + `${target.packageName} runtime tool bin/${tool}`, + ); + } + const tools = await resolveNativeToolsPackage(target, expectedVersion, packageJsonPath); + const mergedRuntimeDirectory = await materializeNativeToolsRuntime({ + target: target.id, + libraryPath, + runtimePackage: { + name: target.packageName, + version: packageJson.version, + runtimeDirectory, + }, + toolsPackage: tools, + }); + return { libraryPath, runtimeDirectory: mergedRuntimeDirectory, icuDataDirectory }; +} + +async function resolveNativeToolsPackage( + target: NativePackageTarget, + expectedVersion: string, + runtimePackageJsonPath: string, +): Promise<{ name: string; version: string; runtimeDirectory: string }> { + let packageJsonPath: string; + try { + packageJsonPath = createRequire(runtimePackageJsonPath).resolve( + `${target.toolsPackageName}/package.json`, + ); + } catch (error) { + throw new Error( + `${target.toolsPackageName} is not installed; reinstall @oliphaunt/ts with optional dependencies enabled`, + { cause: error }, + ); + } + const packageRoot = dirname(packageJsonPath); + const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')) as NativeToolsPackageMetadata; + if (packageJson.name !== target.toolsPackageName) { + throw new Error( + `${target.toolsPackageName} package metadata has name ${packageJson.name ?? ''}`, + ); + } + if (packageJson.version !== expectedVersion) { + throw new Error( + `${target.toolsPackageName} version ${packageJson.version ?? ''} does not match @oliphaunt/ts liboliphauntVersion ${expectedVersion}`, + ); + } + if (packageJson.oliphaunt?.product !== 'oliphaunt-tools') { + throw new Error(`${target.toolsPackageName} package metadata does not declare oliphaunt-tools`); + } + if (packageJson.oliphaunt?.kind !== 'native-tools') { + throw new Error(`${target.toolsPackageName} package metadata does not declare native tools`); + } + if (packageJson.oliphaunt?.target !== target.id) { + throw new Error(`${target.toolsPackageName} package metadata does not target ${target.id}`); + } + const runtimeDirectory = join( + packageRoot, + packageJson.oliphaunt?.runtimeRelativePath ?? target.toolsRuntimeRelativePath, + ); + await requireDirectory(runtimeDirectory, `${target.toolsPackageName} runtime directory`); + for (const tool of nativeClientToolsForTarget(target.id)) { + await requireFile( + join(runtimeDirectory, 'bin', tool), + `${target.toolsPackageName} native tool bin/${tool}`, + ); + } + return { + name: target.toolsPackageName, + version: packageJson.version, + runtimeDirectory, + }; +} + +async function materializeNativeToolsRuntime(config: { + target: string; + libraryPath: string; + runtimePackage: { + name: string; + version?: string; + runtimeDirectory: string; + }; + toolsPackage: { + name: string; + version: string; + runtimeDirectory: string; + }; +}): Promise { + const cacheKey = runtimeCacheKey(config); + const root = join(tmpdir(), 'oliphaunt-js-runtime-cache', cacheKey); + const runtimeDirectory = join(root, 'runtime'); + const marker = join(root, 'manifest.json'); + const manifest = JSON.stringify(config, null, 2); + if ((await optionalRead(marker)) === manifest) { + return runtimeDirectory; + } + + await rm(root, { force: true, recursive: true }); + await mkdir(root, { recursive: true }); + await cp(config.runtimePackage.runtimeDirectory, runtimeDirectory, { recursive: true }); + await cp(config.toolsPackage.runtimeDirectory, runtimeDirectory, { + force: true, + recursive: true, + }); + await writeFile(marker, manifest, 'utf8'); + return runtimeDirectory; } function resolvePackageJson(packageName: string): string { @@ -546,6 +663,16 @@ function nativeModuleDirectoryCandidates(libraryPath: string): string[] { return [join(libraryDir, 'modules'), join(dirname(libraryDir), 'lib', 'modules')]; } +function nativeRuntimeToolsForTarget(target: string): string[] { + return target === 'windows-x64-msvc' + ? ['initdb.exe', 'pg_ctl.exe', 'postgres.exe'] + : ['initdb', 'pg_ctl', 'postgres']; +} + +function nativeClientToolsForTarget(target: string): string[] { + return target === 'windows-x64-msvc' ? ['pg_dump.exe', 'psql.exe'] : ['pg_dump', 'psql']; +} + function runtimeCacheKey(value: unknown): string { return createHash('sha256').update(JSON.stringify(value)).digest('hex').slice(0, 32); } diff --git a/src/sdks/js/src/native/common.ts b/src/sdks/js/src/native/common.ts index f995b657..bfaea335 100644 --- a/src/sdks/js/src/native/common.ts +++ b/src/sdks/js/src/native/common.ts @@ -22,6 +22,8 @@ export type NativePackageTarget = { packageName: string; libraryRelativePath: string; runtimeRelativePath: string; + toolsPackageName: string; + toolsRuntimeRelativePath: string; }; export function resolveLibraryPath(libraryPath?: string): string { @@ -89,6 +91,8 @@ export function liboliphauntPackageTarget( packageName: '@oliphaunt/liboliphaunt-darwin-arm64', libraryRelativePath: 'lib/liboliphaunt.dylib', runtimeRelativePath: 'runtime', + toolsPackageName: '@oliphaunt/tools-darwin-arm64', + toolsRuntimeRelativePath: 'runtime', }; } if (normalizedPlatform === 'linux' && normalizedArch === 'x64') { @@ -97,6 +101,8 @@ export function liboliphauntPackageTarget( packageName: '@oliphaunt/liboliphaunt-linux-x64-gnu', libraryRelativePath: 'lib/liboliphaunt.so', runtimeRelativePath: 'runtime', + toolsPackageName: '@oliphaunt/tools-linux-x64-gnu', + toolsRuntimeRelativePath: 'runtime', }; } if (normalizedPlatform === 'linux' && normalizedArch === 'arm64') { @@ -105,6 +111,8 @@ export function liboliphauntPackageTarget( packageName: '@oliphaunt/liboliphaunt-linux-arm64-gnu', libraryRelativePath: 'lib/liboliphaunt.so', runtimeRelativePath: 'runtime', + toolsPackageName: '@oliphaunt/tools-linux-arm64-gnu', + toolsRuntimeRelativePath: 'runtime', }; } if (normalizedPlatform === 'windows' && normalizedArch === 'x64') { @@ -113,6 +121,8 @@ export function liboliphauntPackageTarget( packageName: '@oliphaunt/liboliphaunt-win32-x64-msvc', libraryRelativePath: 'bin/oliphaunt.dll', runtimeRelativePath: 'runtime', + toolsPackageName: '@oliphaunt/tools-win32-x64-msvc', + toolsRuntimeRelativePath: 'runtime', }; } throw new Error( diff --git a/src/sdks/js/src/runtime/server.ts b/src/sdks/js/src/runtime/server.ts index e4835c7f..70e217eb 100644 --- a/src/sdks/js/src/runtime/server.ts +++ b/src/sdks/js/src/runtime/server.ts @@ -17,7 +17,7 @@ import { import { createPhysicalArchive } from './physical-archive.js'; import { PostgresWireClient } from './pgwire.js'; import type { RuntimeBinding, RuntimeHandle } from './types.js'; -import { resolveNodeIcuDataDirectory } from '../native/assets-node.js'; +import { resolveNodeIcuDataDirectory, resolveNodeNativeInstall } from '../native/assets-node.js'; const SERVER_HOST = '127.0.0.1'; const SERVER_STARTUP_TIMEOUT_MS_ENV = 'OLIPHAUNT_SERVER_STARTUP_TIMEOUT_MS'; @@ -67,7 +67,7 @@ export async function serverModeSupport(options: { }): Promise { const capabilities = serverCapabilities(32); try { - await resolveServerExecutable(options); + await resolveServerTools(options); return { engine: 'nativeServer', available: true, capabilities }; } catch (error) { return { @@ -190,11 +190,12 @@ class ServerHandle { async function openServer(config: NormalizedOpenConfig): Promise { const startupTimeoutMs = serverStartupTimeoutMs(); - const executable = await resolveServerExecutable({ + const tools = await resolveServerTools({ serverExecutable: config.serverExecutable, serverToolDirectory: config.serverToolDirectory, }); - const toolDirectory = config.serverToolDirectory ?? dirname(executable); + const executable = tools.executable; + const toolDirectory = tools.toolDirectory; let socketDir: string | undefined; let child: ManagedChild | undefined; try { @@ -364,10 +365,10 @@ function serverStartupTimeoutMs(): number { return parsed; } -async function resolveServerExecutable(options: { +async function resolveServerTools(options: { serverExecutable?: string; serverToolDirectory?: string; -}): Promise { +}): Promise<{ executable: string; toolDirectory: string }> { const candidates = [ options.serverExecutable, process.env.OLIPHAUNT_POSTGRES, @@ -377,10 +378,26 @@ async function resolveServerExecutable(options: { ].filter((value): value is string => value !== undefined && value.length > 0); for (const candidate of candidates) { if (await isFile(candidate)) { - return candidate; + return { + executable: candidate, + toolDirectory: options.serverToolDirectory ?? dirname(candidate), + }; } } - throw new Error('set serverExecutable, serverToolDirectory, or OLIPHAUNT_POSTGRES'); + if (options.serverExecutable !== undefined || options.serverToolDirectory !== undefined) { + throw new Error('set serverExecutable, serverToolDirectory, or OLIPHAUNT_POSTGRES'); + } + const install = await resolveNodeNativeInstall(); + if (install.runtimeDirectory !== undefined) { + const toolDirectory = join(install.runtimeDirectory, 'bin'); + const executable = join(toolDirectory, executableName('postgres')); + if (await isFile(executable)) { + return { executable, toolDirectory }; + } + } + throw new Error( + 'set serverExecutable, serverToolDirectory, or OLIPHAUNT_POSTGRES, or install @oliphaunt/ts with optional native runtime packages enabled', + ); } async function optionalTool( diff --git a/src/sdks/js/tools/check-sdk.sh b/src/sdks/js/tools/check-sdk.sh index b927bf63..15b95719 100755 --- a/src/sdks/js/tools/check-sdk.sh +++ b/src/sdks/js/tools/check-sdk.sh @@ -62,6 +62,7 @@ JSON packages: - "src/sdks/js" - "src/runtimes/liboliphaunt/native/packages/*" + - "src/runtimes/liboliphaunt/native/tools-packages/*" - "src/runtimes/broker/packages/*" - "src/runtimes/node-direct/packages/*" catalog: @@ -94,6 +95,10 @@ YAML rsync -a --delete \ src/runtimes/liboliphaunt/native/packages/ \ "$scratch_root/src/runtimes/liboliphaunt/native/packages/" + mkdir -p "$scratch_root/src/runtimes/liboliphaunt/native/tools-packages" + rsync -a --delete \ + src/runtimes/liboliphaunt/native/tools-packages/ \ + "$scratch_root/src/runtimes/liboliphaunt/native/tools-packages/" mkdir -p "$scratch_root/src/runtimes/broker/packages" rsync -a --delete \ src/runtimes/broker/packages/ \ @@ -213,6 +218,10 @@ process.stdin.on('end', () => { '@oliphaunt/node-direct-linux-arm64-gnu': nodeDirectVersion, '@oliphaunt/node-direct-linux-x64-gnu': nodeDirectVersion, '@oliphaunt/node-direct-win32-x64-msvc': nodeDirectVersion, + '@oliphaunt/tools-darwin-arm64': liboliphauntVersion, + '@oliphaunt/tools-linux-arm64-gnu': liboliphauntVersion, + '@oliphaunt/tools-linux-x64-gnu': liboliphauntVersion, + '@oliphaunt/tools-win32-x64-msvc': liboliphauntVersion, }; if (JSON.stringify(pkg.dependencies || {}) !== JSON.stringify(expectedDependencies)) { throw new Error('packed TypeScript package must not declare regular runtime artifact dependencies'); @@ -338,6 +347,10 @@ const expectedOptional = [ '@oliphaunt/node-direct-linux-arm64-gnu', '@oliphaunt/node-direct-linux-x64-gnu', '@oliphaunt/node-direct-win32-x64-msvc', + '@oliphaunt/tools-darwin-arm64', + '@oliphaunt/tools-linux-arm64-gnu', + '@oliphaunt/tools-linux-x64-gnu', + '@oliphaunt/tools-win32-x64-msvc', ]; const optional = Object.keys(pkg.optionalDependencies || {}).sort(); if ( diff --git a/src/sdks/rust/src/backup.rs b/src/sdks/rust/src/backup.rs index 66ef736c..047d35a4 100644 --- a/src/sdks/rust/src/backup.rs +++ b/src/sdks/rust/src/backup.rs @@ -9,8 +9,8 @@ use tar::{Builder, EntryType, Header}; use crate::error::{Error, Result}; use crate::extension::Extension; use crate::liboliphaunt::{ - NATIVE_ROOT_MANIFEST_FILE, NativeRootLock, ensure_native_root_manifest, - native_root_manifest_text, validate_native_root_manifest_text, + NATIVE_ROOT_MANIFEST_FILE, NativeRootLock, configure_native_tool_env, + ensure_native_root_manifest, native_root_manifest_text, validate_native_root_manifest_text, }; use crate::protocol::{ProtocolRequest, ProtocolResponse}; use crate::storage::{ @@ -298,7 +298,11 @@ pub(crate) fn sql_backup_with_pg_dump( pg_dump.display() ))); } - let output = std::process::Command::new(pg_dump) + let mut command = std::process::Command::new(pg_dump); + if let Some(runtime_dir) = pg_dump.parent().and_then(Path::parent) { + configure_native_tool_env(&mut command, runtime_dir); + } + let output = command .arg("--dbname") .arg(connection_string) .arg("--format=plain") diff --git a/src/sdks/rust/src/build_resources.rs b/src/sdks/rust/src/build_resources.rs new file mode 100644 index 00000000..dd0a903c --- /dev/null +++ b/src/sdks/rust/src/build_resources.rs @@ -0,0 +1,62 @@ +use std::path::PathBuf; +use std::sync::{OnceLock, RwLock}; + +use crate::error::{Error, Result}; + +static BUILD_RESOURCES_DIR: OnceLock>> = OnceLock::new(); + +/// Register the Oliphaunt resource directory staged by `oliphaunt-build`. +/// +/// Applications usually call [`register_build_resources!`] once during startup +/// after their `build.rs` has called `oliphaunt_build::configure()`. The native +/// runtime locator uses this directory before falling back to explicit +/// environment variables and source-tree build layouts. +pub fn register_build_resources_dir(path: impl Into) -> Result<()> { + let path = path.into(); + if path.as_os_str().is_empty() { + return Err(Error::InvalidConfig( + "Oliphaunt build resources directory cannot be empty".to_owned(), + )); + } + + let lock = BUILD_RESOURCES_DIR.get_or_init(|| RwLock::new(None)); + let mut guard = lock + .write() + .map_err(|_| Error::Engine("Oliphaunt build resources registry was poisoned".to_owned()))?; + if let Some(existing) = guard.as_ref() { + if existing == &path { + return Ok(()); + } + return Err(Error::InvalidConfig(format!( + "Oliphaunt build resources are already registered as {}; cannot replace them with {}", + existing.display(), + path.display() + ))); + } + *guard = Some(path); + Ok(()) +} + +pub(crate) fn registered_build_resources_dir() -> Option { + BUILD_RESOURCES_DIR + .get() + .and_then(|lock| lock.read().ok().and_then(|guard| guard.clone())) +} + +/// Register the resources staged by `oliphaunt-build` for the current package. +/// +/// The macro expands in the application crate, so it can read the +/// `OLIPHAUNT_RESOURCES_DIR` compile-time value emitted by +/// `oliphaunt_build::configure()`. +#[macro_export] +macro_rules! register_build_resources { + () => { + match option_env!("OLIPHAUNT_RESOURCES_DIR") { + Some(path) => $crate::register_build_resources_dir(path), + None => Err($crate::Error::InvalidConfig( + "OLIPHAUNT_RESOURCES_DIR was not emitted for this package; add oliphaunt-build as a build dependency and call oliphaunt_build::configure() from build.rs" + .to_owned(), + )), + } + }; +} diff --git a/src/sdks/rust/src/lib.rs b/src/sdks/rust/src/lib.rs index 3d2ef805..604c4a5d 100644 --- a/src/sdks/rust/src/lib.rs +++ b/src/sdks/rust/src/lib.rs @@ -7,6 +7,7 @@ mod backup; mod broker; +mod build_resources; mod builder; mod config; mod database; @@ -28,6 +29,7 @@ mod server; mod storage; pub use broker::NativeBrokerRuntime; +pub use build_resources::register_build_resources_dir; pub use builder::OliphauntBuilder; pub use config::{ DEFAULT_DATABASE, DEFAULT_USERNAME, DurabilityProfile, EngineMode, NativeBrokerConfig, diff --git a/src/sdks/rust/src/liboliphaunt/mod.rs b/src/sdks/rust/src/liboliphaunt/mod.rs index 72232050..9122e09d 100644 --- a/src/sdks/rust/src/liboliphaunt/mod.rs +++ b/src/sdks/rust/src/liboliphaunt/mod.rs @@ -12,8 +12,8 @@ pub(crate) use self::root::{ }; pub(crate) use self::root::{ NativeRootLock, PreparedNativeRoot, ROOT_MANIFEST_FILE as NATIVE_ROOT_MANIFEST_FILE, - ensure_root_manifest as ensure_native_root_manifest, native_root_key, - root_manifest_text as native_root_manifest_text, + configure_native_tool_env, ensure_root_manifest as ensure_native_root_manifest, + native_root_key, root_manifest_text as native_root_manifest_text, validate_root_manifest_text as validate_native_root_manifest_text, }; diff --git a/src/sdks/rust/src/liboliphaunt/root.rs b/src/sdks/rust/src/liboliphaunt/root.rs index e04dc017..156d47ca 100644 --- a/src/sdks/rust/src/liboliphaunt/root.rs +++ b/src/sdks/rust/src/liboliphaunt/root.rs @@ -10,6 +10,7 @@ use std::ffi::OsString; use std::fmt::Write as _; use std::fs::{self, File, OpenOptions}; use std::path::{Component, Path, PathBuf}; +use std::process::Command; use std::sync::{Mutex, OnceLock}; use std::time::{SystemTime, UNIX_EPOCH}; @@ -106,6 +107,50 @@ pub(super) fn existing_native_tool_path(root: &Path, tool_name: &str) -> PathBuf root.join("bin").join(tool_name) } +pub(crate) fn configure_native_tool_env(command: &mut Command, runtime_dir: &Path) { + let dirs = native_dynamic_library_dirs(runtime_dir); + if dirs.is_empty() { + return; + } + let Some(joined) = prepend_env_paths(native_dynamic_library_env_name(), dirs) else { + return; + }; + command.env(native_dynamic_library_env_name(), joined); +} + +fn native_dynamic_library_env_name() -> &'static str { + if cfg!(target_os = "macos") { + "DYLD_LIBRARY_PATH" + } else if cfg!(target_os = "windows") { + "PATH" + } else { + "LD_LIBRARY_PATH" + } +} + +fn native_dynamic_library_dirs(runtime_dir: &Path) -> Vec { + let mut dirs = Vec::new(); + #[cfg(windows)] + { + let bin_dir = runtime_dir.join("bin"); + if bin_dir.is_dir() { + dirs.push(bin_dir); + } + } + let lib_dir = runtime_dir.join("lib"); + if lib_dir.is_dir() { + dirs.push(lib_dir); + } + dirs +} + +fn prepend_env_paths(name: &str, mut dirs: Vec) -> Option { + if let Some(existing) = env::var_os(name) { + dirs.extend(env::split_paths(&existing)); + } + env::join_paths(dirs).ok() +} + impl Drop for PreparedNativeRoot { fn drop(&mut self) { drop(self.lock.take()); diff --git a/src/sdks/rust/src/liboliphaunt/root/runtime.rs b/src/sdks/rust/src/liboliphaunt/root/runtime.rs index 4b9a8eeb..49cf3cc4 100644 --- a/src/sdks/rust/src/liboliphaunt/root/runtime.rs +++ b/src/sdks/rust/src/liboliphaunt/root/runtime.rs @@ -13,7 +13,8 @@ use fs2::FileExt; use cache_key::{cached_runtime_is_valid, runtime_cache_key, runtime_cache_manifest}; use install::install_cached_runtime; use locate::{ - locate_native_embedded_modules_dir, locate_native_install_dir, locate_native_tools_dir, + locate_native_embedded_modules_dir, locate_native_extension_artifact_dirs, + locate_native_install_dir, locate_native_tools_dir, }; use super::NativeRuntimeProfile; @@ -27,7 +28,13 @@ pub(super) fn materialize_runtime( extensions: &[Extension], ) -> Result { let install_dir = locate_native_install_dir()?; - let tools_dir = locate_native_tools_dir(&install_dir); + let tools_dir = locate_native_tools_dir(&install_dir).ok_or_else(|| { + Error::Engine( + "could not locate native PostgreSQL client tools pg_dump and psql; add the target oliphaunt-tools artifact crate or set OLIPHAUNT_TOOLS_DIR" + .to_owned(), + ) + })?; + let extension_artifact_dirs = locate_native_extension_artifact_dirs(); let embedded_modules = if profile.needs_embedded_modules() { Some(locate_native_embedded_modules_dir(&install_dir)?) } else { @@ -36,8 +43,9 @@ pub(super) fn materialize_runtime( let key = runtime_cache_key( profile, &install_dir, - tools_dir.as_deref(), + Some(tools_dir.as_path()), embedded_modules.as_deref(), + &extension_artifact_dirs, extensions, )?; let cache_root = runtime_cache_root()?; @@ -100,8 +108,9 @@ pub(super) fn materialize_runtime( let build_result = install_cached_runtime( profile, &install_dir, - tools_dir.as_deref(), + Some(tools_dir.as_path()), embedded_modules.as_deref(), + &extension_artifact_dirs, &build_dir, extensions, ); @@ -151,6 +160,24 @@ pub(super) fn materialize_runtime( Ok(cache_dir) } +pub(super) fn extension_artifact_root_for<'a>( + install_dir: &'a std::path::Path, + extension_artifact_dirs: &'a [PathBuf], + extension: Extension, +) -> &'a std::path::Path { + extension_artifact_dirs + .iter() + .find(|root| extension_artifact_root_contains(root, extension)) + .map(PathBuf::as_path) + .unwrap_or(install_dir) +} + +fn extension_artifact_root_contains(root: &std::path::Path, extension: Extension) -> bool { + root.join("share/postgresql/extension") + .join(format!("{}.control", extension.sql_name())) + .is_file() +} + pub(super) fn runtime_cache_root() -> Result { if let Some(path) = std::env::var_os(ENV_RUNTIME_CACHE_DIR) { return Ok(PathBuf::from(path)); diff --git a/src/sdks/rust/src/liboliphaunt/root/runtime/cache_key.rs b/src/sdks/rust/src/liboliphaunt/root/runtime/cache_key.rs index 7b131dbe..923bc3b9 100644 --- a/src/sdks/rust/src/liboliphaunt/root/runtime/cache_key.rs +++ b/src/sdks/rust/src/liboliphaunt/root/runtime/cache_key.rs @@ -1,5 +1,5 @@ use std::fs; -use std::path::Path; +use std::path::{Path, PathBuf}; use super::super::NativeRuntimeProfile; use super::super::extensions::{ @@ -15,6 +15,7 @@ use super::super::fingerprint::{ use super::super::{ NATIVE_RUNTIME_TOOLS, NATIVE_TOOLS_PACKAGE_TOOLS, existing_native_tool_path, native_tool_path, }; +use super::extension_artifact_root_for; use crate::error::{Error, Result}; use crate::extension::Extension; @@ -25,6 +26,7 @@ pub(super) fn runtime_cache_key( install_dir: &Path, tools_dir: Option<&Path>, embedded_modules: Option<&Path>, + extension_artifact_dirs: &[PathBuf], extensions: &[Extension], ) -> Result { let mut state = new_state(); @@ -66,9 +68,25 @@ pub(super) fn runtime_cache_key( fingerprint_directory_filtered(&mut state, &source_share, &source_share, core_share_file)?; fingerprint_named_extension_sql_files(&mut state, &source_share, "plpgsql")?; for extension in extensions { - fingerprint_named_extension_sql_files(&mut state, &source_share, extension.sql_name())?; + let extension_root = + extension_artifact_root_for(install_dir, extension_artifact_dirs, *extension); + let extension_share = extension_root.join("share/postgresql"); + fingerprint_named_extension_sql_files(&mut state, &extension_share, extension.sql_name())?; for relative in data_files(*extension) { - fingerprint_optional_file(&mut state, &source_share, &source_share.join(relative))?; + fingerprint_optional_file( + &mut state, + &extension_share, + &extension_share.join(relative), + )?; + } + } + let source_runtime_lib = install_dir.join("lib"); + if source_runtime_lib.is_dir() { + for entry in sorted_read_dir(&source_runtime_lib)? { + let source = entry.path(); + if source.is_file() { + fingerprint_file(&mut state, &source_runtime_lib, &source)?; + } } } let source_lib = install_dir.join("lib/postgresql"); @@ -103,10 +121,16 @@ pub(super) fn runtime_cache_key( } for extension in extensions { if let Some(module) = extension.native_module_file() { + let extension_root = extension_artifact_root_for( + install_dir, + extension_artifact_dirs, + *extension, + ); + let extension_lib = extension_root.join("lib/postgresql"); fingerprint_optional_file( &mut state, - embedded_modules, - &embedded_modules.join(module), + &extension_lib, + &extension_lib.join(module), )?; } } @@ -114,7 +138,17 @@ pub(super) fn runtime_cache_key( NativeRuntimeProfile::PostgresServer => { for extension in extensions { if let Some(module) = extension.native_module_file() { - fingerprint_optional_file(&mut state, &source_lib, &source_lib.join(module))?; + let extension_root = extension_artifact_root_for( + install_dir, + extension_artifact_dirs, + *extension, + ); + let extension_lib = extension_root.join("lib/postgresql"); + fingerprint_optional_file( + &mut state, + &extension_lib, + &extension_lib.join(module), + )?; } } } @@ -129,9 +163,12 @@ pub(super) fn cached_runtime_is_valid( extensions: &[Extension], ) -> bool { if !cache_dir.join(".complete").is_file() - || !native_tool_path(cache_dir, "postgres").is_file() - || !native_tool_path(cache_dir, "initdb").is_file() - || !native_tool_path(cache_dir, "pg_ctl").is_file() + || !NATIVE_RUNTIME_TOOLS + .iter() + .all(|tool| native_tool_path(cache_dir, tool).is_file()) + || !NATIVE_TOOLS_PACKAGE_TOOLS + .iter() + .all(|tool| native_tool_path(cache_dir, tool).is_file()) || !cache_dir .join("share/postgresql/postgresql.conf.sample") .is_file() @@ -251,6 +288,7 @@ mod tests { &install_dir, None, None, + &[], &[Extension::Hstore], ) .expect("create first runtime cache key"); @@ -264,6 +302,7 @@ mod tests { &install_dir, None, None, + &[], &[Extension::Hstore], ) .expect("create SQL-mutated runtime cache key"); @@ -283,6 +322,7 @@ mod tests { &install_dir, None, None, + &[], &[Extension::Hstore], ) .expect("create module-mutated runtime cache key"); @@ -292,6 +332,49 @@ mod tests { ); } + #[test] + fn selected_sidecar_extension_content_participates_in_cache_key() { + let temp = TempTree::new("selected-sidecar-extension"); + let install_dir = temp.path().join("install"); + let extension_dir = temp.path().join("extension/oliphaunt-extension-hstore"); + write_fake_install(&install_dir); + write_fake_hstore_extension( + &extension_dir, + b"select 'sidecar-v1';\n", + b"sidecar-module-v1", + ); + + let first = runtime_cache_key( + NativeRuntimeProfile::PostgresServer, + &install_dir, + None, + None, + std::slice::from_ref(&extension_dir), + &[Extension::Hstore], + ) + .expect("create first sidecar extension runtime cache key"); + + write_fake_hstore_extension( + &extension_dir, + b"select 'sidecar-v2';\n", + b"sidecar-module-v2", + ); + let second = runtime_cache_key( + NativeRuntimeProfile::PostgresServer, + &install_dir, + None, + None, + std::slice::from_ref(&extension_dir), + &[Extension::Hstore], + ) + .expect("create changed sidecar extension runtime cache key"); + + assert_ne!( + first, second, + "selected sidecar extension artifact changes must invalidate the runtime cache" + ); + } + #[test] fn unselected_extension_assets_do_not_pollute_cache_key() { let temp = TempTree::new("unselected-extension"); @@ -304,6 +387,7 @@ mod tests { None, None, &[], + &[], ) .expect("create first runtime cache key"); @@ -328,6 +412,7 @@ mod tests { None, None, &[], + &[], ) .expect("create second runtime cache key"); assert_eq!( @@ -356,6 +441,7 @@ mod tests { None, None, &[], + &[], ) .expect("create first ICU runtime cache key"); @@ -369,6 +455,7 @@ mod tests { None, None, &[], + &[], ) .expect("create changed ICU runtime cache key"); @@ -435,6 +522,19 @@ mod tests { ); } + #[test] + fn runtime_validation_requires_split_tools() { + let temp = TempTree::new("validation-tools"); + let cache_dir = temp.path().join("cache"); + write_minimal_cache_dir(&cache_dir, "cache-key"); + std::fs::remove_file(cache_dir.join("bin/pg_dump")).expect("remove pg_dump"); + + assert!( + !cached_runtime_is_valid(&cache_dir, "cache-key", &[]), + "runtime cache must require tools from the split oliphaunt-tools artifact" + ); + } + fn write_fake_install(install_dir: &Path) { for tool in ["postgres", "initdb", "pg_ctl", "pg_dump", "psql"] { write_file(&install_dir.join("bin").join(tool), tool.as_bytes()); @@ -467,6 +567,23 @@ mod tests { ); } + fn write_fake_hstore_extension(extension_dir: &Path, sql: &[u8], module: &[u8]) { + write_file( + &extension_dir.join("share/postgresql/extension/hstore.control"), + b"comment = 'hstore'\n", + ); + write_file( + &extension_dir.join("share/postgresql/extension/hstore--1.0.sql"), + sql, + ); + write_file( + &extension_dir + .join("lib/postgresql") + .join(format!("hstore{}", std::env::consts::DLL_SUFFIX)), + module, + ); + } + fn write_minimal_cache_dir(cache_dir: &Path, key: &str) { write_file(&cache_dir.join(".complete"), b"ok\n"); write_file( @@ -476,6 +593,8 @@ mod tests { write_file(&cache_dir.join("bin/postgres"), b"postgres"); write_file(&cache_dir.join("bin/initdb"), b"initdb"); write_file(&cache_dir.join("bin/pg_ctl"), b"pg_ctl"); + write_file(&cache_dir.join("bin/pg_dump"), b"pg_dump"); + write_file(&cache_dir.join("bin/psql"), b"psql"); write_file( &cache_dir.join("share/postgresql/postgresql.conf.sample"), b"# sample\n", diff --git a/src/sdks/rust/src/liboliphaunt/root/runtime/install.rs b/src/sdks/rust/src/liboliphaunt/root/runtime/install.rs index 1c6ac577..4cd68a2e 100644 --- a/src/sdks/rust/src/liboliphaunt/root/runtime/install.rs +++ b/src/sdks/rust/src/liboliphaunt/root/runtime/install.rs @@ -1,5 +1,5 @@ use std::fs; -use std::path::Path; +use std::path::{Path, PathBuf}; use super::super::NativeRuntimeProfile; use super::super::extensions::{ @@ -13,6 +13,7 @@ use super::super::files::{ use super::super::{ NATIVE_RUNTIME_TOOLS, NATIVE_TOOLS_PACKAGE_TOOLS, existing_native_tool_path, native_tool_path, }; +use super::extension_artifact_root_for; use crate::error::{Error, Result}; use crate::extension::Extension; @@ -21,6 +22,7 @@ pub(super) fn install_cached_runtime( install_dir: &Path, tools_dir: Option<&Path>, embedded_modules: Option<&Path>, + extension_artifact_dirs: &[PathBuf], runtime_dir: &Path, extensions: &[Extension], ) -> Result<()> { @@ -32,29 +34,45 @@ pub(super) fn install_cached_runtime( })?; for tool in NATIVE_RUNTIME_TOOLS { - let source = existing_native_tool_path(install_dir, tool); - if source.is_file() { - install_runtime_tool(&source, &native_tool_path(runtime_dir, tool))?; - } + install_required_runtime_tool(install_dir, runtime_dir, tool, "native runtime")?; } let tools_dir = tools_dir.unwrap_or(install_dir); for tool in NATIVE_TOOLS_PACKAGE_TOOLS { - let source = existing_native_tool_path(tools_dir, tool); - if source.is_file() { - install_runtime_tool(&source, &native_tool_path(runtime_dir, tool))?; - } + install_required_runtime_tool(tools_dir, runtime_dir, tool, "native tools")?; } - install_native_share_tree(install_dir, runtime_dir, extensions)?; + install_native_share_tree( + install_dir, + extension_artifact_dirs, + runtime_dir, + extensions, + )?; install_native_library_tree( profile, install_dir, embedded_modules, + extension_artifact_dirs, runtime_dir, extensions, ) } +fn install_required_runtime_tool( + source_root: &Path, + runtime_dir: &Path, + tool: &str, + label: &str, +) -> Result<()> { + let source = existing_native_tool_path(source_root, tool); + if !source.is_file() { + return Err(Error::Engine(format!( + "{label} artifact is missing required PostgreSQL tool {tool} at {}", + source.display() + ))); + } + install_runtime_tool(&source, &native_tool_path(runtime_dir, tool)) +} + fn install_runtime_tool(source: &Path, destination: &Path) -> Result<()> { copy_file_preserving_permissions(source, destination)?; ensure_runtime_tool_executable(destination) @@ -91,6 +109,7 @@ fn ensure_runtime_tool_executable(_path: &Path) -> Result<()> { fn install_native_share_tree( install_dir: &Path, + extension_artifact_dirs: &[PathBuf], runtime_dir: &Path, extensions: &[Extension], ) -> Result<()> { @@ -114,8 +133,11 @@ fn install_native_share_tree( copy_named_extension_sql_files(&source_share, &target_share, "plpgsql", true)?; for extension in extensions { - copy_extension_sql_files(&source_share, &target_share, *extension)?; - copy_extension_data_files(&source_share, &target_share, *extension)?; + let extension_root = + extension_artifact_root_for(install_dir, extension_artifact_dirs, *extension); + let extension_share = extension_root.join("share/postgresql"); + copy_extension_sql_files(&extension_share, &target_share, *extension)?; + copy_extension_data_files(&extension_share, &target_share, *extension)?; } Ok(()) } @@ -143,6 +165,7 @@ mod tests { &install_dir, None, None, + &[], &temp.path().join("runtime"), &extensions, ) @@ -171,6 +194,7 @@ mod tests { &install_dir, None, None, + &[], &runtime_dir, &[Extension::Vector], ) @@ -202,6 +226,39 @@ mod tests { ); } + #[test] + fn install_copies_selected_extension_assets_from_sidecar_artifact() { + let temp = TempTree::new("sidecar-extension-assets"); + let install_dir = temp.path().join("install"); + let extension_dir = temp.path().join("extension/oliphaunt-extension-hstore"); + let runtime_dir = temp.path().join("runtime"); + write_minimal_install(&install_dir); + write_extension_assets(&extension_dir, Extension::Hstore); + + install_cached_runtime( + NativeRuntimeProfile::PostgresServer, + &install_dir, + None, + None, + &[extension_dir], + &runtime_dir, + &[Extension::Hstore], + ) + .unwrap(); + + assert!( + runtime_dir + .join("share/postgresql/extension/hstore.control") + .is_file() + ); + assert!( + runtime_dir + .join("lib/postgresql") + .join(Extension::Hstore.native_module_file().unwrap()) + .is_file() + ); + } + #[cfg(unix)] #[test] fn install_restores_executable_bits_for_runtime_tools() { @@ -228,6 +285,7 @@ mod tests { &install_dir, None, None, + &[], &runtime_dir, &[], ) @@ -266,6 +324,7 @@ mod tests { &install_dir, None, None, + &[], &runtime_dir, &[], ) @@ -277,6 +336,30 @@ mod tests { ); } + #[test] + fn install_copies_runtime_library_root_files() { + let temp = TempTree::new("runtime-lib-root"); + let install_dir = temp.path().join("install"); + let runtime_dir = temp.path().join("runtime"); + write_minimal_install(&install_dir); + + install_cached_runtime( + NativeRuntimeProfile::PostgresServer, + &install_dir, + None, + None, + &[], + &runtime_dir, + &[], + ) + .unwrap(); + + assert_eq!( + fs::read(runtime_dir.join("lib/libpq.so")).unwrap(), + b"libpq" + ); + } + #[test] fn install_accepts_icu_enabled_installs_without_icu_data() { let temp = TempTree::new("missing-icu-data"); @@ -293,6 +376,7 @@ mod tests { &install_dir, None, None, + &[], &runtime_dir, &[], ) @@ -317,6 +401,7 @@ mod tests { &install_dir, Some(&tools_dir), None, + &[], &runtime_dir, &[], ) @@ -365,6 +450,8 @@ mod tests { write_file(&install_dir.join("bin/postgres"), b"postgres"); write_file(&install_dir.join("bin/initdb"), b"initdb"); write_file(&install_dir.join("bin/pg_ctl"), b"pg_ctl"); + write_file(&install_dir.join("bin/pg_dump"), b"pg_dump"); + write_file(&install_dir.join("bin/psql"), b"psql"); write_file( &install_dir.join("share/postgresql/postgresql.conf.sample"), b"# sample\n", @@ -378,6 +465,7 @@ mod tests { b"select 'plpgsql install';\n", ); fs::create_dir_all(install_dir.join("lib/postgresql")).expect("create lib dir"); + write_file(&install_dir.join("lib/libpq.so"), b"libpq"); } fn write_extension_assets(install_dir: &Path, extension: Extension) { @@ -413,9 +501,12 @@ fn install_native_library_tree( profile: NativeRuntimeProfile, install_dir: &Path, embedded_modules: Option<&Path>, + extension_artifact_dirs: &[PathBuf], runtime_dir: &Path, extensions: &[Extension], ) -> Result<()> { + install_runtime_library_root(install_dir, runtime_dir)?; + let source_lib = install_dir.join("lib/postgresql"); let target_lib = runtime_dir.join("lib/postgresql"); if !source_lib.is_dir() { @@ -465,19 +556,29 @@ fn install_native_library_tree( let Some(module) = extension.native_module_file() else { continue; }; + let extension_root = + extension_artifact_root_for(install_dir, extension_artifact_dirs, *extension); + let extension_lib = extension_root.join("lib/postgresql"); match profile { NativeRuntimeProfile::OliphauntEmbedded => { - let embedded_modules = embedded_modules.ok_or_else(|| { - Error::Engine( - "native liboliphaunt runtime requires embedded PostgreSQL extension modules" - .to_owned(), - ) - })?; - copy_embedded_module(embedded_modules, &target_lib, &module)?; + if extension_lib.join(&module).is_file() { + copy_file_preserving_permissions( + &extension_lib.join(&module), + &target_lib.join(&module), + )?; + } else { + let embedded_modules = embedded_modules.ok_or_else(|| { + Error::Engine( + "native liboliphaunt runtime requires embedded PostgreSQL extension modules" + .to_owned(), + ) + })?; + copy_embedded_module(embedded_modules, &target_lib, &module)?; + } } NativeRuntimeProfile::PostgresServer => { copy_file_preserving_permissions( - &source_lib.join(&module), + &extension_lib.join(&module), &target_lib.join(&module), )?; } @@ -485,3 +586,28 @@ fn install_native_library_tree( } Ok(()) } + +fn install_runtime_library_root(install_dir: &Path, runtime_dir: &Path) -> Result<()> { + let source_lib = install_dir.join("lib"); + if !source_lib.is_dir() { + return Ok(()); + } + let target_lib = runtime_dir.join("lib"); + fs::create_dir_all(&target_lib).map_err(|err| { + Error::Engine(format!( + "create native runtime library dir {}: {err}", + target_lib.display() + )) + })?; + for entry in fs::read_dir(&source_lib) + .map_err(|err| Error::Engine(format!("read native runtime library dir: {err}")))? + { + let entry = entry + .map_err(|err| Error::Engine(format!("read native runtime library entry: {err}")))?; + let source = entry.path(); + if source.is_file() { + copy_file_preserving_permissions(&source, &target_lib.join(entry.file_name()))?; + } + } + Ok(()) +} diff --git a/src/sdks/rust/src/liboliphaunt/root/runtime/locate.rs b/src/sdks/rust/src/liboliphaunt/root/runtime/locate.rs index 1913eeb9..7d7560bc 100644 --- a/src/sdks/rust/src/liboliphaunt/root/runtime/locate.rs +++ b/src/sdks/rust/src/liboliphaunt/root/runtime/locate.rs @@ -4,6 +4,7 @@ use super::super::super::ffi::{ ENV_EMBEDDED_MODULE_DIR, ENV_INITDB, ENV_INSTALL_DIR, ENV_POSTGRES, env_path_candidates, resolve_library_path_candidates, }; +use crate::build_resources::registered_build_resources_dir; use crate::error::{Error, Result}; const ENV_RESOURCES_DIR: &str = "OLIPHAUNT_RESOURCES_DIR"; @@ -12,8 +13,8 @@ const ENV_TOOLS_DIR: &str = "OLIPHAUNT_TOOLS_DIR"; pub(super) fn locate_native_install_dir() -> Result { let mut candidates = Vec::new(); candidates.extend(env_path_candidates([ENV_INSTALL_DIR])); - if let Some(path) = std::env::var_os(ENV_RESOURCES_DIR) { - candidates.push(PathBuf::from(path).join("native-runtime/liboliphaunt-native/runtime")); + for path in resources_dir_candidates() { + candidates.push(path.join("native-runtime/liboliphaunt-native/runtime")); } for env_name in [ENV_POSTGRES, ENV_INITDB] { if let Some(path) = std::env::var_os(env_name) { @@ -49,8 +50,8 @@ pub(super) fn locate_native_install_dir() -> Result { pub(super) fn locate_native_tools_dir(install_dir: &Path) -> Option { let mut candidates = Vec::new(); candidates.extend(env_path_candidates([ENV_TOOLS_DIR])); - if let Some(path) = std::env::var_os(ENV_RESOURCES_DIR) { - candidates.push(PathBuf::from(path).join("native-tools/oliphaunt-tools/runtime")); + for path in resources_dir_candidates() { + candidates.push(path.join("native-tools/oliphaunt-tools/runtime")); } candidates.push(install_dir.to_path_buf()); candidates @@ -58,6 +59,25 @@ pub(super) fn locate_native_tools_dir(install_dir: &Path) -> Option { .find(|candidate| native_tools_dir_is_valid(candidate)) } +pub(super) fn locate_native_extension_artifact_dirs() -> Vec { + let mut dirs = Vec::new(); + for resources_dir in resources_dir_candidates() { + let extension_root = resources_dir.join("extension"); + let Ok(entries) = std::fs::read_dir(extension_root) else { + continue; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + dirs.push(path); + } + } + } + dirs.sort(); + dirs.dedup(); + dirs +} + pub(super) fn locate_native_embedded_modules_dir(install_dir: &Path) -> Result { locate_native_embedded_modules_dir_from_libraries( install_dir, @@ -119,6 +139,17 @@ fn native_tool_is_file(path: &Path, tool: &str) -> bool { path.join("bin").join(tool).is_file() || path.join("bin").join(format!("{tool}.exe")).is_file() } +fn resources_dir_candidates() -> Vec { + let mut candidates = Vec::new(); + if let Some(path) = registered_build_resources_dir() { + candidates.push(path); + } + if let Some(path) = std::env::var_os(ENV_RESOURCES_DIR) { + candidates.push(PathBuf::from(path)); + } + candidates +} + fn native_host_target_id() -> Option<&'static str> { match (std::env::consts::OS, std::env::consts::ARCH) { ("macos", "aarch64") => Some("macos-arm64"), diff --git a/src/sdks/rust/src/liboliphaunt/root/template.rs b/src/sdks/rust/src/liboliphaunt/root/template.rs index c4553a68..21c471c8 100644 --- a/src/sdks/rust/src/liboliphaunt/root/template.rs +++ b/src/sdks/rust/src/liboliphaunt/root/template.rs @@ -12,7 +12,7 @@ use super::files::{ }; use super::fingerprint::{hash_path, hash_str, new_state}; use super::runtime::{materialize_runtime, monotonic_cache_nonce, runtime_cache_root}; -use super::{NativeRuntimeProfile, native_tool_path}; +use super::{NativeRuntimeProfile, configure_native_tool_env, native_tool_path}; use crate::error::{Error, Result}; use crate::storage::BootstrapStrategy; @@ -233,6 +233,7 @@ fn template_initdb_args(runtime_dir: &Path, pgdata: &Path) -> Vec { } fn configure_template_runtime_env(command: &mut Command, runtime_dir: &Path) { + configure_native_tool_env(command, runtime_dir); let icu_data = runtime_dir.join("share/icu"); if icu_data.is_dir() { command.env("ICU_DATA", icu_data); diff --git a/src/sdks/rust/src/runtime_resources/package.rs b/src/sdks/rust/src/runtime_resources/package.rs index 648ae3e8..19365a36 100644 --- a/src/sdks/rust/src/runtime_resources/package.rs +++ b/src/sdks/rust/src/runtime_resources/package.rs @@ -1,4 +1,5 @@ use super::*; +use crate::build_resources::registered_build_resources_dir; pub(super) fn prepare_output_root(root: &Path, replace_existing: bool) -> Result<()> { if root.exists() { @@ -126,6 +127,9 @@ fn find_icu_data_root(materialized: &MaterializedNativeResources) -> Option list[dict]: }, ] ) + for target in sorted(published & set(DESKTOP_TARGETS)): + platform = DESKTOP_TARGETS[target] + rows.append( + { + "id": f"{product}.tools-{target}", + "product": product, + "kind": "native-tools", + "target": target, + "triple": platform["triple"], + "runner": platform["runner"], + "asset": _archive_asset("liboliphaunt", target, platform.get("archive", "tar.gz")), + "npm_package": platform.get("liboliphaunt_tools_npm_package"), + "npm_os": platform.get("npm_os"), + "npm_cpu": platform.get("npm_cpu"), + "npm_libc": platform.get("npm_libc"), + "surfaces": ["typescript-native-direct"], + "published": True, + "_source_file": "Moon release metadata", + } + ) return rows diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index bb99e112..e0018378 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -345,6 +345,10 @@ def check_liboliphaunt(findings: list[Finding]) -> None: "npm:@oliphaunt/liboliphaunt-linux-x64-gnu", "npm:@oliphaunt/liboliphaunt-linux-arm64-gnu", "npm:@oliphaunt/liboliphaunt-win32-x64-msvc", + "npm:@oliphaunt/tools-darwin-arm64", + "npm:@oliphaunt/tools-linux-arm64-gnu", + "npm:@oliphaunt/tools-linux-x64-gnu", + "npm:@oliphaunt/tools-win32-x64-msvc", "maven:dev.oliphaunt.runtime:oliphaunt-icu", "maven:dev.oliphaunt.runtime:liboliphaunt-runtime-resources", "maven:dev.oliphaunt.runtime:liboliphaunt-android-arm64-v8a", @@ -1248,6 +1252,10 @@ def check_typescript(findings: list[Finding]) -> None: "@oliphaunt/node-direct-linux-x64-gnu": product_metadata.read_current_version("oliphaunt-node-direct"), "@oliphaunt/node-direct-linux-arm64-gnu": product_metadata.read_current_version("oliphaunt-node-direct"), "@oliphaunt/node-direct-win32-x64-msvc": product_metadata.read_current_version("oliphaunt-node-direct"), + "@oliphaunt/tools-darwin-arm64": product_metadata.read_current_version("liboliphaunt-native"), + "@oliphaunt/tools-linux-x64-gnu": product_metadata.read_current_version("liboliphaunt-native"), + "@oliphaunt/tools-linux-arm64-gnu": product_metadata.read_current_version("liboliphaunt-native"), + "@oliphaunt/tools-win32-x64-msvc": product_metadata.read_current_version("liboliphaunt-native"), } optional_dependencies = package.get("optionalDependencies", {}) require( diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 302832c1..36ac2c2b 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -138,7 +138,7 @@ def validate_platform_npm_packages( metadata = package.get("oliphaunt") if not isinstance(metadata, dict) or metadata.get("target") != target.target: fail(f"{target.npm_package} package oliphaunt.target must be {target.target}") - if product == "liboliphaunt-native": + if product == "liboliphaunt-native" and kind == "native-runtime": if target.library_relative_path is None: fail(f"{target.id} must declare library_relative_path") if metadata.get("libraryRelativePath") != target.library_relative_path: @@ -148,7 +148,19 @@ def validate_platform_npm_packages( files = ["bin", "runtime", "README.md"] if target.target == "windows-x64-msvc" else ["lib", "runtime", "README.md"] executable_files = [ f"./runtime/bin/{tool}" - for tool in sorted(optimize_native_runtime_payload.packaged_runtime_tools(target.target)) + for tool in sorted(optimize_native_runtime_payload.required_runtime_tools(target.target)) + ] + elif product == "liboliphaunt-native" and kind == "native-tools": + if metadata.get("product") != "oliphaunt-tools": + fail(f"{target.npm_package} product must be oliphaunt-tools") + if metadata.get("kind") != "native-tools": + fail(f"{target.npm_package} kind must be native-tools") + if metadata.get("runtimeRelativePath") != "runtime": + fail(f"{target.npm_package} runtimeRelativePath must be runtime") + files = ["runtime", "README.md"] + executable_files = [ + f"./runtime/bin/{tool}" + for tool in sorted(optimize_native_runtime_payload.required_tools_package_tools(target.target)) ] elif product == "oliphaunt-broker": if target.executable_relative_path is None: @@ -751,6 +763,10 @@ def validate_typescript( "@oliphaunt/node-direct-linux-x64-gnu": node_direct_version, "@oliphaunt/node-direct-linux-arm64-gnu": node_direct_version, "@oliphaunt/node-direct-win32-x64-msvc": node_direct_version, + "@oliphaunt/tools-darwin-arm64": liboliphaunt_version, + "@oliphaunt/tools-linux-x64-gnu": liboliphaunt_version, + "@oliphaunt/tools-linux-arm64-gnu": liboliphaunt_version, + "@oliphaunt/tools-win32-x64-msvc": liboliphaunt_version, } optional_dependencies = package.get("optionalDependencies", {}) if not isinstance(optional_dependencies, dict) or set(optional_dependencies) != set(expected_optional): @@ -769,6 +785,13 @@ def validate_typescript( "src/runtimes/liboliphaunt/native/packages", liboliphaunt_version, ) + validate_platform_npm_packages( + "liboliphaunt-native", + "native-tools", + "typescript-native-direct", + "src/runtimes/liboliphaunt/native/tools-packages", + liboliphaunt_version, + ) icu_package = json.loads(read_text("src/runtimes/liboliphaunt/native/icu-npm/package.json")) icu_metadata = icu_package.get("oliphaunt") if ( diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index 26851edb..631fb2e5 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -1284,11 +1284,9 @@ def stage_cargo_source_crates( ) available_package_names = cargo_package_names_from_roots(roots) native_source_root = ROOT / "target/liboliphaunt/cargo-package-sources" - if native_source_root.is_dir(): - for manifest in sorted(native_source_root.glob("liboliphaunt-native-*/Cargo.toml")): - name, _version = read_cargo_package_name_version(manifest) - if "-part-" not in name: - available_package_names.add(name) + for manifest in native_runtime_artifact_manifests(native_source_root): + name, _version = read_cargo_package_name_version(manifest) + available_package_names.add(name) prune_missing_local_artifact_target_dependencies( oliphaunt_manifest, available_package_names, @@ -1299,17 +1297,33 @@ def stage_cargo_source_crates( wasix_manifest = ROOT / "src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml" generated.append(manual_cargo_package_source(wasix_manifest, output_dir)) - if native_source_root.is_dir(): - for manifest in sorted(native_source_root.glob("liboliphaunt-native-*/Cargo.toml")): - name, _version = read_cargo_package_name_version(manifest) - if "-part-" in name: - continue - generated.append(manual_cargo_package_source(manifest, output_dir)) + for manifest in native_runtime_artifact_manifests(native_source_root): + generated.append(manual_cargo_package_source(manifest, output_dir)) result.staged.extend(rel(path) for path in generated) return generated +def native_runtime_artifact_manifests(source_root: Path) -> list[Path]: + if not source_root.is_dir(): + return [] + manifests = [ + *source_root.glob("liboliphaunt-native-*/Cargo.toml"), + *source_root.glob("oliphaunt-tools-*/Cargo.toml"), + ] + result: list[Path] = [] + seen: set[Path] = set() + for manifest in sorted(manifests): + if manifest in seen: + continue + seen.add(manifest) + name, _version = read_cargo_package_name_version(manifest) + if "-part-" in name: + continue + result.append(manifest) + return result + + def native_extension_cargo_package_name(product: str, target: str) -> str: return f"{product}-{target}" diff --git a/tools/release/optimize_native_runtime_payload.py b/tools/release/optimize_native_runtime_payload.py index 51f85759..b93b1087 100644 --- a/tools/release/optimize_native_runtime_payload.py +++ b/tools/release/optimize_native_runtime_payload.py @@ -20,6 +20,7 @@ NATIVE_RUNTIME_TOOL_STEMS = ("initdb", "pg_ctl", "postgres") NATIVE_TOOLS_TOOL_STEMS = ("pg_dump", "psql") NATIVE_PACKAGED_TOOL_STEMS = (*NATIVE_RUNTIME_TOOL_STEMS, *NATIVE_TOOLS_TOOL_STEMS) +NativeToolSet = Literal["packaged", "runtime", "tools"] ELF_DEBUG_SECTION = re.compile(r"\]\s+\.(debug_[^\s]+|symtab|strtab)\s") MACHO_MAGICS = { b"\xfe\xed\xfa\xce", @@ -107,6 +108,19 @@ def packaged_runtime_tools(target: str | None, runtime_dir: Path | None = None) return NATIVE_PACKAGED_TOOL_STEMS +def runtime_tools_for_set( + target: str | None, + runtime_dir: Path | None = None, + *, + tool_set: NativeToolSet = "packaged", +) -> tuple[str, ...]: + if tool_set == "runtime": + return required_runtime_tools(target, runtime_dir) + if tool_set == "tools": + return required_tools_package_tools(target, runtime_dir) + return packaged_runtime_tools(target, runtime_dir) + + def required_runtime_member_paths(target: str | None, *, prefix: str) -> list[str]: return [f"{prefix.rstrip('/')}/{tool}" for tool in required_runtime_tools(target)] @@ -153,13 +167,18 @@ def is_dev_runtime_file(relative: PurePosixPath, *, windows: bool) -> bool: return False -def prune_runtime_payload(root: Path, target: str | None = None) -> None: +def prune_runtime_payload( + root: Path, + target: str | None = None, + *, + tool_set: NativeToolSet = "packaged", +) -> None: runtime_dir = runtime_dir_for(root) if runtime_dir is None: return windows = is_windows_target(target, runtime_dir) - required_tools = set(packaged_runtime_tools(target, runtime_dir)) + required_tools = set(runtime_tools_for_set(target, runtime_dir, tool_set=tool_set)) bin_dir = runtime_dir / "bin" if bin_dir.is_dir(): for path in sorted(bin_dir.iterdir()): @@ -261,7 +280,13 @@ def validate_native_files(root: Path) -> list[str]: return errors -def validate_runtime_tree(root: Path, target: str | None, require_runtime: bool) -> list[str]: +def validate_runtime_tree( + root: Path, + target: str | None, + require_runtime: bool, + *, + tool_set: NativeToolSet = "packaged", +) -> list[str]: errors: list[str] = [] runtime_dir = runtime_dir_for(root) if runtime_dir is None: @@ -270,7 +295,7 @@ def validate_runtime_tree(root: Path, target: str | None, require_runtime: bool) return errors windows = is_windows_target(target, runtime_dir) - required_tools = set(packaged_runtime_tools(target, runtime_dir)) + required_tools = set(runtime_tools_for_set(target, runtime_dir, tool_set=tool_set)) bin_dir = runtime_dir / "bin" if require_runtime and not bin_dir.is_dir(): errors.append(f"{rel(runtime_dir)} is missing bin") @@ -312,9 +337,15 @@ def validate_payload( target: str | None = None, *, require_runtime: bool = True, + tool_set: NativeToolSet = "packaged", ) -> None: errors = [ - *validate_runtime_tree(root, target, require_runtime=require_runtime), + *validate_runtime_tree( + root, + target, + require_runtime=require_runtime, + tool_set=tool_set, + ), *validate_native_files(root), ] if errors: @@ -329,12 +360,13 @@ def optimize_payload( *, strip: bool | Literal["auto"] = "auto", require_runtime: bool = True, + tool_set: NativeToolSet = "packaged", ) -> None: - prune_runtime_payload(root, target) + prune_runtime_payload(root, target, tool_set=tool_set) should_strip = strip is True or (strip == "auto" and strip_supported_for_target(target)) if should_strip: strip_payload(root) - validate_payload(root, target, require_runtime=require_runtime) + validate_payload(root, target, require_runtime=require_runtime, tool_set=tool_set) def parse_args(argv: list[str]) -> argparse.Namespace: @@ -352,6 +384,12 @@ def parse_args(argv: list[str]) -> argparse.Namespace: action="store_true", help="validate native files even when the archive is a library-only mobile payload", ) + parser.add_argument( + "--tool-set", + choices=("packaged", "runtime", "tools"), + default="packaged", + help="which packaged runtime bin tools are expected in the payload", + ) return parser.parse_args(argv) @@ -361,13 +399,19 @@ def main(argv: list[str]) -> int: if not root.exists(): fail(f"payload root does not exist: {root}") if args.check: - validate_payload(root, args.target, require_runtime=not args.allow_missing_runtime) + validate_payload( + root, + args.target, + require_runtime=not args.allow_missing_runtime, + tool_set=args.tool_set, + ) return 0 optimize_payload( root, args.target, strip=False if args.no_strip else "auto", require_runtime=not args.allow_missing_runtime, + tool_set=args.tool_set, ) return 0 diff --git a/tools/release/package_liboliphaunt_cargo_artifacts.py b/tools/release/package_liboliphaunt_cargo_artifacts.py index be2711a6..906bfdcb 100644 --- a/tools/release/package_liboliphaunt_cargo_artifacts.py +++ b/tools/release/package_liboliphaunt_cargo_artifacts.py @@ -150,6 +150,7 @@ def write_part_crate( artifact_product: str, artifact_label: str, ) -> None: + shutil.rmtree(crate_dir, ignore_errors=True) name = part_package_name(target_id, index, package_base=package_base) links = part_links_name(target_id, index, artifact_product=artifact_product) (crate_dir / "src").mkdir(parents=True, exist_ok=True) @@ -227,6 +228,7 @@ def write_aggregator_crate( artifact_kind: str, artifact_label: str, ) -> None: + shutil.rmtree(crate_dir, ignore_errors=True) if target.triple is None: fail(f"{target.id} must declare Cargo target triple") name = cargo_package_name(target.target, package_base=package_base) @@ -740,9 +742,18 @@ def package_target( fail(f"missing liboliphaunt native release asset: {rel(archive)}") extracted_root = source_root / f"{target.target}-extracted" extract_archive(archive, extracted_root) - optimize_native_runtime_payload.optimize_payload(extracted_root, target.target) tools_root = source_root / f"{target.target}-tools-extracted" copy_tools_payload(extracted_root, tools_root, target.target) + optimize_native_runtime_payload.optimize_payload( + extracted_root, + target.target, + tool_set="runtime", + ) + optimize_native_runtime_payload.optimize_payload( + tools_root, + target.target, + tool_set="tools", + ) return [ *package_payload( extracted_root, diff --git a/tools/release/release.py b/tools/release/release.py index 17352501..93374627 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -2216,7 +2216,48 @@ def stage_liboliphaunt_npm_payloads(version: str) -> dict[str, Path]: stage / target.library_relative_path, ) extract_tar_tree(archive, "runtime", stage / "runtime") - optimize_native_runtime_payload.optimize_payload(stage, target.target) + remove_native_tools_from_runtime(stage, target.target) + optimize_native_runtime_payload.optimize_payload(stage, target.target, tool_set="runtime") + stages[package_name] = stage + return stages + + +def remove_native_tools_from_runtime(stage: Path, target: str) -> None: + runtime_dir = stage / "runtime" + for tool in optimize_native_runtime_payload.required_tools_package_tools(target, runtime_dir): + path = runtime_dir / "bin" / tool + if not path.is_file(): + fail(f"{stage.relative_to(ROOT)} is missing native tools payload bin/{tool}") + path.unlink() + optimize_native_runtime_payload.prune_empty_dirs(runtime_dir) + + +def stage_liboliphaunt_tools_npm_payloads(version: str) -> dict[str, Path]: + ensure_liboliphaunt_release_assets() + asset_dir = liboliphaunt_release_asset_dir() + packages = artifact_npm_package_targets( + "liboliphaunt-native", + "native-tools", + "typescript-native-direct", + ROOT / "src/runtimes/liboliphaunt/native/tools-packages", + ) + stages: dict[str, Path] = {} + for package_name, package_dir, target in packages: + stage = stage_npm_package_descriptor( + package_name, + package_dir, + version, + target=target.target, + ) + archive = asset_dir / target.asset_name(version) + for tool in optimize_native_runtime_payload.required_tools_package_tools(target.target): + member = f"runtime/bin/{tool}" + destination = stage / member + if archive.name.endswith(".zip"): + extract_zip_file(archive, member, destination, mode=0o755) + else: + extract_tar_file(archive, member, destination) + optimize_native_runtime_payload.optimize_payload(stage, target.target, tool_set="tools") stages[package_name] = stage return stages @@ -2303,6 +2344,7 @@ def node_direct_optional_npm_tarballs(version: str) -> list[tuple[str, Path]]: def liboliphaunt_npm_tarballs(version: str) -> list[tuple[str, Path]]: packages: list[tuple[str, Path]] = [] stages = stage_liboliphaunt_npm_payloads(version) + tools_stages = stage_liboliphaunt_tools_npm_payloads(version) for package_name, _package_dir, target in artifact_npm_package_targets( "liboliphaunt-native", "native-runtime", @@ -2313,7 +2355,7 @@ def liboliphaunt_npm_tarballs(version: str) -> list[tuple[str, Path]]: fail(f"{target.id} must declare library_relative_path for npm artifact package publication") runtime_members = [ f"package/runtime/bin/{tool}" - for tool in sorted(optimize_native_runtime_payload.packaged_runtime_tools(target.target)) + for tool in sorted(optimize_native_runtime_payload.required_runtime_tools(target.target)) ] required_members = [f"package/{target.library_relative_path}", *runtime_members] package_dir = stages[package_name] @@ -2326,6 +2368,25 @@ def liboliphaunt_npm_tarballs(version: str) -> list[tuple[str, Path]]: target=target.target, ) packages.append((package_name, tarball)) + for package_name, _package_dir, target in artifact_npm_package_targets( + "liboliphaunt-native", + "native-tools", + "typescript-native-direct", + ROOT / "src/runtimes/liboliphaunt/native/tools-packages", + ): + runtime_members = [ + f"package/runtime/bin/{tool}" + for tool in sorted(optimize_native_runtime_payload.required_tools_package_tools(target.target)) + ] + tarball = npm_pack_and_validate( + package_name, + tools_stages[package_name], + version, + required_members=runtime_members, + executable_members=tuple(runtime_members), + target=target.target, + ) + packages.append((package_name, tarball)) icu_package = "@oliphaunt/icu" icu_stage = stage_liboliphaunt_icu_npm_payload(version) icu_tarball = pnpm_pack_for_npm_publish(icu_stage) diff --git a/tools/release/sync-example-lockfiles.py b/tools/release/sync-example-lockfiles.py index 3e49444d..3f4a05d4 100755 --- a/tools/release/sync-example-lockfiles.py +++ b/tools/release/sync-example-lockfiles.py @@ -13,10 +13,15 @@ INTERNAL_PACKAGE_MANIFESTS = [ ROOT / "src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml", ROOT / "src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml", + ROOT / "src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml", ROOT / "src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/Cargo.toml", ROOT / "src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/Cargo.toml", ROOT / "src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/Cargo.toml", ROOT / "src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/Cargo.toml", + ROOT / "src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/Cargo.toml", + ROOT / "src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/Cargo.toml", + ROOT / "src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/Cargo.toml", + ROOT / "src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/Cargo.toml", ] PACKAGE_START_RE = re.compile(r"^\s*\[\[package\]\]\s*$") STRING_KEY_RE = re.compile(r'^\s*([A-Za-z0-9_-]+)\s*=\s*"([^"]*)"\s*(?:#.*)?$') @@ -70,28 +75,25 @@ def package_block_ranges(lines: list[str]) -> list[tuple[int, int]]: ] -def check_lockfile_contains_path_packages(lockfile: pathlib.Path, versions: dict[str, str]) -> None: +def check_lockfile_contains_internal_packages(lockfile: pathlib.Path, versions: dict[str, str]) -> None: data = tomllib.loads(lockfile.read_text(encoding="utf-8")) packages = data.get("package") if not isinstance(packages, list): raise SystemExit(f"{lockfile.relative_to(ROOT)} is missing [[package]] entries") - present = { - package.get("name") - for package in packages - if isinstance(package, dict) and package.get("name") in versions and "source" not in package - } + present = {package.get("name") for package in packages if isinstance(package, dict)} missing = sorted(set(versions) - present) if missing: raise SystemExit( - f"{lockfile.relative_to(ROOT)} is missing internal path packages: {', '.join(missing)}" + f"{lockfile.relative_to(ROOT)} is missing internal Oliphaunt packages: {', '.join(missing)}" ) -def sync_lockfile(lockfile: pathlib.Path, versions: dict[str, str]) -> list[str]: - check_lockfile_contains_path_packages(lockfile, versions) +def sync_lockfile(lockfile: pathlib.Path, versions: dict[str, str], *, check: bool) -> list[str]: + check_lockfile_contains_internal_packages(lockfile, versions) lines = lockfile.read_text(encoding="utf-8").splitlines(keepends=True) changes = [] + registry_changes = [] for start, end in package_block_ranges(lines): block = lines[start:end] @@ -118,11 +120,24 @@ def sync_lockfile(lockfile: pathlib.Path, versions: dict[str, str]) -> list[str] expected_version = versions[name] if current_version != expected_version: - lines[version_idx] = replace_version_line(lines[version_idx], expected_version) + if has_source: + registry_changes.append( + f"{lockfile.relative_to(ROOT)}: {name} {current_version} -> {expected_version}" + ) + continue + if not check: + lines[version_idx] = replace_version_line(lines[version_idx], expected_version) changes.append( f"{lockfile.relative_to(ROOT)}: {name} {current_version} -> {expected_version}" ) + if registry_changes: + for change in registry_changes: + print(change, file=sys.stderr) + raise SystemExit( + "registry-sourced example lockfiles are stale; run Cargo update through " + "`examples/tools/with-local-registries.sh` after staging the local registry" + ) if changes: lockfile.write_text("".join(lines), encoding="utf-8") return changes @@ -137,7 +152,7 @@ def main() -> int: all_changes = [] for lockfile in LOCKFILES: before = lockfile.read_text(encoding="utf-8") - changes = sync_lockfile(lockfile, versions) + changes = sync_lockfile(lockfile, versions, check=args.check) if args.check and changes: lockfile.write_text(before, encoding="utf-8") all_changes.extend(changes) From 4c480d3b59490d60867e2a31811c75bd30f49ad0 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Thu, 25 Jun 2026 20:51:55 +0000 Subject: [PATCH 013/137] fix: split wasix runtime tools artifacts --- .../examples-ci-release-validation.md | 33 +++++-- examples/electron-wasix/src-wasix/Cargo.lock | 89 +++++++++++++++---- examples/tauri-wasix/src-tauri/Cargo.lock | 89 +++++++++++++++---- .../tauri-sqlx-vanilla/src-tauri/Cargo.lock | 86 ++++++++++++++++++ .../wasix/assets/build/docker_initdb.sh | 20 ++++- .../wasix/assets/build/docker_psql.sh | 13 ++- .../wasix_shim/oliphaunt_wasix_initdb_shim.c | 51 +++++++++++ tools/release/check_artifact_targets.py | 17 ++-- tools/release/local_registry_publish.py | 10 +++ ...kage_liboliphaunt_wasix_cargo_artifacts.py | 72 ++++++++++++++- tools/release/release.py | 16 ++-- tools/xtask/src/asset_checks.rs | 17 ++++ 12 files changed, 451 insertions(+), 62 deletions(-) diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index c0916ab3..04c5b1bb 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -5,8 +5,8 @@ the release/tooling surface after the runtime tool crate split. ## P0: Registry-First Example Validation -- [ ] Rebuild or stage current local registry artifacts from the active branch. -- [ ] Publish local Cargo crates into `target/local-registries/cargo`, including: +- [x] Rebuild or stage current local registry artifacts from the active branch. +- [x] Publish local Cargo crates into `target/local-registries/cargo`, including: - `liboliphaunt-native-linux-x64-gnu` - `oliphaunt-tools-linux-x64-gnu` - `oliphaunt-broker-linux-x64-gnu` @@ -16,7 +16,7 @@ the release/tooling surface after the runtime tool crate split. - host WASIX AOT and tools-AOT crates - selected WASIX extension crates and extension-AOT crates - [ ] Publish local npm packages to Verdaccio for root desktop examples. -- [ ] Update root examples so their manifests model the registry install path: +- [x] Update root examples so their manifests model the registry install path: - native Tauri explicitly resolves the native tools artifact crate - WASIX examples explicitly resolve the WASIX tools and tools-AOT artifact crates - product-local WASIX example no longer uses path dependencies @@ -24,7 +24,7 @@ the release/tooling surface after the runtime tool crate split. - native example should execute a flow that requires packaged `pg_dump` - WASIX example should execute a flow that requires packaged `pg_dump` - WASIX example should compile with `psql` available from `oliphaunt-wasix-tools` -- [ ] Run `examples/tools/with-local-registries.sh` installs/builds for each root example. +- [x] Run `examples/tools/with-local-registries.sh` installs/builds for each root example. - [ ] Run native and WASIX app smoke flows where available. ## P1: CI and Release Shape @@ -38,7 +38,7 @@ the release/tooling surface after the runtime tool crate split. - WASIX AOT crates - WASIX tools-AOT crates - extension runtime/AOT crates -- [ ] Verify release dry-runs publish the same package families to local registries. +- [x] Verify release dry-runs publish the same package families to local registries. - [ ] Keep release checks DRY: generation, validation, and publication should share one package-family model per ecosystem. - [ ] Validate local Linux CI lanes with a local GitHub Actions runner when practical. @@ -81,7 +81,22 @@ the release/tooling surface after the runtime tool crate split. packages carry the root native runtime, while `@oliphaunt/tools-*` packages carry `pg_dump` and `psql`. `@oliphaunt/ts` keeps the user install path unchanged by selecting both package families as optional dependencies. -- Current local WASIX release assets are stale: the new WASIX packager rejects - them because `oliphaunt.wasix.tar.zst` still contains `oliphaunt/bin/pg_dump`. - A fresh WASIX release asset build is required before WASIX example e2e can be - claimed. +- WASIX portable assets were rebuilt with the runtime root limited to + `postgres` and `initdb`; `pg_ctl` is not bundled for WASIX, and `pg_dump` plus + `psql` are split into standalone tool payloads. +- WASIX Cargo artifact generation now emits `liboliphaunt-wasix-portable`, + `oliphaunt-wasix-tools`, per-target `liboliphaunt-wasix-aot-*`, and + per-target `oliphaunt-wasix-tools-aot-*` crates. The root portable crate, + tools crate, ICU crate, WASIX extension crates, and AOT crates are all below + the 10 MiB crates.io package limit in the local generated artifact set. +- The local Cargo publisher now ignores legacy `oliphaunt-wasix-assets` and + old `oliphaunt-wasix-aot-*` artifact crates when stale target directories are + present, so local registries expose the new split package surface. +- Cargo example checks passed through `examples/tools/with-local-registries.sh` + for native Tauri, Electron WASIX, Tauri WASIX, and the nested WASIX SQLx + Tauri example. The WASIX example lockfiles now pin the new + `oliphaunt-wasix-tools` and `oliphaunt-wasix-tools-aot-*` registry packages. +- Release and asset guards passed for `xtask assets check --strict-generated`, + `check_consumer_shape.py`, and `check_artifact_targets.py`. Native tools are + modeled as derived registry package targets from the native runtime release + archive, not as standalone GitHub release assets. diff --git a/examples/electron-wasix/src-wasix/Cargo.lock b/examples/electron-wasix/src-wasix/Cargo.lock index 8e4916b7..463f842c 100644 --- a/examples/electron-wasix/src-wasix/Cargo.lock +++ b/examples/electron-wasix/src-wasix/Cargo.lock @@ -1545,6 +1545,60 @@ dependencies = [ "windows-link", ] +[[package]] +name = "liboliphaunt-wasix-aot-aarch64-apple-darwin" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "f7c773796df578853baca2f0dcfb610dc78c103f17fbd260f053c5945a5d0ba1" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "9611d8528c54f4a6981217d6acaddaba0b26cbc20841b8698cb14332fd1b8a64" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "43067bd9d8aa2499d867443a39dcba33195f83c525193a730b6e9b7d66570f88" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "8856bae97b2d60f323f5847db4223fe768a0ee34ebb785b795b11482bd1a9b86" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "liboliphaunt-wasix-portable" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "67857a0fbca85a256e60c4ea9901958cad8fb28b7d1ee4033dbdbc0385ab9baa" +dependencies = [ + "oliphaunt-extension-hstore-wasix", + "oliphaunt-extension-pg-trgm-wasix", + "oliphaunt-extension-unaccent-wasix", + "serde", + "serde_json", + "sha2 0.10.9", +] + [[package]] name = "libredox" version = "0.1.17" @@ -1853,7 +1907,10 @@ name = "oliphaunt-electron-wasix-sidecar" version = "0.1.0" dependencies = [ "anyhow", + "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", "oliphaunt-wasix", + "oliphaunt-wasix-tools", + "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu", "serde_json", ] @@ -1879,7 +1936,7 @@ checksum = "9ab06b4d61878a87b53afc7b047d09f5f2fd794528acb5e40d359e599b0fc956" name = "oliphaunt-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "ce6b8585b7d1314c42b2cb9ae8ccad6e65c2c70c6d037607de2e0894dd115f48" +checksum = "4e04c1110a51cbaa3df9f3db71e81edc4040c3cdd9ff8c8596d311d18c726645" dependencies = [ "anyhow", "async-trait", @@ -1893,6 +1950,11 @@ dependencies = [ "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc", "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", "liboliphaunt-wasix-portable", + "oliphaunt-wasix-tools", + "oliphaunt-wasix-tools-aot-aarch64-apple-darwin", + "oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu", + "oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc", + "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu", "regex", "serde", "serde_json", @@ -1910,55 +1972,50 @@ dependencies = [ ] [[package]] -name = "liboliphaunt-wasix-aot-aarch64-apple-darwin" +name = "oliphaunt-wasix-tools" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "9576d617b17ff231bd9edac4e9a4aec7e20b9e09f5db1fe1791d730e2af2b0ac" +checksum = "d0e68ff6be7ea53e3d8685859a8f2cf67597ff4d0badb24623df3bb56824530c" dependencies = [ - "serde_json", "sha2 0.10.9", ] [[package]] -name = "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu" +name = "oliphaunt-wasix-tools-aot-aarch64-apple-darwin" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "43cdd574cd33c901cab077a772364ff82760c0e4d40747c4811fe8cf102ca5c3" +checksum = "5129bc72a7419128b828189dc54a3a5a82eafc1754b08e8b0316528fcdbfea3b" dependencies = [ "serde_json", "sha2 0.10.9", ] [[package]] -name = "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc" +name = "oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "47dbaab95593814aaa187d44e49bc54c02a14a559d6d30f09c0785282ef7467d" +checksum = "00ababb85de5d0fde8235e1f833726944cb4b1ff948de487166759e9d9784390" dependencies = [ "serde_json", "sha2 0.10.9", ] [[package]] -name = "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu" +name = "oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "0afe5cb3df0987556274309165ca158c644437421bd93fa2892023b6a4578da4" +checksum = "f0efc748599c21e28a1900dc055847dbdb65f79948159fb1333229713a4b1bf5" dependencies = [ "serde_json", "sha2 0.10.9", ] [[package]] -name = "liboliphaunt-wasix-portable" +name = "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "6aafe0b142fc074331ae191f07c3df3b0973b6d95dfcf6c88b66d4969fa0bce4" +checksum = "608a00fadaa05b4e1d714024d1ef77d6ce536f1f547cc1dc37ed686bdf1f2340" dependencies = [ - "oliphaunt-extension-hstore-wasix", - "oliphaunt-extension-pg-trgm-wasix", - "oliphaunt-extension-unaccent-wasix", - "serde", "serde_json", "sha2 0.10.9", ] diff --git a/examples/tauri-wasix/src-tauri/Cargo.lock b/examples/tauri-wasix/src-tauri/Cargo.lock index 6b8ecb4d..bb7b2fda 100644 --- a/examples/tauri-wasix/src-tauri/Cargo.lock +++ b/examples/tauri-wasix/src-tauri/Cargo.lock @@ -2738,6 +2738,60 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "liboliphaunt-wasix-aot-aarch64-apple-darwin" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "f7c773796df578853baca2f0dcfb610dc78c103f17fbd260f053c5945a5d0ba1" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "9611d8528c54f4a6981217d6acaddaba0b26cbc20841b8698cb14332fd1b8a64" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "43067bd9d8aa2499d867443a39dcba33195f83c525193a730b6e9b7d66570f88" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "8856bae97b2d60f323f5847db4223fe768a0ee34ebb785b795b11482bd1a9b86" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "liboliphaunt-wasix-portable" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "67857a0fbca85a256e60c4ea9901958cad8fb28b7d1ee4033dbdbc0385ab9baa" +dependencies = [ + "oliphaunt-extension-hstore-wasix", + "oliphaunt-extension-pg-trgm-wasix", + "oliphaunt-extension-unaccent-wasix", + "serde", + "serde_json", + "sha2 0.10.9", +] + [[package]] name = "libredox" version = "0.1.17" @@ -3322,7 +3376,10 @@ name = "oliphaunt-example-tauri-wasix" version = "0.1.0" dependencies = [ "anyhow", + "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", "oliphaunt-wasix", + "oliphaunt-wasix-tools", + "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu", "serde", "sqlx", "tauri", @@ -3353,7 +3410,7 @@ checksum = "9ab06b4d61878a87b53afc7b047d09f5f2fd794528acb5e40d359e599b0fc956" name = "oliphaunt-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "ce6b8585b7d1314c42b2cb9ae8ccad6e65c2c70c6d037607de2e0894dd115f48" +checksum = "4e04c1110a51cbaa3df9f3db71e81edc4040c3cdd9ff8c8596d311d18c726645" dependencies = [ "anyhow", "async-trait", @@ -3367,6 +3424,11 @@ dependencies = [ "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc", "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", "liboliphaunt-wasix-portable", + "oliphaunt-wasix-tools", + "oliphaunt-wasix-tools-aot-aarch64-apple-darwin", + "oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu", + "oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc", + "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu", "regex", "serde", "serde_json", @@ -3384,55 +3446,50 @@ dependencies = [ ] [[package]] -name = "liboliphaunt-wasix-aot-aarch64-apple-darwin" +name = "oliphaunt-wasix-tools" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "9576d617b17ff231bd9edac4e9a4aec7e20b9e09f5db1fe1791d730e2af2b0ac" +checksum = "d0e68ff6be7ea53e3d8685859a8f2cf67597ff4d0badb24623df3bb56824530c" dependencies = [ - "serde_json", "sha2 0.10.9", ] [[package]] -name = "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu" +name = "oliphaunt-wasix-tools-aot-aarch64-apple-darwin" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "43cdd574cd33c901cab077a772364ff82760c0e4d40747c4811fe8cf102ca5c3" +checksum = "5129bc72a7419128b828189dc54a3a5a82eafc1754b08e8b0316528fcdbfea3b" dependencies = [ "serde_json", "sha2 0.10.9", ] [[package]] -name = "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc" +name = "oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "47dbaab95593814aaa187d44e49bc54c02a14a559d6d30f09c0785282ef7467d" +checksum = "00ababb85de5d0fde8235e1f833726944cb4b1ff948de487166759e9d9784390" dependencies = [ "serde_json", "sha2 0.10.9", ] [[package]] -name = "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu" +name = "oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "0afe5cb3df0987556274309165ca158c644437421bd93fa2892023b6a4578da4" +checksum = "f0efc748599c21e28a1900dc055847dbdb65f79948159fb1333229713a4b1bf5" dependencies = [ "serde_json", "sha2 0.10.9", ] [[package]] -name = "liboliphaunt-wasix-portable" +name = "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "6aafe0b142fc074331ae191f07c3df3b0973b6d95dfcf6c88b66d4969fa0bce4" +checksum = "608a00fadaa05b4e1d714024d1ef77d6ce536f1f547cc1dc37ed686bdf1f2340" dependencies = [ - "oliphaunt-extension-hstore-wasix", - "oliphaunt-extension-pg-trgm-wasix", - "oliphaunt-extension-unaccent-wasix", - "serde", "serde_json", "sha2 0.10.9", ] diff --git a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock index f96a4c55..46e56bd3 100644 --- a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock +++ b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock @@ -3527,6 +3527,8 @@ dependencies = [ [[package]] name = "oliphaunt-wasix" version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "4e04c1110a51cbaa3df9f3db71e81edc4040c3cdd9ff8c8596d311d18c726645" dependencies = [ "anyhow", "async-trait", @@ -3540,6 +3542,11 @@ dependencies = [ "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc", "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", "liboliphaunt-wasix-portable", + "oliphaunt-wasix-tools", + "oliphaunt-wasix-tools-aot-aarch64-apple-darwin", + "oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu", + "oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc", + "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu", "regex", "serde", "serde_json", @@ -3559,25 +3566,101 @@ dependencies = [ [[package]] name = "liboliphaunt-wasix-aot-aarch64-apple-darwin" version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "f7c773796df578853baca2f0dcfb610dc78c103f17fbd260f053c5945a5d0ba1" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] [[package]] name = "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu" version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "9611d8528c54f4a6981217d6acaddaba0b26cbc20841b8698cb14332fd1b8a64" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] [[package]] name = "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc" version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "43067bd9d8aa2499d867443a39dcba33195f83c525193a730b6e9b7d66570f88" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] [[package]] name = "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu" version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "8856bae97b2d60f323f5847db4223fe768a0ee34ebb785b795b11482bd1a9b86" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] [[package]] name = "liboliphaunt-wasix-portable" version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "67857a0fbca85a256e60c4ea9901958cad8fb28b7d1ee4033dbdbc0385ab9baa" dependencies = [ "serde", "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "oliphaunt-wasix-tools" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "d0e68ff6be7ea53e3d8685859a8f2cf67597ff4d0badb24623df3bb56824530c" +dependencies = [ + "sha2 0.10.9", +] + +[[package]] +name = "oliphaunt-wasix-tools-aot-aarch64-apple-darwin" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "5129bc72a7419128b828189dc54a3a5a82eafc1754b08e8b0316528fcdbfea3b" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "00ababb85de5d0fde8235e1f833726944cb4b1ff948de487166759e9d9784390" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "f0efc748599c21e28a1900dc055847dbdb65f79948159fb1333229713a4b1bf5" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "608a00fadaa05b4e1d714024d1ef77d6ce536f1f547cc1dc37ed686bdf1f2340" +dependencies = [ + "serde_json", + "sha2 0.10.9", ] [[package]] @@ -5510,7 +5593,10 @@ name = "tauri-sqlx-vanilla" version = "0.1.0" dependencies = [ "anyhow", + "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", "oliphaunt-wasix", + "oliphaunt-wasix-tools", + "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu", "serde", "serde_json", "sqlx", diff --git a/src/runtimes/liboliphaunt/wasix/assets/build/docker_initdb.sh b/src/runtimes/liboliphaunt/wasix/assets/build/docker_initdb.sh index 8eb68c99..7b7ae57a 100755 --- a/src/runtimes/liboliphaunt/wasix/assets/build/docker_initdb.sh +++ b/src/runtimes/liboliphaunt/wasix/assets/build/docker_initdb.sh @@ -90,6 +90,17 @@ fi ICU_CFLAGS="$(oliphaunt_wasix_icu_cflags "$ICU_PREFIX")" ICU_LIBS="$(oliphaunt_wasix_icu_libs "$ICU_PREFIX")" + rebuild_generic_frontend_archives() { + make -s -C "$BUILD_DIR/src/interfaces/libpq" clean + make -s -C "$BUILD_DIR/src/fe_utils" clean + make -s -C "$BUILD_DIR/src/port" clean + make -s -C "$BUILD_DIR/src/common" clean + make -s -C "$BUILD_DIR/src/port" all + make -s -C "$BUILD_DIR/src/common" all + make -s -C "$BUILD_DIR/src/interfaces/libpq" all + make -s -C "$BUILD_DIR/src/fe_utils" all + } + COMMON_CPPFLAGS="-I$PGSRC/src/include/port/wasix-dl $ICU_CFLAGS" COMMON_CFLAGS="$OLIPHAUNT_WASM_PROFILE_CFLAGS -sWASM_EXCEPTIONS=yes -sPIC=yes -Wno-unused-command-line-argument" COMMON_LDFLAGS="$OLIPHAUNT_WASM_PROFILE_LDFLAGS -sWASM_EXCEPTIONS=yes -sPIC=yes -L$ICU_PREFIX/lib" @@ -111,9 +122,10 @@ fi -o "$INITDB_SHIM" make -s -C "$BUILD_DIR/src/bin/initdb" clean - make -s -j"$JOBS" -C "$BUILD_DIR/src/bin/initdb" initdb \ - CFLAGS="$COMMON_CFLAGS -Dsystem=oliphaunt_wasix_initdb_system -Dpopen=oliphaunt_wasix_initdb_popen -Dpclose=oliphaunt_wasix_initdb_pclose -Dgeteuid=oliphaunt_wasix_geteuid -Dgetuid=oliphaunt_wasix_getuid -Dgetegid=oliphaunt_wasix_getegid -Dgetgid=oliphaunt_wasix_getgid -Dgetpwuid=oliphaunt_wasix_getpwuid -Dgetpwuid_r=oliphaunt_wasix_getpwuid_r -Wno-unused-function -Wno-missing-prototypes" \ - LDFLAGS="$COMMON_LDFLAGS -L$BUILD_DIR/src/common -L$BUILD_DIR/src/port" \ - LDFLAGS_EX="$MAIN_LDFLAGS $GENERIC_SHIM $INITDB_SHIM $BUILD_DIR/src/fe_utils/libpgfeutils.a $BUILD_DIR/src/interfaces/libpq/libpq.a $BUILD_DIR/src/common/libpgcommon.a $BUILD_DIR/src/port/libpgport.a $ICU_LIBS" + make -s -j"$JOBS" -C "$BUILD_DIR/src/bin/initdb" initdb \ + CFLAGS="$COMMON_CFLAGS -Dsystem=oliphaunt_wasix_initdb_system -Dpopen=oliphaunt_wasix_initdb_popen -Dpclose=oliphaunt_wasix_initdb_pclose -Dgeteuid=oliphaunt_wasix_geteuid -Dgetuid=oliphaunt_wasix_getuid -Dgetegid=oliphaunt_wasix_getegid -Dgetgid=oliphaunt_wasix_getgid -Dgetpwuid=oliphaunt_wasix_getpwuid -Dgetpwuid_r=oliphaunt_wasix_getpwuid_r -Wno-unused-function -Wno-missing-prototypes" \ + LDFLAGS="$COMMON_LDFLAGS -L$BUILD_DIR/src/common -L$BUILD_DIR/src/port" \ + LDFLAGS_EX="$MAIN_LDFLAGS $GENERIC_SHIM $INITDB_SHIM $BUILD_DIR/src/fe_utils/libpgfeutils.a $BUILD_DIR/src/interfaces/libpq/libpq.a $BUILD_DIR/src/common/libpgcommon.a $BUILD_DIR/src/port/libpgport.a $ICU_LIBS" test -f "$BUILD_DIR/src/bin/initdb/initdb" + rebuild_generic_frontend_archives ' diff --git a/src/runtimes/liboliphaunt/wasix/assets/build/docker_psql.sh b/src/runtimes/liboliphaunt/wasix/assets/build/docker_psql.sh index f604357d..73c26980 100755 --- a/src/runtimes/liboliphaunt/wasix/assets/build/docker_psql.sh +++ b/src/runtimes/liboliphaunt/wasix/assets/build/docker_psql.sh @@ -88,10 +88,21 @@ fi oliphaunt_wasix_check_source_markers sha256sum -c "$BUILD_DIR/.oliphaunt-wasix-bridge-sha256" >/dev/null test "$(oliphaunt_wasix_wasix_profile_signature)" = "$(cat "$BUILD_DIR/.oliphaunt-wasix-build-profile")" + + # initdb uses tool-specific symbol rewrites. Rebuild shared frontend + # archives with the generic bridge before linking standalone psql. + make -s -C "$BUILD_DIR/src/interfaces/libpq" clean + make -s -C "$BUILD_DIR/src/fe_utils" clean + make -s -C "$BUILD_DIR/src/port" clean + make -s -C "$BUILD_DIR/src/common" clean + make -s -C "$BUILD_DIR/src/port" all + make -s -C "$BUILD_DIR/src/common" all + make -s -C "$BUILD_DIR/src/interfaces/libpq" all + make -s -C "$BUILD_DIR/src/fe_utils" all make -s -C "$BUILD_DIR/src/bin/psql" clean make -s -C "$BUILD_DIR/src/bin/psql" psql \ libpq="$BUILD_DIR/src/interfaces/libpq/libpq.a" \ - LIBS="$BUILD_DIR/src/common/libpgcommon.a $BUILD_DIR/src/port/libpgport.a $ICU_LIBS -lm" + LIBS="$BUILD_DIR/src/common/libpgcommon_shlib.a $BUILD_DIR/src/common/libpgcommon_excluded_shlib.a $BUILD_DIR/src/port/libpgport_shlib.a $ICU_LIBS -lm" test -f "$BUILD_DIR/src/bin/psql/psql" if wasixnm -u "$BUILD_DIR/src/bin/psql/psql" | grep -E " PQ[A-Za-z0-9_]+$"; then echo "psql still imports libpq symbols; expected standalone WASIX psql" >&2 diff --git a/src/runtimes/liboliphaunt/wasix/assets/build/wasix_shim/oliphaunt_wasix_initdb_shim.c b/src/runtimes/liboliphaunt/wasix/assets/build/wasix_shim/oliphaunt_wasix_initdb_shim.c index ae272cd0..f1d5b656 100644 --- a/src/runtimes/liboliphaunt/wasix/assets/build/wasix_shim/oliphaunt_wasix_initdb_shim.c +++ b/src/runtimes/liboliphaunt/wasix/assets/build/wasix_shim/oliphaunt_wasix_initdb_shim.c @@ -10,12 +10,14 @@ #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -603,6 +605,55 @@ oliphaunt_wasix_pclose(FILE *file) return oliphaunt_wasix_initdb_pclose(file); } +int +oliphaunt_wasix_setsockopt(int fd, int level, int optname, const void *optval, socklen_t optlen) +{ + (void) fd; + (void) level; + (void) optname; + (void) optval; + (void) optlen; + return 0; +} + +int +oliphaunt_wasix_getsockopt(int fd, int level, int optname, void *optval, socklen_t *optlen) +{ + (void) fd; + (void) level; + (void) optname; + (void) optval; + (void) optlen; + errno = ENOSYS; + return -1; +} + +int +oliphaunt_wasix_getsockname(int fd, struct sockaddr *addr, socklen_t *len) +{ + (void) fd; + (void) addr; + (void) len; + errno = ENOSYS; + return -1; +} + +int +oliphaunt_wasix_connect(int socket, const struct sockaddr *address, socklen_t address_len) +{ + (void) socket; + (void) address; + (void) address_len; + errno = ENOSYS; + return -1; +} + +int +oliphaunt_wasix_poll(struct pollfd fds[], nfds_t nfds, int timeout) +{ + return poll(fds, nfds, timeout); +} + int __wrap_system(const char *command) { diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py index 20ac9757..02b9efc8 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -69,7 +69,11 @@ def validate_target_shape() -> None: raw_target = raw_targets.get(target.id, {}) if "{version}" not in target.asset: fail(f"{target.id} asset template must contain {{version}}") - if target.published and "github-release" not in target.surfaces: + if ( + target.published + and "github-release" not in target.surfaces + and target.kind not in {"native-tools"} + ): fail(f"{target.id} is published but is not a GitHub release asset") if not target.published: if raw_target.get("tier") != "planned": @@ -101,11 +105,12 @@ def validate_target_shape() -> None: ) if target.kind == "broker-helper" and target.executable_relative_path is None: fail(f"{target.id} must declare executable_relative_path") - dedupe_key = (target.product, target.asset) - previous = seen_assets.get(dedupe_key) - if previous is not None: - fail(f"{target.id} and {previous} use the same asset template {target.asset}") - seen_assets[dedupe_key] = target.id + if "github-release" in target.surfaces: + dedupe_key = (target.product, target.asset) + previous = seen_assets.get(dedupe_key) + if previous is not None: + fail(f"{target.id} and {previous} use the same asset template {target.asset}") + seen_assets[dedupe_key] = target.id def validate_moon_runtime_targets() -> None: diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index 631fb2e5..ebcba453 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -45,6 +45,13 @@ CARGO_PACKAGE_SIZE_LIMIT_BYTES = 10 * 1024 * 1024 CARGO_EXTENSION_PART_BYTES = 7 * 1024 * 1024 CARGO_EXTENSION_SPLIT_THRESHOLD_BYTES = 9 * 1024 * 1024 +LEGACY_WASIX_ARTIFACT_CRATES = { + "oliphaunt-wasix-assets", + "oliphaunt-wasix-aot-aarch64-apple-darwin", + "oliphaunt-wasix-aot-aarch64-unknown-linux-gnu", + "oliphaunt-wasix-aot-x86_64-pc-windows-msvc", + "oliphaunt-wasix-aot-x86_64-unknown-linux-gnu", +} LOCAL_PUBLISH_ARTIFACTS = [ "liboliphaunt-native-release-assets", @@ -2183,6 +2190,9 @@ def publish_cargo(roots: list[Path], registry_root: Path, dry_run: bool, strict: if strict: raise continue + if package.get("name") in LEGACY_WASIX_ARTIFACT_CRATES: + result.add_skip(f"ignored legacy WASIX artifact crate {crate_path.name}") + continue target_name = f"{package['name']}-{package['version']}.crate" packages_by_target_name[target_name] = (crate_path, package) diff --git a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py index e79287f3..5b2eeccd 100644 --- a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py +++ b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py @@ -31,6 +31,10 @@ "bin/pg_dump.wasix.wasm", "bin/psql.wasix.wasm", ) +BUNDLED_RUNTIME_TOOL_FILES = ( + "oliphaunt/bin/pg_dump", + "oliphaunt/bin/psql", +) TOOLS_AOT_ARTIFACTS = {"tool:pg_dump", "tool:psql"} AOT_PACKAGES = { "macos-arm64": "liboliphaunt-wasix-aot-aarch64-apple-darwin", @@ -202,6 +206,9 @@ def validate_runtime_payload(root: Path) -> None: manifest = json.loads((root / "manifest.json").read_text(encoding="utf-8")) if manifest.get("extensions") != []: fail(f"{rel(root / 'manifest.json')} must have an empty extensions array") + for tool_key in ["pg-dump", "psql"]: + if manifest.get(tool_key) is not None: + fail(f"{rel(root / 'manifest.json')} must not advertise split WASIX tool {tool_key}") for required in [ "oliphaunt.wasix.tar.zst", "bin/initdb.wasix.wasm", @@ -224,7 +231,7 @@ def validate_runtime_payload(root: Path) -> None: bundled_tools = sorted( member for member in runtime_members - if member in {"oliphaunt/bin/pg_dump", "oliphaunt/bin/psql"} + if member in BUNDLED_RUNTIME_TOOL_FILES ) if bundled_tools: fail( @@ -233,12 +240,67 @@ def validate_runtime_payload(root: Path) -> None: ) +def validate_tools_payload(root: Path) -> None: + actual = {path.relative_to(root).as_posix() for path in payload_files(root)} + expected = set(TOOLS_PAYLOAD_FILES) + if actual != expected: + fail(f"WASIX tools Cargo payload file set mismatch for {rel(root)}: expected {sorted(expected)}, got {sorted(actual)}") + + +def prune_runtime_archive_tools(archive: Path, scratch: Path) -> None: + runtime_members = tar_zstd_members(archive) + if not any(member in BUNDLED_RUNTIME_TOOL_FILES for member in runtime_members): + return + + extract_tar_zstd(archive, scratch) + for member in BUNDLED_RUNTIME_TOOL_FILES: + path = scratch / member + if path.exists(): + path.unlink() + prune_empty_dirs(scratch) + + replacement = archive.with_name(f"{archive.name}.tmp") + if replacement.exists(): + replacement.unlink() + run( + [ + "tar", + "--sort=name", + "--owner=0", + "--group=0", + "--numeric-owner", + "--mtime=@0", + "--use-compress-program=zstd -19", + "-cf", + str(replacement), + "-C", + str(scratch), + "oliphaunt", + ] + ) + replacement.replace(archive) + + +def rewrite_runtime_core_manifest(root: Path) -> None: + manifest_path = root / "manifest.json" + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + runtime = manifest.get("runtime") + if not isinstance(runtime, dict): + fail(f"{rel(manifest_path)} is missing runtime metadata") + runtime["sha256"] = sha256_file(root / "oliphaunt.wasix.tar.zst") + manifest["extensions"] = [] + manifest.pop("pg-dump", None) + manifest.pop("psql", None) + manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") + + def split_runtime_tools_payload(runtime_root: Path, extract_root: Path) -> tuple[Path, Path]: core_root = extract_root / "runtime-core-payload" tools_root = extract_root / "tools-payload" shutil.rmtree(core_root, ignore_errors=True) shutil.rmtree(tools_root, ignore_errors=True) shutil.copytree(runtime_root, core_root) + shutil.rmtree(core_root / "extensions", ignore_errors=True) missing: list[str] = [] for relative in TOOLS_PAYLOAD_FILES: source = runtime_root / relative @@ -253,6 +315,11 @@ def split_runtime_tools_payload(runtime_root: Path, extract_root: Path) -> tuple core_file.unlink() if missing: fail("WASIX tools Cargo payload is missing " + ", ".join(missing)) + prune_runtime_archive_tools( + core_root / "oliphaunt.wasix.tar.zst", + extract_root / "runtime-archive-core-pruned", + ) + rewrite_runtime_core_manifest(core_root) prune_empty_dirs(core_root) return core_root, tools_root @@ -972,8 +1039,9 @@ def package_specs(asset_dir: Path, extract_root: Path, version: str) -> list[Pac runtime_extract = extract_root / "runtime-extracted" extract_tar_zstd(runtime_archive, runtime_extract) runtime_root = target_asset_root(runtime_extract) - validate_runtime_payload(runtime_root) runtime_core_root, tools_root = split_runtime_tools_payload(runtime_root, extract_root) + validate_runtime_payload(runtime_core_root) + validate_tools_payload(tools_root) specs.append( PackageSpec( name=RUNTIME_PACKAGE, diff --git a/tools/release/release.py b/tools/release/release.py index 93374627..ee6a8b6b 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -2353,10 +2353,10 @@ def liboliphaunt_npm_tarballs(version: str) -> list[tuple[str, Path]]: ): if target.library_relative_path is None: fail(f"{target.id} must declare library_relative_path for npm artifact package publication") - runtime_members = [ - f"package/runtime/bin/{tool}" - for tool in sorted(optimize_native_runtime_payload.required_runtime_tools(target.target)) - ] + runtime_members = optimize_native_runtime_payload.required_runtime_member_paths( + target.target, + prefix="package/runtime/bin", + ) required_members = [f"package/{target.library_relative_path}", *runtime_members] package_dir = stages[package_name] tarball = npm_pack_and_validate( @@ -2374,10 +2374,10 @@ def liboliphaunt_npm_tarballs(version: str) -> list[tuple[str, Path]]: "typescript-native-direct", ROOT / "src/runtimes/liboliphaunt/native/tools-packages", ): - runtime_members = [ - f"package/runtime/bin/{tool}" - for tool in sorted(optimize_native_runtime_payload.required_tools_package_tools(target.target)) - ] + runtime_members = optimize_native_runtime_payload.required_tools_member_paths( + target.target, + prefix="package/runtime/bin", + ) tarball = npm_pack_and_validate( package_name, tools_stages[package_name], diff --git a/tools/xtask/src/asset_checks.rs b/tools/xtask/src/asset_checks.rs index 3d45f2fa..5f079509 100644 --- a/tools/xtask/src/asset_checks.rs +++ b/tools/xtask/src/asset_checks.rs @@ -1044,6 +1044,7 @@ pub(crate) fn check_production_wasix_build_inputs() -> Result<()> { "src/runtimes/liboliphaunt/wasix/assets/build/docker_pgxs_extensions.sh", "src/runtimes/liboliphaunt/wasix/assets/build/docker_contrib_extensions.sh", "src/runtimes/liboliphaunt/wasix/assets/build/docker_pgdump.sh", + "src/runtimes/liboliphaunt/wasix/assets/build/docker_psql.sh", "src/runtimes/liboliphaunt/wasix/assets/build/docker_initdb.sh", "src/runtimes/liboliphaunt/wasix/assets/build/wasix_shim/oliphaunt_wasix_initdb_shim.c", "src/runtimes/liboliphaunt/native/portable-uuid/include/uuid/uuid.h", @@ -1084,6 +1085,7 @@ pub(crate) fn check_production_wasix_build_inputs() -> Result<()> { "src/runtimes/liboliphaunt/wasix/assets/build/docker_pgxs_extensions.sh", "src/runtimes/liboliphaunt/wasix/assets/build/docker_contrib_extensions.sh", "src/runtimes/liboliphaunt/wasix/assets/build/docker_pgdump.sh", + "src/runtimes/liboliphaunt/wasix/assets/build/docker_psql.sh", "src/runtimes/liboliphaunt/wasix/assets/build/docker_initdb.sh", "src/runtimes/liboliphaunt/wasix/assets/build/wasix_shim/oliphaunt_wasix_initdb_shim.c", ]; @@ -1270,12 +1272,23 @@ pub(crate) fn check_production_wasix_build_inputs() -> Result<()> { "ICU_LIBS", ], )?; + ensure_file_contains_all( + "src/runtimes/liboliphaunt/wasix/assets/build/docker_psql.sh", + &[ + "build_wasix_icu.sh", + "oliphaunt_wasix_icu_cflags", + "oliphaunt_wasix_icu_libs", + "ICU_CFLAGS", + "ICU_LIBS", + ], + )?; for path in [ "src/runtimes/liboliphaunt/wasix/assets/build/docker_oliphaunt.sh", "src/runtimes/liboliphaunt/wasix/assets/build/docker_runtime_support.sh", "src/runtimes/liboliphaunt/wasix/assets/build/docker_pgxs_extensions.sh", "src/runtimes/liboliphaunt/wasix/assets/build/docker_contrib_extensions.sh", "src/runtimes/liboliphaunt/wasix/assets/build/docker_pgdump.sh", + "src/runtimes/liboliphaunt/wasix/assets/build/docker_psql.sh", "src/runtimes/liboliphaunt/wasix/assets/build/docker_initdb.sh", ] { ensure_file_contains_all(path, &["OLIPHAUNT_WASM_SKIP_IMAGE_BUILD"])?; @@ -1325,6 +1338,7 @@ fn wasix_build_scripts_requiring_docker_env() -> Result> { | "docker_oliphaunt.sh" | "docker_pgdump.sh" | "docker_pgxs_extensions.sh" + | "docker_psql.sh" | "docker_runtime_support.sh" ) }) @@ -1348,6 +1362,7 @@ fn check_root_asset_metadata_keys() -> Result<()> { "oliphaunt-wasix-sha256", "pgdata-template-archive-sha256", "pg-dump-wasix-sha256", + "psql-wasix-sha256", "initdb-wasix-sha256", ] { let needle = format!("{required} = \""); @@ -1412,6 +1427,8 @@ pub(crate) fn check_canonical_asset_layout_in(asset_dir: &Path, strict: bool) -> "oliphaunt/share/timezonesets", "oliphaunt/lib/plpgsql.so", "oliphaunt/lib/dict_snowball.so", + "oliphaunt/bin/pg_dump", + "oliphaunt/bin/psql", ] { if runtime_entries.contains(forbidden) || runtime_entries From cb9845d907f37d36442bce098e23a53bf2e6d782 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Thu, 25 Jun 2026 21:51:15 +0000 Subject: [PATCH 014/137] fix: package runtime tools separately --- examples/electron-wasix/src-wasix/Cargo.lock | 89 ++++++- examples/electron-wasix/src-wasix/Cargo.toml | 1 + examples/electron-wasix/src-wasix/src/main.rs | 49 +++- examples/tauri-wasix/src-tauri/Cargo.lock | 88 ++++++- examples/tauri-wasix/src-tauri/Cargo.toml | 2 +- examples/tauri-wasix/src-tauri/src/lib.rs | 36 ++- examples/tauri/src-tauri/Cargo.lock | 22 +- .../crates/oliphaunt-wasix/Cargo.toml | 14 +- .../tauri-sqlx-vanilla/src-tauri/Cargo.lock | 4 +- src/extensions/artifacts/packages/moon.yml | 1 + ...2026-06-07-transitional-catalog-smoke.json | 2 +- .../generated/docs/extension-evidence.json | 80 +++--- .../assets/generated/asset-inputs.sha256 | 2 +- tools/release/check_artifact_targets.py | 21 +- tools/release/check_consumer_shape.py | 5 +- tools/release/local_registry_publish.py | 19 +- ...kage_liboliphaunt_wasix_cargo_artifacts.py | 242 +++++++++++++++--- tools/release/sync_release_pr.py | 6 +- 18 files changed, 563 insertions(+), 120 deletions(-) diff --git a/examples/electron-wasix/src-wasix/Cargo.lock b/examples/electron-wasix/src-wasix/Cargo.lock index 463f842c..06e2143e 100644 --- a/examples/electron-wasix/src-wasix/Cargo.lock +++ b/examples/electron-wasix/src-wasix/Cargo.lock @@ -1589,11 +1589,23 @@ dependencies = [ name = "liboliphaunt-wasix-portable" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "67857a0fbca85a256e60c4ea9901958cad8fb28b7d1ee4033dbdbc0385ab9baa" +checksum = "74e4a84c8db15e4be7945d7b3a2ab1cb30a687b155367f32a25155891f604e77" dependencies = [ "oliphaunt-extension-hstore-wasix", + "oliphaunt-extension-hstore-wasix-aot-aarch64-apple-darwin", + "oliphaunt-extension-hstore-wasix-aot-aarch64-unknown-linux-gnu", + "oliphaunt-extension-hstore-wasix-aot-x86_64-pc-windows-msvc", + "oliphaunt-extension-hstore-wasix-aot-x86_64-unknown-linux-gnu", "oliphaunt-extension-pg-trgm-wasix", + "oliphaunt-extension-pg-trgm-wasix-aot-aarch64-apple-darwin", + "oliphaunt-extension-pg-trgm-wasix-aot-aarch64-unknown-linux-gnu", + "oliphaunt-extension-pg-trgm-wasix-aot-x86_64-pc-windows-msvc", + "oliphaunt-extension-pg-trgm-wasix-aot-x86_64-unknown-linux-gnu", "oliphaunt-extension-unaccent-wasix", + "oliphaunt-extension-unaccent-wasix-aot-aarch64-apple-darwin", + "oliphaunt-extension-unaccent-wasix-aot-aarch64-unknown-linux-gnu", + "oliphaunt-extension-unaccent-wasix-aot-x86_64-pc-windows-msvc", + "oliphaunt-extension-unaccent-wasix-aot-x86_64-unknown-linux-gnu", "serde", "serde_json", "sha2 0.10.9", @@ -1912,6 +1924,7 @@ dependencies = [ "oliphaunt-wasix-tools", "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu", "serde_json", + "tokio", ] [[package]] @@ -1920,23 +1933,95 @@ version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "1d0b20fd2a03b45880974241e3443d9e324de637fefa4f43859efce70089812b" +[[package]] +name = "oliphaunt-extension-hstore-wasix-aot-aarch64-apple-darwin" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "004e128d02237a749af8e0219532f4af55b65de588709b0cf2bbef99e7fa6292" + +[[package]] +name = "oliphaunt-extension-hstore-wasix-aot-aarch64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "ae54c87147a7b4adba32fc6519a68937a8fb5155c4da28dcf36bd66b3e7e98ad" + +[[package]] +name = "oliphaunt-extension-hstore-wasix-aot-x86_64-pc-windows-msvc" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "98af804e5514ba341aa03e630320e135f7761b60104d4592743d68b324923fa9" + +[[package]] +name = "oliphaunt-extension-hstore-wasix-aot-x86_64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "b71adb2ca0f694aac91994c099572ae14906d333279e7bf91662431f86b8a06f" + [[package]] name = "oliphaunt-extension-pg-trgm-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "6ea075c13c8283d2eb26526c63061b116ffc515899fa59478a8a6c570539a312" +[[package]] +name = "oliphaunt-extension-pg-trgm-wasix-aot-aarch64-apple-darwin" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "0c5c91b06e0a5101433533753876dac7aee89936212967606175c9f141976a14" + +[[package]] +name = "oliphaunt-extension-pg-trgm-wasix-aot-aarch64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "c14ce6cbf988af1eb13f567b9a975f5bf566076688514133c093971f5a737aa6" + +[[package]] +name = "oliphaunt-extension-pg-trgm-wasix-aot-x86_64-pc-windows-msvc" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "d4e164a68f4047ac3c268ef71b9807d33242e06f61bf862bf60df9cb9a47b4ae" + +[[package]] +name = "oliphaunt-extension-pg-trgm-wasix-aot-x86_64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "96f7d7cd8ba652876f221b37e4f290a84d054e2c50625c243803224ce3e12b03" + [[package]] name = "oliphaunt-extension-unaccent-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "9ab06b4d61878a87b53afc7b047d09f5f2fd794528acb5e40d359e599b0fc956" +[[package]] +name = "oliphaunt-extension-unaccent-wasix-aot-aarch64-apple-darwin" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "37e5978c9d6e020c01336f58c8922ebaed2f4dfd6ae4568b5f91b5d416fc7cdb" + +[[package]] +name = "oliphaunt-extension-unaccent-wasix-aot-aarch64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "4ae9dd2c37edc58bf3dc34b88314e5f012221f74c96e9c538133ed162a12509e" + +[[package]] +name = "oliphaunt-extension-unaccent-wasix-aot-x86_64-pc-windows-msvc" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "f869c3c96abb7169927c921e92e44401f148e6de6138213ead88d1208462685d" + +[[package]] +name = "oliphaunt-extension-unaccent-wasix-aot-x86_64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "5c4389eaa071ac1e9bc837958ec1f5caf7f9d44a75a789b576a4938f3f0ec7cc" + [[package]] name = "oliphaunt-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "4e04c1110a51cbaa3df9f3db71e81edc4040c3cdd9ff8c8596d311d18c726645" +checksum = "0fe403cee7d4d080ba6795a93a99d14a43812202639eb7295410c0cd27d6a022" dependencies = [ "anyhow", "async-trait", diff --git a/examples/electron-wasix/src-wasix/Cargo.toml b/examples/electron-wasix/src-wasix/Cargo.toml index 7ceaeee2..8a6e2090 100644 --- a/examples/electron-wasix/src-wasix/Cargo.toml +++ b/examples/electron-wasix/src-wasix/Cargo.toml @@ -15,6 +15,7 @@ oliphaunt-wasix = { version = "=0.1.0", registry = "oliphaunt-local", features = ] } oliphaunt-wasix-tools = { version = "=0.1.0", registry = "oliphaunt-local" } serde_json = "1" +tokio = { version = "1", features = ["rt-multi-thread"] } [target.'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))'.dependencies] liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu = { version = "=0.1.0", registry = "oliphaunt-local" } diff --git a/examples/electron-wasix/src-wasix/src/main.rs b/examples/electron-wasix/src-wasix/src/main.rs index ff163fe5..4140f436 100644 --- a/examples/electron-wasix/src-wasix/src/main.rs +++ b/examples/electron-wasix/src-wasix/src/main.rs @@ -3,12 +3,27 @@ use std::io::{self, Write}; use std::path::PathBuf; use std::thread; -use anyhow::{Context, Result, bail}; -use oliphaunt_wasix::{OliphauntServer, PgDumpOptions, extensions}; +use anyhow::{bail, Context, Result}; +use oliphaunt_wasix::{extensions, OliphauntServer, PgDumpOptions}; use serde_json::json; fn main() -> Result<()> { let root = parse_root()?; + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .context("build WASIX sidecar Tokio runtime")?; + let _runtime_context = runtime.enter(); + let server = start_server(root)?; + println!("{}", json!({ "databaseUrl": server.connection_uri() })); + io::stdout().flush()?; + let _server = server; + loop { + thread::park(); + } +} + +fn start_server(root: PathBuf) -> Result { let server = OliphauntServer::builder() .path(root) .extensions([ @@ -19,12 +34,7 @@ fn main() -> Result<()> { .start() .context("start oliphaunt-wasix server")?; validate_wasix_tools(&server)?; - println!("{}", json!({ "databaseUrl": server.connection_uri() })); - io::stdout().flush()?; - let _server = server; - loop { - thread::park(); - } + Ok(server) } fn validate_wasix_tools(server: &OliphauntServer) -> Result<()> { @@ -46,3 +56,26 @@ fn parse_root() -> Result { } bail!("usage: oliphaunt-electron-wasix-sidecar --root ") } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn startup_smoke_runs_split_wasix_tools() { + let root = std::env::temp_dir().join(format!( + "oliphaunt-electron-wasix-sidecar-smoke-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("build WASIX sidecar smoke runtime"); + let _runtime_context = runtime.enter(); + let server = start_server(root.clone()) + .expect("start sidecar server and run split WASIX pg_dump tool"); + drop(server); + let _ = std::fs::remove_dir_all(root); + } +} diff --git a/examples/tauri-wasix/src-tauri/Cargo.lock b/examples/tauri-wasix/src-tauri/Cargo.lock index bb7b2fda..69135012 100644 --- a/examples/tauri-wasix/src-tauri/Cargo.lock +++ b/examples/tauri-wasix/src-tauri/Cargo.lock @@ -2782,11 +2782,23 @@ dependencies = [ name = "liboliphaunt-wasix-portable" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "67857a0fbca85a256e60c4ea9901958cad8fb28b7d1ee4033dbdbc0385ab9baa" +checksum = "74e4a84c8db15e4be7945d7b3a2ab1cb30a687b155367f32a25155891f604e77" dependencies = [ "oliphaunt-extension-hstore-wasix", + "oliphaunt-extension-hstore-wasix-aot-aarch64-apple-darwin", + "oliphaunt-extension-hstore-wasix-aot-aarch64-unknown-linux-gnu", + "oliphaunt-extension-hstore-wasix-aot-x86_64-pc-windows-msvc", + "oliphaunt-extension-hstore-wasix-aot-x86_64-unknown-linux-gnu", "oliphaunt-extension-pg-trgm-wasix", + "oliphaunt-extension-pg-trgm-wasix-aot-aarch64-apple-darwin", + "oliphaunt-extension-pg-trgm-wasix-aot-aarch64-unknown-linux-gnu", + "oliphaunt-extension-pg-trgm-wasix-aot-x86_64-pc-windows-msvc", + "oliphaunt-extension-pg-trgm-wasix-aot-x86_64-unknown-linux-gnu", "oliphaunt-extension-unaccent-wasix", + "oliphaunt-extension-unaccent-wasix-aot-aarch64-apple-darwin", + "oliphaunt-extension-unaccent-wasix-aot-aarch64-unknown-linux-gnu", + "oliphaunt-extension-unaccent-wasix-aot-x86_64-pc-windows-msvc", + "oliphaunt-extension-unaccent-wasix-aot-x86_64-unknown-linux-gnu", "serde", "serde_json", "sha2 0.10.9", @@ -3394,23 +3406,95 @@ version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "1d0b20fd2a03b45880974241e3443d9e324de637fefa4f43859efce70089812b" +[[package]] +name = "oliphaunt-extension-hstore-wasix-aot-aarch64-apple-darwin" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "004e128d02237a749af8e0219532f4af55b65de588709b0cf2bbef99e7fa6292" + +[[package]] +name = "oliphaunt-extension-hstore-wasix-aot-aarch64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "ae54c87147a7b4adba32fc6519a68937a8fb5155c4da28dcf36bd66b3e7e98ad" + +[[package]] +name = "oliphaunt-extension-hstore-wasix-aot-x86_64-pc-windows-msvc" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "98af804e5514ba341aa03e630320e135f7761b60104d4592743d68b324923fa9" + +[[package]] +name = "oliphaunt-extension-hstore-wasix-aot-x86_64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "b71adb2ca0f694aac91994c099572ae14906d333279e7bf91662431f86b8a06f" + [[package]] name = "oliphaunt-extension-pg-trgm-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "6ea075c13c8283d2eb26526c63061b116ffc515899fa59478a8a6c570539a312" +[[package]] +name = "oliphaunt-extension-pg-trgm-wasix-aot-aarch64-apple-darwin" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "0c5c91b06e0a5101433533753876dac7aee89936212967606175c9f141976a14" + +[[package]] +name = "oliphaunt-extension-pg-trgm-wasix-aot-aarch64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "c14ce6cbf988af1eb13f567b9a975f5bf566076688514133c093971f5a737aa6" + +[[package]] +name = "oliphaunt-extension-pg-trgm-wasix-aot-x86_64-pc-windows-msvc" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "d4e164a68f4047ac3c268ef71b9807d33242e06f61bf862bf60df9cb9a47b4ae" + +[[package]] +name = "oliphaunt-extension-pg-trgm-wasix-aot-x86_64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "96f7d7cd8ba652876f221b37e4f290a84d054e2c50625c243803224ce3e12b03" + [[package]] name = "oliphaunt-extension-unaccent-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "9ab06b4d61878a87b53afc7b047d09f5f2fd794528acb5e40d359e599b0fc956" +[[package]] +name = "oliphaunt-extension-unaccent-wasix-aot-aarch64-apple-darwin" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "37e5978c9d6e020c01336f58c8922ebaed2f4dfd6ae4568b5f91b5d416fc7cdb" + +[[package]] +name = "oliphaunt-extension-unaccent-wasix-aot-aarch64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "4ae9dd2c37edc58bf3dc34b88314e5f012221f74c96e9c538133ed162a12509e" + +[[package]] +name = "oliphaunt-extension-unaccent-wasix-aot-x86_64-pc-windows-msvc" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "f869c3c96abb7169927c921e92e44401f148e6de6138213ead88d1208462685d" + +[[package]] +name = "oliphaunt-extension-unaccent-wasix-aot-x86_64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "5c4389eaa071ac1e9bc837958ec1f5caf7f9d44a75a789b576a4938f3f0ec7cc" + [[package]] name = "oliphaunt-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "4e04c1110a51cbaa3df9f3db71e81edc4040c3cdd9ff8c8596d311d18c726645" +checksum = "0fe403cee7d4d080ba6795a93a99d14a43812202639eb7295410c0cd27d6a022" dependencies = [ "anyhow", "async-trait", diff --git a/examples/tauri-wasix/src-tauri/Cargo.toml b/examples/tauri-wasix/src-tauri/Cargo.toml index 6662b1a1..b2c3e64d 100644 --- a/examples/tauri-wasix/src-tauri/Cargo.toml +++ b/examples/tauri-wasix/src-tauri/Cargo.toml @@ -26,7 +26,7 @@ serde = { version = "1", features = ["derive"] } sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio-rustls", "postgres"] } tauri = { version = "2", features = [] } thiserror = "2" -tokio = { version = "1", features = ["sync"] } +tokio = { version = "1", features = ["rt-multi-thread", "sync"] } [target.'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))'.dependencies] liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu = { version = "=0.1.0", registry = "oliphaunt-local" } diff --git a/examples/tauri-wasix/src-tauri/src/lib.rs b/examples/tauri-wasix/src-tauri/src/lib.rs index deedbe90..ce95962e 100644 --- a/examples/tauri-wasix/src-tauri/src/lib.rs +++ b/examples/tauri-wasix/src-tauri/src/lib.rs @@ -133,7 +133,17 @@ impl From for CommandError { } } -async fn open_database(root: PathBuf) -> Result { +fn open_database(root: PathBuf) -> Result { + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .context("build WASIX example Tokio runtime")?; + let _runtime_context = runtime.enter(); + let server = start_database_server(root)?; + runtime.block_on(connect_database(server)) +} + +fn start_database_server(root: PathBuf) -> Result { let server = OliphauntServer::builder() .path(root) .extensions([ @@ -144,6 +154,10 @@ async fn open_database(root: PathBuf) -> Result { .start() .context("start oliphaunt-wasix server")?; validate_wasix_tools(&server)?; + Ok(server) +} + +async fn connect_database(server: OliphauntServer) -> Result { let pool = PgPoolOptions::new() .max_connections(1) .acquire_timeout(Duration::from_secs(30)) @@ -253,7 +267,7 @@ pub fn run() { tauri::Builder::default() .setup(|app| { let root = app.path().app_data_dir()?.join("oliphaunt-wasix-todos"); - let db = tauri::async_runtime::block_on(open_database(root))?; + let db = open_database(root)?; app.manage(TodoStore { inner: Mutex::new(db), }); @@ -268,3 +282,21 @@ pub fn run() { .run(tauri::generate_context!()) .expect("error while running tauri application"); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn startup_smoke_runs_split_wasix_tools() { + let root = std::env::temp_dir().join(format!( + "oliphaunt-example-tauri-wasix-smoke-{}", + std::process::id() + )); + let _ = std::fs::remove_dir_all(&root); + let db = open_database(root.clone()) + .expect("start oliphaunt-wasix example database and run pg_dump smoke"); + drop(db); + let _ = std::fs::remove_dir_all(root); + } +} diff --git a/examples/tauri/src-tauri/Cargo.lock b/examples/tauri/src-tauri/Cargo.lock index 97735068..70c64ac6 100644 --- a/examples/tauri/src-tauri/Cargo.lock +++ b/examples/tauri/src-tauri/Cargo.lock @@ -1730,43 +1730,43 @@ dependencies = [ name = "liboliphaunt-native-linux-x64-gnu-part-000" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "520041a055281a65b0e300ea4d6c8113a2bcd08f4c9ef95393342ffbf1232351" +checksum = "5610cfaffb481874bd2d56d10fce3ed07581d3b312619d0c664aacfe87d7b095" [[package]] name = "liboliphaunt-native-linux-x64-gnu-part-001" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "4f38eeb858943d8587fbf9dc4ad6d86f3b993eb4154c50135c2f22378285373e" +checksum = "627a1e5101e32dd4ad382d4c8939d558562eff92136aab0baed3c9bf5a4ee910" [[package]] name = "liboliphaunt-native-linux-x64-gnu-part-002" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "b6ba9d8dbd493f4ca293108a70344154d188073b287a51defd0ff4b6e59217de" +checksum = "de88e6326ad8b8ae559de1f827ea7adf56e2a3c29099b5b99daed7d53bf45746" [[package]] name = "liboliphaunt-native-linux-x64-gnu-part-003" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "74f1d81d6d570a5cf189b816e503dc2087d107675bb6137b388322bd8f35fd9e" +checksum = "85bf22215694ecbf17e8a8b2328b431ca27cf4848fa2b337751a5b3e92488f0a" [[package]] name = "liboliphaunt-native-linux-x64-gnu-part-004" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "043608eb604121ea3201a4a24825d95d4205808b7ff933bf94b5e02eae5842c4" +checksum = "fe14dd7b52188e80b9afdc53af2eed678ec5c577393b9e8b947a8d4a37a90b7b" [[package]] name = "liboliphaunt-native-linux-x64-gnu-part-005" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "bc5c16a1cd47f5f90bb94288d3c9ff6f201139a98a31571aa0479308d9884b6a" +checksum = "87b3c9cc20a00f3285582b9a6b265287f304b5a4368dd86e9f329607b783a5e1" [[package]] name = "liboliphaunt-native-linux-x64-gnu-part-006" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "6560450839c262fa76b36ab35e8eb4d84e1a736c49f77bf4f6bd57114eb5772a" +checksum = "a3fa2b24de388519f09f5f502b992b61ea80be2179a4b3d9bcc42eee223045ba" [[package]] name = "libredox" @@ -2203,7 +2203,7 @@ dependencies = [ name = "oliphaunt-extension-hstore-linux-x64-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "6a4ff122d6b692bcc1a0b7e3c20e88c4255f76deb9507c0c6300f67870839efd" +checksum = "b60b0280f8b9b38ef0f02a30b4bccc4a09869e8f4b8476277fc274a376ad0632" dependencies = [ "sha2", ] @@ -2212,7 +2212,7 @@ dependencies = [ name = "oliphaunt-extension-pg-trgm-linux-x64-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "1877c71f7a75afadc5cd5a34bc3b246a1b1603c24f06aa9a1c762145a6672596" +checksum = "1028e6777a424b90fa3cfb0139b3e0737db6059360df52976d06e086e80afae7" dependencies = [ "sha2", ] @@ -2221,7 +2221,7 @@ dependencies = [ name = "oliphaunt-extension-unaccent-linux-x64-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "9eabb41963dd6935ae1418179f0667b89a604eb30a636b781583157527f21901" +checksum = "cb599a9723f73ccf66e7e33a0c395e1ef449578c5d7f5338d18c3d62fec40bda" dependencies = [ "sha2", ] @@ -2240,7 +2240,7 @@ dependencies = [ name = "oliphaunt-tools-linux-x64-gnu-part-000" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "dba5682416ca2fb0ed7ea5d36cad304962f064898469211ef5c1b1063159f6b7" +checksum = "c069918c5c037a145fc0b0453f7f90ea06a26556344b3b096c3ab09f82864c03" [[package]] name = "once_cell" diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml b/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml index 1ec14a38..6ccb6a0f 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml @@ -65,14 +65,14 @@ icu = ["dep:oliphaunt-icu"] postgres-version = "18.4" postgres-source-url = "https://ftp.postgresql.org/pub/source/v18.4/postgresql-18.4.tar.bz2" postgres-source-sha256 = "81a81ec695fb0c7901407defaa1d2f7973617154cf27ba74e3a7ab8e64436094" -postgres-patch-count = "37" +postgres-patch-count = "38" oliphaunt-npm-version-checked = "0.4.5" -runtime-archive-sha256 = "810a238bbb430b24b9a606bcdf9c2346270d729530f24e5c61772fe69d070577" -oliphaunt-wasix-sha256 = "d6438a0dd57c13cd160d6f58de3c5549f5b94c8d99d834ebed63ade841716f72" -pgdata-template-archive-sha256 = "c525b376a9667fdc7b7beb74d902ab56da5b017a4571e5ab62cd1b1bb4c0d65a" -pg-dump-wasix-sha256 = "19579204268759917a3efafa81ae1de7f2e67c7e0f4de11ea8aa03f948bf15bd" -psql-wasix-sha256 = "0000000000000000000000000000000000000000000000000000000000000000" -initdb-wasix-sha256 = "91cfb13243c371d4937d4e6fca513aaa82a33dfde42be17f04ad64c4cb75e6e1" +runtime-archive-sha256 = "7dccedb08fdc32b0092ff92a0882d911230e0361d0f4fdf228d6a6cb7d981178" +oliphaunt-wasix-sha256 = "da58c392818149789b8ca9824952abf20ed1a084e7b580369a5478e6db280b05" +pgdata-template-archive-sha256 = "6155909517d8e5e8979a49fbd635d980474fccf7f5124e77316d213655f6235a" +pg-dump-wasix-sha256 = "6f3e92ba8a9faae2cf108a9d6e0f91e399e27d2f54c543297eaf5de63d511418" +psql-wasix-sha256 = "41c20c6c43ad437a732b0248efa173b5e0edcd2ab5bb4eee2752595201aa9db9" +initdb-wasix-sha256 = "8c2b936abfd01ba7d7272897a1719ce2a0e2bfaa4835bea3458f462afe74f8fc" [dependencies] anyhow = "1" diff --git a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock index 46e56bd3..a8685aaf 100644 --- a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock +++ b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock @@ -3528,7 +3528,7 @@ dependencies = [ name = "oliphaunt-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "4e04c1110a51cbaa3df9f3db71e81edc4040c3cdd9ff8c8596d311d18c726645" +checksum = "0fe403cee7d4d080ba6795a93a99d14a43812202639eb7295410c0cd27d6a022" dependencies = [ "anyhow", "async-trait", @@ -3607,7 +3607,7 @@ dependencies = [ name = "liboliphaunt-wasix-portable" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "67857a0fbca85a256e60c4ea9901958cad8fb28b7d1ee4033dbdbc0385ab9baa" +checksum = "74e4a84c8db15e4be7945d7b3a2ab1cb30a687b155367f32a25155891f604e77" dependencies = [ "serde", "serde_json", diff --git a/src/extensions/artifacts/packages/moon.yml b/src/extensions/artifacts/packages/moon.yml index a55aadfb..f089b89d 100644 --- a/src/extensions/artifacts/packages/moon.yml +++ b/src/extensions/artifacts/packages/moon.yml @@ -64,6 +64,7 @@ tasks: - "/tools/release/product_metadata.py" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" + - "/target/extensions/wasix/aot-artifacts/**/*" outputs: - "/target/extension-artifacts/**/*" options: diff --git a/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json b/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json index 323a99f3..144e6d0f 100644 --- a/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json +++ b/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json @@ -514,7 +514,7 @@ } ], "schema": "oliphaunt-extension-evidence-v1", - "sourceDigest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d", + "sourceDigest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1", "sourceDigestInputs": [ "src/postgres/versions/18/source.toml", "src/extensions/catalog/extensions.promoted.toml", diff --git a/src/extensions/generated/docs/extension-evidence.json b/src/extensions/generated/docs/extension-evidence.json index 8b46714d..3118ad62 100644 --- a/src/extensions/generated/docs/extension-evidence.json +++ b/src/extensions/generated/docs/extension-evidence.json @@ -20,7 +20,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -56,7 +56,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -92,7 +92,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -128,7 +128,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -164,7 +164,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -200,7 +200,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -236,7 +236,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -272,7 +272,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -308,7 +308,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -344,7 +344,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -380,7 +380,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -416,7 +416,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -452,7 +452,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -488,7 +488,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -524,7 +524,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -560,7 +560,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -596,7 +596,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -632,7 +632,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -668,7 +668,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -704,7 +704,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -740,7 +740,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -776,7 +776,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -812,7 +812,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -848,7 +848,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -884,7 +884,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -920,7 +920,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -956,7 +956,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -992,7 +992,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -1028,7 +1028,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -1064,7 +1064,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -1100,7 +1100,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -1136,7 +1136,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -1172,7 +1172,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -1208,7 +1208,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -1244,7 +1244,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -1280,7 +1280,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -1316,7 +1316,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -1352,7 +1352,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -1388,7 +1388,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d" + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" } ], "platform-targets": [ @@ -1420,7 +1420,7 @@ "path": "src/extensions/evidence/runs" } ], - "source-digest": "sha256:def2483b438de11d47ce64518e729c04e2055c5ed7262f6d36c222f3ce5f023d", + "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1", "source-digest-inputs": [ "src/postgres/versions/18/source.toml", "src/extensions/catalog/extensions.promoted.toml", diff --git a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 index 67038273..8c2f53f0 100644 --- a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 +++ b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 @@ -1 +1 @@ -d208dde15f9d8aec1a34249292342a72148664fd0093b3573082950440a936d5 +72c65d6de94b4529d2a8e852b10da2de355d86c7ba0ddb9379064b86c794bd84 diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py index 02b9efc8..a8792afa 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -909,10 +909,15 @@ def validate_ci_release_artifacts() -> None: "DEFAULT_PART_COUNT", "WASIX Cargo artifact packager must not generate reserved part crates", ) - reject_text( + require_text( + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.py", + "wasix_extension_aot_part_package_name", + "WASIX Cargo artifact packager may only generate named part crates for oversized extension AOT artifacts", + ) + require_text( "tools/release/package_liboliphaunt_wasix_cargo_artifacts.py", - "part_package_name", - "WASIX Cargo artifact packager must not generate part crate names", + "EXTENSION_AOT_SPLIT_THRESHOLD_BYTES", + "WASIX Cargo artifact packager must keep extension AOT part splitting behind an explicit size threshold", ) require_text( "tools/release/release.py", @@ -1114,11 +1119,21 @@ def validate_target_matrices() -> None: "tools/release/build-extension-ci-artifacts.py --all --require-native --require-wasix", "CI exact-extension package producer must use the shared product artifact builder", ) + require_text( + "src/extensions/artifacts/packages/moon.yml", + "/target/extensions/wasix/aot-artifacts/**/*", + "CI exact-extension package producer must consume WASIX extension AOT artifacts", + ) require_text( "src/runtimes/liboliphaunt/wasix/tools/build-runtime-portable.sh", "cargo run -p xtask -- assets check --strict-generated", "WASIX portable runtime build must validate generated extension/runtime assets", ) + require_text( + "src/runtimes/liboliphaunt/wasix/tools/build-aot-target.sh", + 'cargo run -p xtask -- assets package-extension-aot --target-triple "$target"', + "WASIX AOT target build must package extension AOT artifacts for extension Cargo crates", + ) require_text( "src/runtimes/liboliphaunt/wasix/tools/build-aot-target.sh", "cargo run -p xtask -- assets check-aot --target-triple \"$target\"", diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index e0018378..79a7bfa2 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -1588,9 +1588,10 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: "CRATES_IO_MAX_BYTES" in wasix_packager_source and "validate_crate_size" in wasix_packager_source and "DEFAULT_PART_COUNT" not in wasix_packager_source - and "part_package_name" not in wasix_packager_source + and "wasix_extension_aot_part_package_name" in wasix_packager_source + and "EXTENSION_AOT_SPLIT_THRESHOLD_BYTES" in wasix_packager_source and '"role": "artifact"' in wasix_packager_source, - "WASIX Cargo artifact packaging must publish direct public artifact crates and fail above the crates.io size limit instead of splitting into part crates.", + "WASIX Cargo artifact packaging must publish direct public artifact crates, enforce the crates.io size limit, and split only oversized internal extension AOT payloads.", "tools/release/package_liboliphaunt_wasix_cargo_artifacts.py", severity="P0", ) diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index ebcba453..3bea9022 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -149,6 +149,8 @@ def discover_roots(extra_roots: Iterable[Path]) -> list[Path]: ROOT / "target" / "sdk-artifacts", ROOT / "target" / "package" / "tmp-crate", ROOT / "target" / "package" / "tmp-registry", + ROOT / "target" / "local-registry-generated" / "broker-cargo", + ROOT / "target" / "oliphaunt-broker" / "cargo-artifacts", ROOT / "target" / "oliphaunt-wasix" / "cargo-artifacts", ROOT / "target" / "oliphaunt-wasix" / "release-assets", ROOT / "target" / "extension-artifacts", @@ -1291,7 +1293,12 @@ def stage_cargo_source_crates( ) available_package_names = cargo_package_names_from_roots(roots) native_source_root = ROOT / "target/liboliphaunt/cargo-package-sources" - for manifest in native_runtime_artifact_manifests(native_source_root): + native_runtime_public_manifests = native_runtime_artifact_manifests(native_source_root) + native_runtime_all_manifests = native_runtime_artifact_manifests( + native_source_root, + include_parts=True, + ) + for manifest in native_runtime_public_manifests: name, _version = read_cargo_package_name_version(manifest) available_package_names.add(name) prune_missing_local_artifact_target_dependencies( @@ -1304,14 +1311,14 @@ def stage_cargo_source_crates( wasix_manifest = ROOT / "src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml" generated.append(manual_cargo_package_source(wasix_manifest, output_dir)) - for manifest in native_runtime_artifact_manifests(native_source_root): + for manifest in native_runtime_all_manifests: generated.append(manual_cargo_package_source(manifest, output_dir)) result.staged.extend(rel(path) for path in generated) return generated -def native_runtime_artifact_manifests(source_root: Path) -> list[Path]: +def native_runtime_artifact_manifests(source_root: Path, *, include_parts: bool = False) -> list[Path]: if not source_root.is_dir(): return [] manifests = [ @@ -1325,7 +1332,7 @@ def native_runtime_artifact_manifests(source_root: Path) -> list[Path]: continue seen.add(manifest) name, _version = read_cargo_package_name_version(manifest) - if "-part-" in name: + if "-part-" in name and not include_parts: continue result.append(manifest) return result @@ -2088,7 +2095,9 @@ def cargo_metadata_for_crate(crate_path: Path) -> dict[str, Any]: def cargo_index_dependency(dep: dict[str, Any], local_package_names: set[str]) -> dict[str, Any]: registry = dep.get("registry") - if registry is None and dep["name"] not in local_package_names: + if dep["name"] in local_package_names: + registry = None + elif registry is None: registry = CRATES_IO_INDEX return { "name": dep["name"], diff --git a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py index 5b2eeccd..ce0155f8 100644 --- a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py +++ b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py @@ -23,6 +23,7 @@ PRODUCT = "liboliphaunt-wasix" SCHEMA = "oliphaunt-liboliphaunt-wasix-cargo-artifacts-v2" CRATES_IO_MAX_BYTES = 10 * 1024 * 1024 +EXTENSION_AOT_SPLIT_THRESHOLD_BYTES = 9 * 1024 * 1024 RUNTIME_PACKAGE = "liboliphaunt-wasix-portable" TOOLS_PACKAGE = "oliphaunt-wasix-tools" ICU_PACKAGE = "oliphaunt-icu" @@ -60,6 +61,7 @@ "x86_64-unknown-linux-gnu": 'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))', "x86_64-pc-windows-msvc": 'cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))', } +EXPECTED_EXTENSION_AOT_TARGETS = frozenset(AOT_TARGET_TRIPLES.values()) @dataclass(frozen=True) @@ -92,6 +94,7 @@ class ExtensionCargoSpec: archive: Path sha256: str size: int + requires_aot: bool aot_targets: tuple["ExtensionAotCargoSpec", ...] @@ -114,6 +117,16 @@ class ExtensionCargoSource: class ExtensionAotCargoSource: spec: ExtensionAotCargoSpec source_dir: Path + part_sources: tuple["ExtensionAotPartCargoSource", ...] = () + + +@dataclass(frozen=True) +class ExtensionAotPartCargoSource: + name: str + version: str + sql_name: str + target: str + source_dir: Path def fail(message: str) -> NoReturn: @@ -749,6 +762,14 @@ def wasix_extension_aot_package_name(product: str, target: str) -> str: return f"{product}-wasix-aot-{target}" +def wasix_extension_aot_part_package_name(package_name: str, index: int) -> str: + return f"{package_name}-part-{index:03d}" + + +def rust_crate_ident(package_name: str) -> str: + return package_name.replace("-", "_") + + def discover_extension_manifests(roots: list[Path]) -> list[Path]: manifests: list[Path] = [] for root in roots: @@ -829,6 +850,7 @@ def extension_cargo_specs(extension_roots: list[Path]) -> list[ExtensionCargoSpe product = manifest.get("product") version = manifest.get("version") sql_name = manifest.get("sqlName") + native_module_stem = manifest.get("nativeModuleStem") if not all(isinstance(value, str) and value for value in [product, version, sql_name]): fail(f"{rel(manifest_path)} is missing product, version, or sqlName") archive = extension_wasix_asset(manifest_path.parent, manifest) @@ -843,6 +865,7 @@ def extension_cargo_specs(extension_roots: list[Path]) -> list[ExtensionCargoSpe archive=archive, sha256=sha256_file(archive), size=archive.stat().st_size, + requires_aot=isinstance(native_module_stem, str) and bool(native_module_stem), aot_targets=extension_aot_specs( manifest_path.parent, product=str(product), @@ -854,6 +877,18 @@ def extension_cargo_specs(extension_roots: list[Path]) -> list[ExtensionCargoSpe return sorted(specs, key=lambda spec: spec.name) +def validate_extension_aot_coverage(extension_specs: list[ExtensionCargoSpec]) -> None: + for spec in extension_specs: + if not spec.requires_aot: + continue + actual_targets = {aot_spec.target for aot_spec in spec.aot_targets} + if actual_targets != EXPECTED_EXTENSION_AOT_TARGETS: + fail( + f"{spec.product} has a WASIX native module but incomplete extension AOT artifacts; " + f"expected={sorted(EXPECTED_EXTENSION_AOT_TARGETS)}, actual={sorted(actual_targets)}" + ) + + def write_extension_cargo_source(spec: ExtensionCargoSpec, source_root: Path) -> ExtensionCargoSource: crate_dir = source_root / spec.name if crate_dir.exists(): @@ -923,12 +958,100 @@ def write_extension_aot_cargo_source( if crate_dir.exists(): fail(f"duplicate generated WASIX extension AOT Cargo package source: {rel(crate_dir)}") (crate_dir / "src").mkdir(parents=True, exist_ok=True) - shutil.copytree(spec.source_dir, crate_dir / "artifacts") - manifest = json.loads((crate_dir / "artifacts/manifest.json").read_text(encoding="utf-8")) - artifact_cases = [] + manifest_path = spec.source_dir / "manifest.json" + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + artifacts: list[tuple[str, str, Path, int]] = [] for artifact in sorted(manifest.get("artifacts", []), key=lambda item: item.get("name", "")): - name = artifact["name"] - path = artifact["path"] + name = artifact.get("name") + path = artifact.get("path") + if not isinstance(name, str) or not isinstance(path, str): + fail(f"{rel(manifest_path)} contains an AOT artifact without name/path") + source = spec.source_dir / path + if not source.is_file(): + fail(f"{rel(manifest_path)} references missing AOT artifact {path}") + artifacts.append((name, path, source, source.stat().st_size)) + if not artifacts: + fail(f"{rel(manifest_path)} must contain extension AOT artifacts") + + split_parts = sum(size for _, _, _, size in artifacts) > EXTENSION_AOT_SPLIT_THRESHOLD_BYTES + part_sources: list[ExtensionAotPartCargoSource] = [] + + if split_parts: + (crate_dir / "artifacts").mkdir(parents=True, exist_ok=True) + shutil.copy2(manifest_path, crate_dir / "artifacts/manifest.json") + for index, (name, path, source, _) in enumerate(artifacts): + part_name = wasix_extension_aot_part_package_name(spec.name, index) + part_dir = source_root / part_name + if part_dir.exists(): + fail(f"duplicate generated WASIX extension AOT Cargo package source: {rel(part_dir)}") + (part_dir / "src").mkdir(parents=True, exist_ok=True) + destination = part_dir / "artifacts" / path + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, destination) + part_dir.joinpath("README.md").write_text( + "\n".join( + [ + f"# {part_name}", + "", + f"Cargo artifact package part for `{spec.sql_name}` Oliphaunt WASIX AOT artifacts on `{spec.target}`.", + "", + ] + ), + encoding="utf-8", + ) + part_dir.joinpath("Cargo.toml").write_text( + "\n".join( + [ + "[package]", + f'name = "{part_name}"', + f'version = "{spec.version}"', + 'edition = "2024"', + 'rust-version = "1.93"', + f'description = "Oliphaunt WASIX AOT artifact package part for the {spec.sql_name} PostgreSQL extension on {spec.target}"', + 'repository = "https://github.com/f0rr0/oliphaunt"', + 'homepage = "https://oliphaunt.dev"', + 'license = "MIT AND Apache-2.0 AND PostgreSQL"', + 'include = ["Cargo.toml", "README.md", "src/**", "artifacts/**"]', + "", + "[lib]", + 'path = "src/lib.rs"', + "", + "[workspace]", + "", + ] + ), + encoding="utf-8", + ) + part_dir.joinpath("src/lib.rs").write_text( + "".join( + [ + "#![deny(unsafe_code)]\n\n", + f'pub const SQL_NAME: &str = "{spec.sql_name}";\n', + f'pub const TARGET_TRIPLE: &str = "{spec.target}";\n\n', + "pub fn aot_artifact_bytes(name: &str) -> Option<&'static [u8]> {\n", + " match name {\n", + f' {json.dumps(name)} => Some(include_bytes!("../artifacts/{path}")),\n', + " _ => None,\n", + " }\n", + "}\n", + ] + ), + encoding="utf-8", + ) + part_sources.append( + ExtensionAotPartCargoSource( + name=part_name, + version=spec.version, + sql_name=spec.sql_name, + target=spec.target, + source_dir=part_dir, + ) + ) + else: + shutil.copytree(spec.source_dir, crate_dir / "artifacts") + + artifact_cases = [] + for name, path, _, _ in artifacts: artifact_cases.append( f' {json.dumps(name)} => Some(include_bytes!("../artifacts/{path}")),\n' ) @@ -960,12 +1083,44 @@ def write_extension_aot_cargo_source( "[lib]", 'path = "src/lib.rs"', "", + *( + [ + "[dependencies]", + *[ + f'{part.name} = {{ version = "={part.version}", path = "../{part.name}" }}' + for part in part_sources + ], + "", + ] + if part_sources + else [] + ), "[workspace]", "", ] ), encoding="utf-8", ) + if part_sources: + artifact_bytes_lines: list[str] = [] + for part in part_sources: + artifact_bytes_lines.extend( + [ + f" if let Some(bytes) = {rust_crate_ident(part.name)}::aot_artifact_bytes(name) {{\n", + " return Some(bytes);\n", + " }\n", + ] + ) + artifact_bytes_body = "".join(artifact_bytes_lines) + else: + artifact_bytes_body = "".join( + [ + " match name {\n", + *artifact_cases, + " _ => None,\n", + " }\n", + ] + ) crate_dir.joinpath("src/lib.rs").write_text( "".join( [ @@ -977,16 +1132,14 @@ def write_extension_aot_cargo_source( " Some(MANIFEST_JSON)\n", "}\n\n", "pub fn aot_artifact_bytes(name: &str) -> Option<&'static [u8]> {\n", - " match name {\n", - *artifact_cases, - " _ => None,\n", - " }\n", + artifact_bytes_body, + " None\n" if part_sources else "", "}\n", ] ), encoding="utf-8", ) - return ExtensionAotCargoSource(spec=spec, source_dir=crate_dir) + return ExtensionAotCargoSource(spec=spec, source_dir=crate_dir, part_sources=tuple(part_sources)) def package_extension_source( @@ -1015,20 +1168,43 @@ def package_extension_aot_source( *, output_dir: Path, cargo_target_dir: Path, -) -> GeneratedPackage: - crate_path = cargo_package(source.source_dir, cargo_target_dir) +) -> list[GeneratedPackage]: + packages: list[GeneratedPackage] = [] + for part in source.part_sources: + crate_path = cargo_package(part.source_dir, cargo_target_dir) + validate_crate_size(crate_path) + output = output_dir / crate_path.name + shutil.copy2(crate_path, output) + packages.append( + GeneratedPackage( + name=part.name, + manifest_path=part.source_dir / "Cargo.toml", + crate_path=output, + target=part.target, + kind="wasix-extension-aot", + size=output.stat().st_size, + sha256=sha256_file(output), + ) + ) + if source.part_sources: + crate_path = cargo_package_without_dependency_resolution(source.source_dir, cargo_target_dir) + else: + crate_path = cargo_package(source.source_dir, cargo_target_dir) validate_crate_size(crate_path) output = output_dir / crate_path.name shutil.copy2(crate_path, output) - return GeneratedPackage( - name=source.spec.name, - manifest_path=source.source_dir / "Cargo.toml", - crate_path=output, - target=source.spec.target, - kind="wasix-extension-aot", - size=output.stat().st_size, - sha256=sha256_file(output), + packages.append( + GeneratedPackage( + name=source.spec.name, + manifest_path=source.source_dir / "Cargo.toml", + crate_path=output, + target=source.spec.target, + kind="wasix-extension-aot", + size=output.stat().st_size, + sha256=sha256_file(output), + ) ) + return packages def package_specs(asset_dir: Path, extract_root: Path, version: str) -> list[PackageSpec]: @@ -1186,6 +1362,7 @@ def main(argv: list[str]) -> int: output_dir.mkdir(parents=True, exist_ok=True) extension_specs = extension_cargo_specs(extension_roots) + validate_extension_aot_coverage(extension_specs) extension_sources = [ write_extension_cargo_source(spec, source_root) for spec in extension_specs @@ -1206,24 +1383,25 @@ def main(argv: list[str]) -> int: for source in extension_sources ], *[ - package_extension_aot_source( + package + for source in extension_aot_sources + for package in package_extension_aot_source( source, output_dir=output_dir, cargo_target_dir=cargo_target_dir, ) - for source in extension_aot_sources ], *[ - package_spec( - spec, - version=args.version, - source_root=source_root, - output_dir=output_dir, - cargo_target_dir=cargo_target_dir, - extension_sources=extension_sources, - extension_aot_sources=extension_aot_sources, - ) - for spec in specs + package_spec( + spec, + version=args.version, + source_root=source_root, + output_dir=output_dir, + cargo_target_dir=cargo_target_dir, + extension_sources=extension_sources, + extension_aot_sources=extension_aot_sources, + ) + for spec in specs ], ] write_packages_manifest(packages, output_dir) diff --git a/tools/release/sync_release_pr.py b/tools/release/sync_release_pr.py index d392b166..dc550b42 100755 --- a/tools/release/sync_release_pr.py +++ b/tools/release/sync_release_pr.py @@ -29,6 +29,10 @@ "@oliphaunt/liboliphaunt-linux-arm64-gnu", "@oliphaunt/liboliphaunt-linux-x64-gnu", "@oliphaunt/liboliphaunt-win32-x64-msvc", + "@oliphaunt/tools-darwin-arm64", + "@oliphaunt/tools-linux-arm64-gnu", + "@oliphaunt/tools-linux-x64-gnu", + "@oliphaunt/tools-win32-x64-msvc", ], "oliphaunt-node-direct": [ "@oliphaunt/node-direct-darwin-arm64", @@ -58,7 +62,7 @@ VERSION_LINE_RE = re.compile(r'^(\s*version\s*=\s*)"[^"]*"(\s*(?:#.*)?)$') TOML_TABLE_RE = re.compile(r"^\s*\[([A-Za-z0-9_.-]+)\]\s*(?:#.*)?$") PNPM_TYPESCRIPT_OPTIONAL_RUNTIME_KEY_RE = re.compile( - r"^(\s*)'(@oliphaunt/(?:broker|liboliphaunt|node-direct)-[^']+)':\s*$" + r"^(\s*)'(@oliphaunt/(?:broker|liboliphaunt|node-direct|tools)-[^']+)':\s*$" ) PNPM_SPECIFIER_RE = re.compile(r"^(\s*specifier:\s*)(\S+)(\s*)$") ASSET_INPUT_FINGERPRINT_PATH = ROOT / "src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256" From 3bf731e506d2898c10b40fe477ab747d8728fd0f Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Thu, 25 Jun 2026 22:27:08 +0000 Subject: [PATCH 015/137] fix: split wasix tools behind feature --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 56 ++++ examples/README.md | 6 +- examples/electron-wasix/src-wasix/Cargo.lock | 2 +- examples/electron-wasix/src-wasix/Cargo.toml | 1 + examples/electron-wasix/src-wasix/src/main.rs | 7 +- examples/tauri-wasix/src-tauri/Cargo.lock | 2 +- examples/tauri-wasix/src-tauri/Cargo.toml | 1 + examples/tauri-wasix/src-tauri/src/lib.rs | 7 +- examples/tools/check-examples.sh | 3 + .../crates/oliphaunt-wasix/Cargo.toml | 19 +- .../crates/oliphaunt-wasix/README.md | 5 +- .../src/bin/oliphaunt_wasix_dump.rs | 18 +- .../crates/oliphaunt-wasix/src/lib.rs | 4 +- .../oliphaunt-wasix/src/oliphaunt/aot.rs | 83 +++-- .../oliphaunt-wasix/src/oliphaunt/assets.rs | 4 +- .../oliphaunt-wasix/src/oliphaunt/backend.rs | 4 +- .../oliphaunt-wasix/src/oliphaunt/client.rs | 30 +- .../src/oliphaunt/extensions.rs | 9 +- .../oliphaunt-wasix/src/oliphaunt/mod.rs | 6 +- .../oliphaunt-wasix/src/oliphaunt/pg_dump.rs | 301 +++++++++++++++++- .../src/oliphaunt/postgres_mod.rs | 2 +- .../oliphaunt-wasix/src/oliphaunt/server.rs | 23 +- .../tauri-sqlx-vanilla/src-tauri/Cargo.lock | 2 +- .../tauri-sqlx-vanilla/src-tauri/Cargo.toml | 5 +- .../tauri-sqlx-vanilla/src-tauri/src/bench.rs | 8 +- .../wasix/crates/tools/Cargo.toml | 4 + .../liboliphaunt/wasix/crates/tools/README.md | 4 +- tools/release/check_consumer_shape.py | 35 +- tools/release/check_release_metadata.py | 30 +- tools/xtask/src/asset_checks.rs | 70 ++-- tools/xtask/src/asset_pipeline.rs | 10 +- 31 files changed, 645 insertions(+), 116 deletions(-) create mode 100644 docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md new file mode 100644 index 00000000..d3066623 --- /dev/null +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -0,0 +1,56 @@ +# Example and Release Validation Tasks + +This document tracks the broader validation work for examples, local registry +installs, package production, SDK parity, dead-code cleanup, and script tooling. +Keep the list ordered by dependency: prove the install/runtime shape first, then +review production pipelines, then normalize implementation details. + +## Priority 0: Current Acceptance Gates + +- [x] Confirm generated Cargo crates stay under the crates.io 10 MiB limit. +- [x] Confirm WASIX example smoke tests install `oliphaunt-wasix-tools` from the local registry and exercise the split tools path with `pg_dump`. +- [x] Confirm native and WASIX examples resolve local published runtime, tools, and extension crates with locked installs. +- [x] Add direct `psql` execution coverage when the WASIX SDK exposes a public tool runner for it. +- [ ] Run GUI-level e2e for Electron and Tauri examples, or document the exact missing host capabilities if a full GUI run is blocked. +- [ ] Verify CI and release workflows produce exactly the package surfaces expected for each registry. + +## Priority 1: Example App Validation + +- [ ] Inventory every example app, its package managers, local-registry dependencies, and runtime/tool/extension paths. +- [ ] Ensure each native example uses `oliphaunt-tools-*` from the local registry when it exercises standalone tools. +- [x] Ensure each WASIX example uses `oliphaunt-wasix-tools` from the local registry and does not rely on path-only tool assets. +- [ ] Add example-app smoke commands that model the desired developer experience and can run on Linux CI. +- [ ] Check frontend build/test flows for the Electron, Electron WASIX, Tauri, Tauri WASIX, and WASIX vanilla examples. + +## Priority 2: CI and Release Shape + +- [ ] Map CI producer jobs to release package consumers for Cargo, npm, Maven, SwiftPM, and GitHub release assets. +- [ ] Verify package naming is symmetric across native and WASIX, with `wasix` special-cased rather than `native`. +- [x] Verify native runtime payloads contain `postgres`, `initdb`, `pg_ctl`; native tools payloads contain `pg_dump`, `psql`. +- [x] Verify WASIX runtime payloads contain `postgres`, `initdb`; WASIX tools payloads contain `pg_dump`, `psql`, not `pg_ctl`. +- [ ] Verify extension packages and runtime tools are published and installed from registries idiomatically. +- [ ] Identify duplicated release metadata or package target matrices that can be safely collapsed. + +## Priority 3: SDK Consistency + +- [ ] Compare SDK install paths and artifact resolution across Rust, JS, React Native, Kotlin, and Swift. +- [ ] Ensure SDKs exercise the same control flows for runtime setup, extension selection, artifact validation, and tool access. +- [ ] Identify feature gaps where one SDK exposes a runtime/tool/extension capability differently from the others. +- [ ] Add or update parity checks where a documented invariant is not machine-checked. + +## Priority 4: Cleanup and Tooling + +- [ ] Run targeted dead-code detection for Rust, TypeScript/JavaScript, shell, and release scripts. +- [ ] Remove confirmed dead code only after proving no CI/release/example path still references it. +- [ ] Inventory Python and Rust helper scripts and decide which should move to Bun. +- [ ] Convert non-critical scripts to Bun incrementally, preserving current CI behavior after each conversion. +- [ ] Keep Rust tools where compilation is idiomatic or the code is part of the Rust product/toolchain surface. +- [ ] Validate Linux CI lanes locally after script conversions. +- [ ] Validate local release dry-run lanes with local registry publishing after script conversions. + +## Current Notes + +- The latest pushed commit is `cb9845d fix: package runtime tools separately`. +- Local-registry WASIX smoke coverage proves `pg_dump` through the SDK `dump_sql` path and `psql` through `PsqlOptions::command("SELECT 1")`. +- Local-registry Cargo payload inspection confirmed `liboliphaunt-native-linux-x64-gnu-part-*` contains `initdb`, `pg_ctl`, and `postgres` only under `runtime/bin`, while `oliphaunt-tools-linux-x64-gnu-part-*` contains only `pg_dump` and `psql` there. +- Full GUI e2e likely needs a headless display/browser harness. Prefer an existing project-native test command if one exists; otherwise evaluate Playwright/WebdriverIO/Tauri-driver style tooling before adding dependencies. diff --git a/examples/README.md b/examples/README.md index 808df27f..3633e975 100644 --- a/examples/README.md +++ b/examples/README.md @@ -11,7 +11,8 @@ Each app opts into `hstore`, `pg_trgm`, and `unaccent`, then uses `hstore` tags plus trigram/accent-insensitive search for the todo list. Native examples load `postgres`, `initdb`, and `pg_ctl` from `liboliphaunt-native-*`, while `pg_dump` and `psql` come from `oliphaunt-tools-*`. WASIX examples load -`postgres` and `initdb` from the runtime crates and `pg_dump`/`psql` from +`postgres` and `initdb` from the runtime crates. WASIX examples enable the +`oliphaunt-wasix` `tools` feature, which resolves `pg_dump`/`psql` from `oliphaunt-wasix-tools`; WASIX intentionally has no `pg_ctl`. Local registry artifacts for Linux x64 from CI run `28049923289` can be @@ -52,7 +53,8 @@ examples/tools/with-local-registries.sh pnpm --dir examples/electron start ``` The native examples run a SQL backup smoke through `pg_dump` during startup. -The WASIX examples run `dump_sql("--schema-only")` during startup. +The WASIX examples run `dump_sql("--schema-only")` and a non-interactive `psql` +`SELECT 1` smoke during startup. On Linux, SwiftPM artifacts are staged for inspection and skipped for registry publish when `swift` is not installed. diff --git a/examples/electron-wasix/src-wasix/Cargo.lock b/examples/electron-wasix/src-wasix/Cargo.lock index 06e2143e..fbd49591 100644 --- a/examples/electron-wasix/src-wasix/Cargo.lock +++ b/examples/electron-wasix/src-wasix/Cargo.lock @@ -2021,7 +2021,7 @@ checksum = "5c4389eaa071ac1e9bc837958ec1f5caf7f9d44a75a789b576a4938f3f0ec7cc" name = "oliphaunt-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "0fe403cee7d4d080ba6795a93a99d14a43812202639eb7295410c0cd27d6a022" +checksum = "987e82c9952421633cc7d31e3ec3615856ff3833e503cac02f5b88930e7d23fc" dependencies = [ "anyhow", "async-trait", diff --git a/examples/electron-wasix/src-wasix/Cargo.toml b/examples/electron-wasix/src-wasix/Cargo.toml index 8a6e2090..6ddb12db 100644 --- a/examples/electron-wasix/src-wasix/Cargo.toml +++ b/examples/electron-wasix/src-wasix/Cargo.toml @@ -9,6 +9,7 @@ publish = false [dependencies] anyhow = "1" oliphaunt-wasix = { version = "=0.1.0", registry = "oliphaunt-local", features = [ + "tools", "extension-hstore", "extension-pg-trgm", "extension-unaccent", diff --git a/examples/electron-wasix/src-wasix/src/main.rs b/examples/electron-wasix/src-wasix/src/main.rs index 4140f436..61138298 100644 --- a/examples/electron-wasix/src-wasix/src/main.rs +++ b/examples/electron-wasix/src-wasix/src/main.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use std::thread; use anyhow::{bail, Context, Result}; -use oliphaunt_wasix::{extensions, OliphauntServer, PgDumpOptions}; +use oliphaunt_wasix::{extensions, OliphauntServer, PgDumpOptions, PsqlOptions}; use serde_json::json; fn main() -> Result<()> { @@ -43,6 +43,11 @@ fn validate_wasix_tools(server: &OliphauntServer) -> Result<()> { dump.contains("PostgreSQL database dump"), "pg_dump SQL backup smoke did not look like a PostgreSQL dump" ); + let psql = server.psql(PsqlOptions::new().arg("-tA").command("SELECT 1"))?; + anyhow::ensure!( + psql.lines().any(|line| line.trim() == "1"), + "psql smoke did not return SELECT 1 output" + ); Ok(()) } diff --git a/examples/tauri-wasix/src-tauri/Cargo.lock b/examples/tauri-wasix/src-tauri/Cargo.lock index 69135012..3cdd28fd 100644 --- a/examples/tauri-wasix/src-tauri/Cargo.lock +++ b/examples/tauri-wasix/src-tauri/Cargo.lock @@ -3494,7 +3494,7 @@ checksum = "5c4389eaa071ac1e9bc837958ec1f5caf7f9d44a75a789b576a4938f3f0ec7cc" name = "oliphaunt-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "0fe403cee7d4d080ba6795a93a99d14a43812202639eb7295410c0cd27d6a022" +checksum = "987e82c9952421633cc7d31e3ec3615856ff3833e503cac02f5b88930e7d23fc" dependencies = [ "anyhow", "async-trait", diff --git a/examples/tauri-wasix/src-tauri/Cargo.toml b/examples/tauri-wasix/src-tauri/Cargo.toml index b2c3e64d..37fbb046 100644 --- a/examples/tauri-wasix/src-tauri/Cargo.toml +++ b/examples/tauri-wasix/src-tauri/Cargo.toml @@ -17,6 +17,7 @@ tauri-build = { version = "2", features = [] } [dependencies] anyhow = "1" oliphaunt-wasix = { version = "=0.1.0", registry = "oliphaunt-local", features = [ + "tools", "extension-hstore", "extension-pg-trgm", "extension-unaccent", diff --git a/examples/tauri-wasix/src-tauri/src/lib.rs b/examples/tauri-wasix/src-tauri/src/lib.rs index ce95962e..e9b75576 100644 --- a/examples/tauri-wasix/src-tauri/src/lib.rs +++ b/examples/tauri-wasix/src-tauri/src/lib.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use std::time::Duration; use anyhow::{Context, Result}; -use oliphaunt_wasix::{OliphauntServer, PgDumpOptions, extensions}; +use oliphaunt_wasix::{extensions, OliphauntServer, PgDumpOptions, PsqlOptions}; use serde::ser::Serializer; use serde::{Deserialize, Serialize}; use sqlx::postgres::PgPoolOptions; @@ -186,6 +186,11 @@ fn validate_wasix_tools(server: &OliphauntServer) -> Result<()> { dump.contains("PostgreSQL database dump"), "pg_dump SQL backup smoke did not look like a PostgreSQL dump" ); + let psql = server.psql(PsqlOptions::new().arg("-tA").command("SELECT 1"))?; + anyhow::ensure!( + psql.lines().any(|line| line.trim() == "1"), + "psql smoke did not return SELECT 1 output" + ); Ok(()) } diff --git a/examples/tools/check-examples.sh b/examples/tools/check-examples.sh index 856740c2..2c87eb8d 100755 --- a/examples/tools/check-examples.sh +++ b/examples/tools/check-examples.sh @@ -96,14 +96,17 @@ require_text "examples/tauri/src-tauri/Cargo.toml" 'oliphaunt-extension-hstore-l require_text "examples/tauri/src-tauri/Cargo.toml" 'oliphaunt-extension-pg-trgm-linux-x64-gnu' require_text "examples/tauri/src-tauri/Cargo.toml" 'oliphaunt-extension-unaccent-linux-x64-gnu' require_text "examples/tauri-wasix/src-tauri/Cargo.toml" 'registry = "oliphaunt-local"' +require_text "examples/tauri-wasix/src-tauri/Cargo.toml" '"tools"' require_text "examples/tauri-wasix/src-tauri/Cargo.toml" 'oliphaunt-wasix-tools' require_text "examples/tauri-wasix/src-tauri/Cargo.toml" 'liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu' require_text "examples/tauri-wasix/src-tauri/Cargo.toml" 'oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu' require_text "examples/electron-wasix/src-wasix/Cargo.toml" 'registry = "oliphaunt-local"' +require_text "examples/electron-wasix/src-wasix/Cargo.toml" '"tools"' require_text "examples/electron-wasix/src-wasix/Cargo.toml" 'oliphaunt-wasix-tools' require_text "examples/electron-wasix/src-wasix/Cargo.toml" 'liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu' require_text "examples/electron-wasix/src-wasix/Cargo.toml" 'oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu' require_text "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml" 'registry = "oliphaunt-local"' +require_text "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml" '"tools"' require_text "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml" 'oliphaunt-wasix-tools' require_text "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml" 'oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu' reject_text "examples/electron/package.json" '"@oliphaunt/ts": "workspace:\*"' diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml b/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml index 6ccb6a0f..4c5a92b9 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml @@ -20,6 +20,13 @@ exclude = [ [features] default = [] extensions = [] +tools = [ + "dep:oliphaunt-wasix-tools", + "dep:oliphaunt-wasix-tools-aot-aarch64-apple-darwin", + "dep:oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu", + "dep:oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc", + "dep:oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu", +] extension-amcheck = ["extensions", "liboliphaunt-wasix-portable/extension-amcheck"] extension-auto-explain = ["extensions", "liboliphaunt-wasix-portable/extension-auto-explain"] extension-bloom = ["extensions", "liboliphaunt-wasix-portable/extension-bloom"] @@ -70,8 +77,6 @@ oliphaunt-npm-version-checked = "0.4.5" runtime-archive-sha256 = "7dccedb08fdc32b0092ff92a0882d911230e0361d0f4fdf228d6a6cb7d981178" oliphaunt-wasix-sha256 = "da58c392818149789b8ca9824952abf20ed1a084e7b580369a5478e6db280b05" pgdata-template-archive-sha256 = "6155909517d8e5e8979a49fbd635d980474fccf7f5124e77316d213655f6235a" -pg-dump-wasix-sha256 = "6f3e92ba8a9faae2cf108a9d6e0f91e399e27d2f54c543297eaf5de63d511418" -psql-wasix-sha256 = "41c20c6c43ad437a732b0248efa173b5e0edcd2ab5bb4eee2752595201aa9db9" initdb-wasix-sha256 = "8c2b936abfd01ba7d7272897a1719ce2a0e2bfaa4835bea3458f462afe74f8fc" [dependencies] @@ -91,7 +96,7 @@ sha2 = "0.10" dunce = "1" filetime = "0.2" liboliphaunt-wasix-portable = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/assets" } -oliphaunt-wasix-tools = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/tools" } +oliphaunt-wasix-tools = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/tools", optional = true } oliphaunt-icu = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/icu", optional = true } tokio = { version = "1", features = ["io-util", "rt-multi-thread"] } wasmer = { version = "7.2.0-alpha.3", default-features = false, features = [ @@ -112,19 +117,19 @@ webc = "=12.0.0" [target.'cfg(all(target_os = "macos", target_arch = "aarch64"))'.dependencies] liboliphaunt-wasix-aot-aarch64-apple-darwin = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin" } -oliphaunt-wasix-tools-aot-aarch64-apple-darwin = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin" } +oliphaunt-wasix-tools-aot-aarch64-apple-darwin = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin", optional = true } [target.'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))'.dependencies] liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu" } -oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu" } +oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu", optional = true } [target.'cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))'.dependencies] liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu" } -oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu" } +oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu", optional = true } [target.'cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))'.dependencies] liboliphaunt-wasix-aot-x86_64-pc-windows-msvc = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc" } -oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc" } +oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc", optional = true } [dev-dependencies] sqlx = { version = "0.8", default-features = false, features = [ diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/README.md b/src/bindings/wasix-rust/crates/oliphaunt-wasix/README.md index 287bc026..f6aee5e8 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/README.md +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/README.md @@ -80,8 +80,9 @@ Postgres should be as easy to add to a Rust project as SQLite. - 💾 **Persistent apps**: keep local app data across restarts when you want it. - 🧩 **Extensions available**: install exact extension release assets owned by your application. -- 📦 **Portable dumps**: use the WASIX `pg_dump` asset from the matching runtime - release for logical backups and upgrade paths. +- 📦 **Portable tools**: enable the `tools` feature to resolve the matching + `oliphaunt-wasix-tools` `pg_dump` and `psql` artifacts for logical backups, + checks, and upgrade paths. - 🚀 **Near-native feel**: close to native Postgres, fully embedded. ## Near-Native Performance 🚀 diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/bin/oliphaunt_wasix_dump.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/bin/oliphaunt_wasix_dump.rs index 27095c3f..29aa3698 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/bin/oliphaunt_wasix_dump.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/bin/oliphaunt_wasix_dump.rs @@ -1,12 +1,12 @@ use anyhow::Result; -#[cfg(feature = "extensions")] +#[cfg(feature = "tools")] use oliphaunt_wasix::{OliphauntServer, PgDumpOptions}; -#[cfg(feature = "extensions")] +#[cfg(feature = "tools")] use std::env; -#[cfg(feature = "extensions")] +#[cfg(feature = "tools")] use std::path::PathBuf; -#[cfg(feature = "extensions")] +#[cfg(feature = "tools")] #[derive(Debug)] struct Args { root: PathBuf, @@ -14,11 +14,11 @@ struct Args { } fn main() -> Result<()> { - #[cfg(not(feature = "extensions"))] + #[cfg(not(feature = "tools"))] { - anyhow::bail!("oliphaunt-wasix-dump requires the `extensions` feature"); + anyhow::bail!("oliphaunt-wasix-dump requires the `tools` feature"); } - #[cfg(feature = "extensions")] + #[cfg(feature = "tools")] { let Args { root, passthrough } = parse_args()?; let server = OliphauntServer::builder().path(root).start()?; @@ -29,7 +29,7 @@ fn main() -> Result<()> { } } -#[cfg(feature = "extensions")] +#[cfg(feature = "tools")] fn parse_args() -> Result { let mut root = PathBuf::from("./.oliphaunt"); let mut passthrough = Vec::new(); @@ -56,7 +56,7 @@ fn parse_args() -> Result { Ok(Args { root, passthrough }) } -#[cfg(feature = "extensions")] +#[cfg(feature = "tools")] fn print_usage() { eprintln!("Usage: oliphaunt-wasix-dump --root PATH -- [pg_dump args]"); eprintln!("Example: oliphaunt-wasix-dump --root ./.oliphaunt -- --schema-only"); diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/lib.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/lib.rs index b383b70b..2dd271fc 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/lib.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/lib.rs @@ -7,8 +7,6 @@ mod protocol; #[cfg(feature = "extensions")] pub use oliphaunt::extensions; -#[cfg(feature = "extensions")] -pub use oliphaunt::PgDumpOptions; pub use oliphaunt::{ DataDirArchiveFormat, DataTransferContainer, DescribeQueryParam, DescribeQueryResult, DescribeResultField, EngineCapabilities, ExecProtocolOptions, ExecProtocolResult, FieldInfo, @@ -17,6 +15,8 @@ pub use oliphaunt::{ QueryOptions, QueryTemplate, Results, RowMode, Serializer, SerializerMap, TemplatedQuery, Transaction, TypeParser, format_query, quote_identifier, }; +#[cfg(feature = "tools")] +pub use oliphaunt::{PgDumpOptions, PsqlOptions}; pub use protocol::messages::{BackendMessage, DatabaseError, NoticeMessage}; #[doc(hidden)] diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs index 4b62dbb4..2d27e0d0 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs @@ -132,11 +132,16 @@ pub(crate) fn load_artifact_module(engine: &Engine, artifact_name: &str) -> Resu Ok(module) } -#[cfg(feature = "extensions")] +#[cfg(feature = "tools")] pub(crate) fn load_pg_dump_module(engine: &Engine) -> Result { load_artifact_module(engine, "tool:pg_dump") } +#[cfg(feature = "tools")] +pub(crate) fn load_psql_module(engine: &Engine) -> Result { + load_artifact_module(engine, "tool:psql") +} + #[cfg(feature = "extensions")] #[allow(dead_code)] pub(crate) fn load_initdb_module(engine: &Engine) -> Result { @@ -766,7 +771,7 @@ fn target_aot_manifest_json_for_crate() -> Option<&'static str> { .then_some(liboliphaunt_wasix_aot_aarch64_apple_darwin::MANIFEST_JSON) } -#[cfg(all(target_os = "macos", target_arch = "aarch64"))] +#[cfg(all(feature = "tools", target_os = "macos", target_arch = "aarch64"))] fn target_tools_aot_artifact_bytes(name: &str) -> Option<&'static [u8]> { if !oliphaunt_wasix_tools_aot_aarch64_apple_darwin::HAS_EMBEDDED_AOT { return None; @@ -774,7 +779,7 @@ fn target_tools_aot_artifact_bytes(name: &str) -> Option<&'static [u8]> { oliphaunt_wasix_tools_aot_aarch64_apple_darwin::artifact_bytes(name) } -#[cfg(all(target_os = "macos", target_arch = "aarch64"))] +#[cfg(all(feature = "tools", target_os = "macos", target_arch = "aarch64"))] fn target_tools_aot_manifest_json_for_crate() -> Option<&'static str> { oliphaunt_wasix_tools_aot_aarch64_apple_darwin::HAS_EMBEDDED_AOT .then_some(oliphaunt_wasix_tools_aot_aarch64_apple_darwin::MANIFEST_JSON) @@ -794,7 +799,12 @@ fn target_aot_manifest_json_for_crate() -> Option<&'static str> { .then_some(liboliphaunt_wasix_aot_x86_64_unknown_linux_gnu::MANIFEST_JSON) } -#[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))] +#[cfg(all( + feature = "tools", + target_os = "linux", + target_arch = "x86_64", + target_env = "gnu" +))] fn target_tools_aot_artifact_bytes(name: &str) -> Option<&'static [u8]> { if !oliphaunt_wasix_tools_aot_x86_64_unknown_linux_gnu::HAS_EMBEDDED_AOT { return None; @@ -802,7 +812,12 @@ fn target_tools_aot_artifact_bytes(name: &str) -> Option<&'static [u8]> { oliphaunt_wasix_tools_aot_x86_64_unknown_linux_gnu::artifact_bytes(name) } -#[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))] +#[cfg(all( + feature = "tools", + target_os = "linux", + target_arch = "x86_64", + target_env = "gnu" +))] fn target_tools_aot_manifest_json_for_crate() -> Option<&'static str> { oliphaunt_wasix_tools_aot_x86_64_unknown_linux_gnu::HAS_EMBEDDED_AOT .then_some(oliphaunt_wasix_tools_aot_x86_64_unknown_linux_gnu::MANIFEST_JSON) @@ -822,7 +837,12 @@ fn target_aot_manifest_json_for_crate() -> Option<&'static str> { .then_some(liboliphaunt_wasix_aot_aarch64_unknown_linux_gnu::MANIFEST_JSON) } -#[cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))] +#[cfg(all( + feature = "tools", + target_os = "linux", + target_arch = "aarch64", + target_env = "gnu" +))] fn target_tools_aot_artifact_bytes(name: &str) -> Option<&'static [u8]> { if !oliphaunt_wasix_tools_aot_aarch64_unknown_linux_gnu::HAS_EMBEDDED_AOT { return None; @@ -830,7 +850,12 @@ fn target_tools_aot_artifact_bytes(name: &str) -> Option<&'static [u8]> { oliphaunt_wasix_tools_aot_aarch64_unknown_linux_gnu::artifact_bytes(name) } -#[cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))] +#[cfg(all( + feature = "tools", + target_os = "linux", + target_arch = "aarch64", + target_env = "gnu" +))] fn target_tools_aot_manifest_json_for_crate() -> Option<&'static str> { oliphaunt_wasix_tools_aot_aarch64_unknown_linux_gnu::HAS_EMBEDDED_AOT .then_some(oliphaunt_wasix_tools_aot_aarch64_unknown_linux_gnu::MANIFEST_JSON) @@ -850,7 +875,12 @@ fn target_aot_manifest_json_for_crate() -> Option<&'static str> { .then_some(liboliphaunt_wasix_aot_x86_64_pc_windows_msvc::MANIFEST_JSON) } -#[cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))] +#[cfg(all( + feature = "tools", + target_os = "windows", + target_arch = "x86_64", + target_env = "msvc" +))] fn target_tools_aot_artifact_bytes(name: &str) -> Option<&'static [u8]> { if !oliphaunt_wasix_tools_aot_x86_64_pc_windows_msvc::HAS_EMBEDDED_AOT { return None; @@ -858,7 +888,12 @@ fn target_tools_aot_artifact_bytes(name: &str) -> Option<&'static [u8]> { oliphaunt_wasix_tools_aot_x86_64_pc_windows_msvc::artifact_bytes(name) } -#[cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))] +#[cfg(all( + feature = "tools", + target_os = "windows", + target_arch = "x86_64", + target_env = "msvc" +))] fn target_tools_aot_manifest_json_for_crate() -> Option<&'static str> { oliphaunt_wasix_tools_aot_x86_64_pc_windows_msvc::HAS_EMBEDDED_AOT .then_some(oliphaunt_wasix_tools_aot_x86_64_pc_windows_msvc::MANIFEST_JSON) @@ -874,12 +909,15 @@ fn target_aot_artifact_bytes(_name: &str) -> Option<&'static [u8]> { None } -#[cfg(not(any( - all(target_os = "macos", target_arch = "aarch64"), - all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"), - all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"), - all(target_os = "windows", target_arch = "x86_64", target_env = "msvc") -)))] +#[cfg(any( + not(feature = "tools"), + not(any( + all(target_os = "macos", target_arch = "aarch64"), + all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"), + all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"), + all(target_os = "windows", target_arch = "x86_64", target_env = "msvc") + )) +))] fn target_tools_aot_artifact_bytes(_name: &str) -> Option<&'static [u8]> { None } @@ -894,12 +932,15 @@ fn target_aot_manifest_json_for_crate() -> Option<&'static str> { None } -#[cfg(not(any( - all(target_os = "macos", target_arch = "aarch64"), - all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"), - all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"), - all(target_os = "windows", target_arch = "x86_64", target_env = "msvc") -)))] +#[cfg(any( + not(feature = "tools"), + not(any( + all(target_os = "macos", target_arch = "aarch64"), + all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"), + all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"), + all(target_os = "windows", target_arch = "x86_64", target_env = "msvc") + )) +))] fn target_tools_aot_manifest_json_for_crate() -> Option<&'static str> { None } diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/assets.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/assets.rs index 42917fac..f53cb893 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/assets.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/assets.rs @@ -52,12 +52,12 @@ pub(crate) fn pgdata_template_manifest() -> Option<&'static [u8]> { liboliphaunt_wasix_portable::pgdata_template_manifest() } -#[allow(dead_code)] +#[cfg(feature = "tools")] pub(crate) fn pg_dump_wasm() -> Option<&'static [u8]> { oliphaunt_wasix_tools::pg_dump_wasm() } -#[allow(dead_code)] +#[cfg(feature = "tools")] pub(crate) fn psql_wasm() -> Option<&'static [u8]> { oliphaunt_wasix_tools::psql_wasm() } diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/backend.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/backend.rs index 7cf3ad8e..675fae76 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/backend.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/backend.rs @@ -229,7 +229,7 @@ impl WasixBackendSession { self.pg.start_protocol_with_startup_packet(message) } - #[cfg(feature = "extensions")] + #[cfg(feature = "tools")] pub(crate) fn existing_startup_response(&self) -> Option> { self.pg.existing_startup_response() } @@ -415,7 +415,7 @@ impl BackendSession { self.0.startup_with_packet(message) } - #[cfg(feature = "extensions")] + #[cfg(feature = "tools")] pub(crate) fn existing_startup_response(&self) -> Option> { self.0.existing_startup_response() } diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/client.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/client.rs index 4ef5f95d..1f573611 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/client.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/client.rs @@ -7,13 +7,13 @@ use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use tempfile::TempDir; -#[cfg(feature = "extensions")] +#[cfg(feature = "tools")] use tokio::io::{AsyncWrite, AsyncWriteExt}; -#[cfg(feature = "extensions")] +#[cfg(feature = "tools")] use tokio::runtime::Runtime; -#[cfg(feature = "extensions")] +#[cfg(feature = "tools")] use wasmer_wasix::virtual_net::VirtualTcpSocket; -#[cfg(feature = "extensions")] +#[cfg(feature = "tools")] use wasmer_wasix::virtual_net::tcp_pair::TcpSocketHalfRx; use crate::oliphaunt::aot; @@ -40,7 +40,7 @@ use crate::oliphaunt::interface::{ use crate::oliphaunt::parse::{ command_tag_row_count, parse_describe_statement_results, parse_results, }; -#[cfg(feature = "extensions")] +#[cfg(feature = "tools")] use crate::oliphaunt::pg_dump::{PgDumpOptions, PgDumpVirtualSocket, dump_direct_sql}; #[cfg(feature = "extensions")] use crate::oliphaunt::postgres_mod::PostgresMod; @@ -48,7 +48,7 @@ use crate::oliphaunt::timing; use crate::oliphaunt::types::{ ArrayTypeInfo, DEFAULT_PARSERS, DEFAULT_SERIALIZERS, TEXT, register_array_type, }; -#[cfg(feature = "extensions")] +#[cfg(feature = "tools")] use crate::oliphaunt::wire::{FrontendFrameKind, FrontendFrameReader, classify_frontend_message}; use crate::protocol::messages::{BackendMessage, DatabaseError}; use crate::protocol::parser::Parser as ProtocolParser; @@ -443,7 +443,7 @@ impl Oliphaunt { } /// Run the bundled WASIX `pg_dump` against this database and return SQL text. - #[cfg(feature = "extensions")] + #[cfg(feature = "tools")] pub fn dump_sql(&mut self, options: PgDumpOptions) -> Result { self.check_ready()?; options.validate()?; @@ -452,7 +452,7 @@ impl Oliphaunt { } /// Run the bundled WASIX `pg_dump` and return UTF-8 SQL bytes. - #[cfg(feature = "extensions")] + #[cfg(feature = "tools")] pub fn dump_bytes(&mut self, options: PgDumpOptions) -> Result> { Ok(self.dump_sql(options)?.into_bytes()) } @@ -532,7 +532,7 @@ impl Oliphaunt { Ok(()) } - #[cfg(feature = "extensions")] + #[cfg(feature = "tools")] fn dump_sql_via_direct_protocol(&mut self, options: &PgDumpOptions) -> Result { ensure_direct_pg_dump_options_match_session(self.backend.startup_config(), options)?; let result = dump_direct_sql(options, |socket| self.serve_direct_pg_dump_protocol(socket)); @@ -548,14 +548,14 @@ impl Oliphaunt { } } - #[cfg(feature = "extensions")] + #[cfg(feature = "tools")] fn cleanup_after_direct_pg_dump_session(&mut self) -> Result<()> { self.exec("DEALLOCATE ALL; SET search_path TO DEFAULT;", None) .context("reset direct pg_dump session state")?; Ok(()) } - #[cfg(feature = "extensions")] + #[cfg(feature = "tools")] fn serve_direct_pg_dump_protocol(&mut self, mut socket: PgDumpVirtualSocket) -> Result<()> { let _ = socket.set_nodelay(true); let (mut socket_tx, mut socket_rx) = socket.split(); @@ -1470,7 +1470,7 @@ impl Drop for Oliphaunt { } } -#[cfg(feature = "extensions")] +#[cfg(feature = "tools")] fn ensure_direct_pg_dump_options_match_session( startup_config: &StartupConfig, options: &PgDumpOptions, @@ -1492,7 +1492,7 @@ fn ensure_direct_pg_dump_options_match_session( Ok(()) } -#[cfg(feature = "extensions")] +#[cfg(feature = "tools")] fn read_direct_pg_dump_socket( runtime: &Runtime, reader: &mut TcpSocketHalfRx, @@ -1518,7 +1518,7 @@ fn read_direct_pg_dump_socket( .context("read direct pg_dump virtual socket") } -#[cfg(feature = "extensions")] +#[cfg(feature = "tools")] fn write_direct_pg_dump_socket( runtime: &Runtime, writer: &mut (impl AsyncWrite + Unpin), @@ -1529,7 +1529,7 @@ fn write_direct_pg_dump_socket( .context("write direct pg_dump virtual socket") } -#[cfg(feature = "extensions")] +#[cfg(feature = "tools")] fn flush_direct_pg_dump_socket( runtime: &Runtime, writer: &mut (impl AsyncWrite + Unpin), diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/extensions.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/extensions.rs index fca400ad..650c986d 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/extensions.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/extensions.rs @@ -232,7 +232,9 @@ pub(crate) fn extension_session_setup_sql(extension: Extension) -> Vec { #[cfg(all(test, feature = "extensions"))] mod candidate_tests { use super::*; - use crate::{Oliphaunt, OliphauntServer, PgDumpOptions}; + #[cfg(feature = "tools")] + use crate::PgDumpOptions; + use crate::{Oliphaunt, OliphauntServer}; use anyhow::{Context, Result, ensure}; use sqlx::{Connection, PgConnection}; use std::collections::BTreeSet; @@ -254,6 +256,7 @@ mod candidate_tests { } #[test] + #[cfg(feature = "tools")] fn public_extensions_pass_direct_dump_restore_smoke() -> Result<()> { run_direct_dump_restore_smoke_set(generated::ALL) } @@ -293,11 +296,13 @@ mod candidate_tests { #[test] #[ignore = "promotion gate: run manually before marking packaged candidates stable"] + #[cfg(feature = "tools")] fn packaged_candidate_extensions_pass_direct_dump_restore_smoke() -> Result<()> { run_direct_dump_restore_smoke_set(generated::CANDIDATES) } #[test] + #[cfg(feature = "tools")] fn uuid_ossp_candidate_passes_direct_dump_restore_smoke() -> Result<()> { run_direct_dump_restore_smoke_set(&[generated::CANDIDATE_UUID_OSSP]) } @@ -443,6 +448,7 @@ mod candidate_tests { assert_only_resolved_extension_libraries_are_materialized(root.path(), extension) } + #[cfg(feature = "tools")] fn run_direct_dump_restore_smoke_set(extensions: &[Extension]) -> Result<()> { let extensions = embedded_extension_archives(extensions); let mut failures = Vec::new(); @@ -459,6 +465,7 @@ mod candidate_tests { Ok(()) } + #[cfg(feature = "tools")] fn run_one_direct_dump_restore_smoke(extension: Extension) -> Result<()> { let name = extension.sql_name(); let dump = { diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/mod.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/mod.rs index 1a3e7b60..8e0a4860 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/mod.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/mod.rs @@ -12,7 +12,7 @@ pub(crate) mod errors; pub mod extensions; pub(crate) mod interface; pub(crate) mod parse; -#[cfg(feature = "extensions")] +#[cfg(feature = "tools")] pub mod pg_dump; pub(crate) mod postgres_mod; pub(crate) mod proxy; @@ -43,8 +43,8 @@ pub use interface::{ DescribeResultField, ExecProtocolOptions, ExecProtocolResult, FieldInfo, NoticeCallback, ParserMap, QueryOptions, Results, RowMode, Serializer, SerializerMap, TypeParser, }; -#[cfg(feature = "extensions")] -pub use pg_dump::PgDumpOptions; +#[cfg(feature = "tools")] +pub use pg_dump::{PgDumpOptions, PsqlOptions}; #[doc(hidden)] pub use postgres_mod::{FsTraceSnapshot, fs_trace_snapshot, reset_fs_trace}; pub use proxy::{ diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/pg_dump.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/pg_dump.rs index b3deab46..508b062f 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/pg_dump.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/pg_dump.rs @@ -103,6 +103,82 @@ impl PgDumpOptions { } } +/// Options for the bundled WASIX `psql` runner. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PsqlOptions { + args: Vec, + database: String, + username: String, +} + +impl Default for PsqlOptions { + fn default() -> Self { + Self { + args: Vec::new(), + database: "template1".to_owned(), + username: "postgres".to_owned(), + } + } +} + +impl PsqlOptions { + pub fn new() -> Self { + Self::default() + } + + /// Add one raw `psql` argument. + pub fn arg(mut self, arg: impl Into) -> Self { + self.args.push(arg.into()); + self + } + + /// Add raw `psql` arguments. + pub fn args(mut self, args: impl IntoIterator>) -> Self { + self.args.extend(args.into_iter().map(Into::into)); + self + } + + /// Run a non-interactive SQL command with `psql -c`. + pub fn command(mut self, sql: impl Into) -> Self { + self.args.push("-c".to_owned()); + self.args.push(sql.into()); + self + } + + /// Select the database passed to `psql`. + pub fn database(mut self, database: impl Into) -> Self { + self.database = database.into(); + self + } + + /// Select the user passed to `psql`. + pub fn username(mut self, username: impl Into) -> Self { + self.username = username.into(); + self + } + + pub(crate) fn validate(&self) -> Result<()> { + for (name, value) in [("database", &self.database), ("username", &self.username)] { + anyhow::ensure!( + !value.is_empty() && !value.contains('\0'), + "psql {name} must not be empty or contain NUL bytes" + ); + } + anyhow::ensure!( + !self.args.is_empty(), + "psql runner requires non-interactive arguments; use PsqlOptions::command or pass raw psql args" + ); + for arg in &self.args { + anyhow::ensure!( + !arg.contains('\0'), + "psql argument must not contain NUL bytes" + ); + validate_psql_passthrough_arg(arg)?; + } + Ok(()) + } +} + fn validate_passthrough_arg(arg: &str) -> Result<()> { if let Some(flag) = disallowed_pg_dump_flag(arg) { anyhow::bail!( @@ -149,10 +225,58 @@ fn disallowed_pg_dump_flag(arg: &str) -> Option<&'static str> { None } +fn validate_psql_passthrough_arg(arg: &str) -> Result<()> { + if let Some(flag) = disallowed_psql_flag(arg) { + anyhow::bail!( + "psql argument '{arg}' conflicts with oliphaunt-wasix's managed {flag}; use PsqlOptions typed setters where available" + ); + } + Ok(()) +} + +fn disallowed_psql_flag(arg: &str) -> Option<&'static str> { + const LONG_FLAGS: &[(&str, &str)] = &[ + ("--host", "host"), + ("--port", "port"), + ("--username", "username"), + ("--dbname", "database"), + ("--output", "stdout capture"), + ("--log-file", "stderr capture"), + ]; + for (flag, label) in LONG_FLAGS { + if arg == *flag + || arg + .strip_prefix(*flag) + .is_some_and(|tail| tail.starts_with('=')) + { + return Some(label); + } + } + + const SHORT_FLAGS: &[(&str, &str)] = &[ + ("-h", "host"), + ("-p", "port"), + ("-U", "username"), + ("-d", "database"), + ("-o", "stdout capture"), + ("-L", "stderr capture"), + ]; + for (flag, label) in SHORT_FLAGS { + if arg == *flag || (arg.starts_with(*flag) && arg.len() > flag.len()) { + return Some(label); + } + } + None +} + pub(crate) fn dump_server_sql(addr: SocketAddr, options: &PgDumpOptions) -> Result { dump_sql_with_networking(addr, options, LocalNetworking::new()) } +pub(crate) fn run_server_psql(addr: SocketAddr, options: &PsqlOptions) -> Result { + run_psql_with_networking(addr, options, LocalNetworking::new()) +} + pub(crate) type PgDumpVirtualSocket = TcpSocketHalf; pub(crate) fn dump_direct_sql(options: &PgDumpOptions, serve: F) -> Result @@ -336,6 +460,129 @@ where Ok(strip_pg_dump_restrict_meta_commands(sql)) } +fn run_psql_with_networking( + addr: SocketAddr, + options: &PsqlOptions, + networking: N, +) -> Result +where + N: VirtualNetworking + Sync, +{ + options.validate()?; + let _phase = timing::phase("psql"); + let wasm = { + let _phase = timing::phase("psql.load_embedded_module"); + assets::psql_wasm() + .ok_or_else(|| anyhow!("WASIX psql asset is not bundled in this build"))? + }; + let engine = aot::headless_engine(); + let module = { + let _phase = timing::phase("psql.load_aot"); + aot::load_psql_module(&engine)? + }; + let _store = Store::new(engine.clone()); + + let fs_root = TempDir::new().context("create psql WASIX filesystem root")?; + if let Some(runtime_archive) = assets::runtime_archive() { + unpack_runtime_archive_reader( + Cursor::new(runtime_archive), + Path::new("oliphaunt.wasix.tar.zst"), + fs_root.path(), + ) + .context("install WASIX runtime files for psql")?; + install_optional_icu_data(&fs_root.path().join("oliphaunt")) + .context("install WASIX ICU data for psql")?; + } + let runtime = { + let _phase = timing::phase("psql.tokio_runtime"); + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .context("create Tokio runtime for WASIX psql")? + }; + let (host_fs, wasix_runtime) = { + let _phase = timing::phase("psql.wasix_runtime"); + let _runtime_guard = runtime.enter(); + let host_fs = SyncHostFileSystem::new(fs_root.path()).with_context(|| { + format!( + "create host filesystem rooted at {}", + fs_root.path().display() + ) + })?; + let host_fs = Arc::new(host_fs) as Arc; + let mut wasix_runtime = PluggableRuntime::new(Arc::new(TokioTaskManager::new( + tokio::runtime::Handle::current(), + ))); + wasix_runtime.set_engine(engine.clone()); + wasix_runtime.set_networking_implementation(networking); + (host_fs, wasix_runtime) + }; + + let port = addr.port().to_string(); + let host = match addr { + SocketAddr::V4(addr) => addr.ip().to_string(), + SocketAddr::V6(addr) => addr.ip().to_string(), + }; + let mut args = vec![ + "-X".to_owned(), + "-v".to_owned(), + "ON_ERROR_STOP=1".to_owned(), + "-U".to_owned(), + options.username.clone(), + "-h".to_owned(), + host, + "-p".to_owned(), + port, + "-d".to_owned(), + options.database.clone(), + ]; + args.extend(options.args.clone()); + + let stdout = Arc::new(Mutex::new(Vec::new())); + let stderr = Arc::new(Mutex::new(Vec::new())); + let mut runner = WasiRunner::new(); + runner + .with_mount("/".to_owned(), Arc::clone(&host_fs)) + .with_mount("/host".to_owned(), host_fs) + .with_current_dir("/") + .with_args(args) + .with_envs([ + ("PGUSER", options.username.as_str()), + ("PGPASSWORD", "password"), + ("PGSSLMODE", "disable"), + ]) + .with_stdout(Box::new(CaptureFile::new(Arc::clone(&stdout)))) + .with_stderr(Box::new(CaptureFile::new(Arc::clone(&stderr)))); + if fs_root.path().join("oliphaunt/share/icu").is_dir() { + runner.with_envs([("ICU_DATA", "/oliphaunt/share/icu")]); + } + { + let _phase = timing::phase("psql.run_wasm"); + runner + .run_wasm( + RuntimeOrEngine::Runtime(Arc::new(wasix_runtime)), + "psql", + module, + ModuleHash::sha256(wasm), + ) + .map_err(|err| { + let stderr = + String::from_utf8_lossy(&stderr.lock().expect("stderr capture poisoned")) + .trim() + .to_owned(); + if stderr.is_empty() { + anyhow!(err) + } else { + anyhow!("{err}; psql stderr: {stderr}") + } + }) + .context("run WASIX psql")?; + } + + String::from_utf8(stdout.lock().expect("stdout capture poisoned").clone()) + .context("decode psql stdout as UTF-8") +} + fn strip_pg_dump_restrict_meta_commands(script: String) -> String { let mut stripped = String::with_capacity(script.len()); for line in script.split_inclusive('\n') { @@ -706,7 +953,7 @@ impl Seek for CaptureFile { } } -#[cfg(all(test, feature = "extensions"))] +#[cfg(all(test, feature = "tools", feature = "extensions"))] mod tests { use super::*; use crate::oliphaunt::Oliphaunt; @@ -771,6 +1018,58 @@ mod tests { .validate() } + #[test] + fn psql_options_reject_managed_args() { + for arg in [ + "-h", + "-hlocalhost", + "--host=localhost", + "-p", + "-p5432", + "--port=5432", + "-U", + "-Upostgres", + "--username=postgres", + "-d", + "-dpostgres", + "--dbname=postgres", + "-o", + "-o/tmp/out", + "--output=/tmp/out", + "-L", + "-L/tmp/log", + "--log-file=/tmp/log", + ] { + let err = PsqlOptions::new() + .arg("-c") + .arg("SELECT 1") + .arg(arg) + .validate() + .expect_err("managed psql arg should be rejected"); + assert!( + err.to_string().contains("conflicts with oliphaunt-wasix"), + "unexpected error for {arg}: {err:#}" + ); + } + } + + #[test] + fn psql_options_require_non_interactive_args() { + let err = PsqlOptions::new() + .validate() + .expect_err("psql without args should be rejected"); + assert!( + err.to_string() + .contains("requires non-interactive arguments"), + "unexpected error: {err:#}" + ); + } + + #[test] + fn psql_options_allow_command_and_formatting_args() -> Result<()> { + PsqlOptions::new().arg("-tA").command("SELECT 1").validate() + } + #[test] fn pg_dump_sql_strips_only_pg18_restrict_meta_commands() { let script = "\\restrict AbC123\n\ diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/postgres_mod.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/postgres_mod.rs index 60e84783..1764a4e2 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/postgres_mod.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/postgres_mod.rs @@ -1005,7 +1005,7 @@ impl PostgresMod { }) } - #[cfg(feature = "extensions")] + #[cfg(feature = "tools")] pub(crate) fn existing_startup_response(&self) -> Option> { self.startup_response.clone() } diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/server.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/server.rs index 5b353d56..d0e0bd4b 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/server.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/server.rs @@ -19,8 +19,8 @@ use crate::oliphaunt::config::{PostgresConfig, StartupConfig}; #[cfg(feature = "extensions")] use crate::oliphaunt::extensions::{Extension, resolve_extension_set}; use crate::oliphaunt::interface::DebugLevel; -#[cfg(feature = "extensions")] -use crate::oliphaunt::pg_dump::{PgDumpOptions, dump_server_sql}; +#[cfg(feature = "tools")] +use crate::oliphaunt::pg_dump::{PgDumpOptions, PsqlOptions, dump_server_sql, run_server_psql}; use crate::oliphaunt::proxy::OliphauntProxy; use crate::oliphaunt::timing; @@ -108,7 +108,7 @@ impl OliphauntServer { } /// Run the bundled WASIX `pg_dump` against this server and return SQL text. - #[cfg(feature = "extensions")] + #[cfg(feature = "tools")] pub fn dump_sql(&self, options: PgDumpOptions) -> Result { let addr = self .tcp_addr() @@ -117,11 +117,26 @@ impl OliphauntServer { } /// Run the bundled WASIX `pg_dump` and return UTF-8 SQL bytes. - #[cfg(feature = "extensions")] + #[cfg(feature = "tools")] pub fn dump_bytes(&self, options: PgDumpOptions) -> Result> { Ok(self.dump_sql(options)?.into_bytes()) } + /// Run the bundled WASIX `psql` against this server and return stdout text. + #[cfg(feature = "tools")] + pub fn psql(&self, options: PsqlOptions) -> Result { + let addr = self + .tcp_addr() + .context("psql currently requires a TCP OliphauntServer endpoint")?; + run_server_psql(addr, &options) + } + + /// Run the bundled WASIX `psql` and return stdout bytes. + #[cfg(feature = "tools")] + pub fn psql_bytes(&self, options: PsqlOptions) -> Result> { + Ok(self.psql(options)?.into_bytes()) + } + /// Request shutdown and wait for the listener thread to exit. /// /// Close database clients before calling this method. The current proxy owns diff --git a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock index a8685aaf..eb2a285a 100644 --- a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock +++ b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock @@ -3528,7 +3528,7 @@ dependencies = [ name = "oliphaunt-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "0fe403cee7d4d080ba6795a93a99d14a43812202639eb7295410c0cd27d6a022" +checksum = "987e82c9952421633cc7d31e3ec3615856ff3833e503cac02f5b88930e7d23fc" dependencies = [ "anyhow", "async-trait", diff --git a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml index 717f6f9c..2a06619e 100644 --- a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml +++ b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml @@ -17,7 +17,10 @@ tauri-build = { version = "2", features = [] } [dependencies] anyhow = "1" -oliphaunt-wasix = { version = "=0.1.0", registry = "oliphaunt-local", features = ["extensions"] } +oliphaunt-wasix = { version = "=0.1.0", registry = "oliphaunt-local", features = [ + "extensions", + "tools", +] } oliphaunt-wasix-tools = { version = "=0.1.0", registry = "oliphaunt-local" } sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio-rustls", "postgres"] } tauri = { version = "2", features = [] } diff --git a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs index 4f4401da..e59363be 100644 --- a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs +++ b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs @@ -5,7 +5,8 @@ use std::time::{Duration, Instant}; use anyhow::{Context, Result, anyhow, bail}; use oliphaunt_wasix::{ - OliphauntPaths, OliphauntServer, PgDumpOptions, install_into, preload_runtime_module, + OliphauntPaths, OliphauntServer, PgDumpOptions, PsqlOptions, install_into, + preload_runtime_module, }; use serde::Serialize; use sqlx::postgres::{PgConnectOptions, PgPoolOptions, PgSslMode}; @@ -341,6 +342,11 @@ fn validate_wasix_tools(server: &OliphauntServer) -> Result<()> { dump.contains("PostgreSQL database dump"), "pg_dump SQL backup smoke did not look like a PostgreSQL dump" ); + let psql = server.psql(PsqlOptions::new().arg("-tA").command("SELECT 1"))?; + anyhow::ensure!( + psql.lines().any(|line| line.trim() == "1"), + "psql smoke did not return SELECT 1 output" + ); Ok(()) } diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml b/src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml index d9c4c6ad..f49f92b6 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml +++ b/src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml @@ -18,6 +18,10 @@ include = [ "payload/**", ] +[package.metadata.oliphaunt-wasix-tools.assets] +pg-dump-wasix-sha256 = "6f3e92ba8a9faae2cf108a9d6e0f91e399e27d2f54c543297eaf5de63d511418" +psql-wasix-sha256 = "41c20c6c43ad437a732b0248efa173b5e0edcd2ab5bb4eee2752595201aa9db9" + [lib] path = "src/lib.rs" diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools/README.md b/src/runtimes/liboliphaunt/wasix/crates/tools/README.md index 7f1ceb6f..63531676 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/tools/README.md +++ b/src/runtimes/liboliphaunt/wasix/crates/tools/README.md @@ -1,5 +1,5 @@ # oliphaunt-wasix-tools Cargo artifact crate for Oliphaunt WASIX PostgreSQL command-line tools. -Applications do not depend on this crate directly; SDK crates select it when -they need the WASIX `pg_dump` or `psql` modules. +The `oliphaunt-wasix` crate selects it through the `tools` feature when an +application needs the WASIX `pg_dump` or `psql` modules. diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 79a7bfa2..725a364d 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -1380,6 +1380,22 @@ def check_wasm(findings: list[Finding]) -> None: f"oliphaunt-wasix Cargo.toml default={features.get('default')!r}", severity="P0", ) + expected_tools_feature = { + "dep:oliphaunt-wasix-tools", + "dep:oliphaunt-wasix-tools-aot-aarch64-apple-darwin", + "dep:oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu", + "dep:oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc", + "dep:oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu", + } + require( + findings, + product, + "wasm-tools-feature", + set(features.get("tools", [])) == expected_tools_feature, + "WASM crate must keep pg_dump/psql artifacts behind an explicit tools feature.", + f"oliphaunt-wasix Cargo.toml tools={features.get('tools')!r}", + severity="P0", + ) runtime_version = product_metadata.read_current_version("liboliphaunt-wasix") dependencies = manifest.get("dependencies", {}) target_tables = manifest.get("target", {}) @@ -1400,8 +1416,9 @@ def check_wasm(findings: list[Finding]) -> None: product, "wasm-tools-artifact-dependency", isinstance(expected_tools_dependency, dict) - and expected_tools_dependency.get("version") == f"={runtime_version}", - "WASM crate must depend on the public WASIX tools artifact crate at the liboliphaunt-wasix version.", + and expected_tools_dependency.get("version") == f"={runtime_version}" + and expected_tools_dependency.get("optional") is True, + "WASM crate must depend optionally on the public WASIX tools artifact crate at the liboliphaunt-wasix version.", f"oliphaunt-wasix-tools dependency={expected_tools_dependency!r}", severity="P0", ) @@ -1418,18 +1435,28 @@ def check_wasm(findings: list[Finding]) -> None: 'cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))': "oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc", } missing_aot_dependencies = [] - for cfg, crate in {**expected_aot_dependencies, **expected_tools_aot_dependencies}.items(): + for cfg, crate in expected_aot_dependencies.items(): target = target_tables.get(cfg) target_dependencies = target.get("dependencies", {}) if isinstance(target, dict) else {} dependency = target_dependencies.get(crate) if not isinstance(dependency, dict) or dependency.get("version") != f"={runtime_version}": missing_aot_dependencies.append(f"{cfg}:{crate}") + for cfg, crate in expected_tools_aot_dependencies.items(): + target = target_tables.get(cfg) + target_dependencies = target.get("dependencies", {}) if isinstance(target, dict) else {} + dependency = target_dependencies.get(crate) + if ( + not isinstance(dependency, dict) + or dependency.get("version") != f"={runtime_version}" + or dependency.get("optional") is not True + ): + missing_aot_dependencies.append(f"{cfg}:{crate}") require( findings, product, "wasm-aot-artifact-dependencies", not missing_aot_dependencies, - "WASM crate must depend on every public target-specific root/tools AOT artifact crate behind exact Cargo target cfgs.", + "WASM crate must depend on every public target-specific root AOT crate and optional tools AOT crate behind exact Cargo target cfgs.", missing_aot_dependencies or "src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml", severity="P0", ) diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 36ac2c2b..2806303c 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1048,8 +1048,12 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None if not isinstance(runtime_dependency, dict) or runtime_dependency.get("version") != f"={wasix_runtime_version}": fail("oliphaunt-wasix must depend on liboliphaunt-wasix-portable at the exact liboliphaunt-wasix runtime version") tools_dependency = dependencies.get("oliphaunt-wasix-tools") - if not isinstance(tools_dependency, dict) or tools_dependency.get("version") != f"={wasix_runtime_version}": - fail("oliphaunt-wasix must depend on oliphaunt-wasix-tools at the exact liboliphaunt-wasix runtime version") + if ( + not isinstance(tools_dependency, dict) + or tools_dependency.get("version") != f"={wasix_runtime_version}" + or tools_dependency.get("optional") is not True + ): + fail("oliphaunt-wasix must optionally depend on oliphaunt-wasix-tools at the exact liboliphaunt-wasix runtime version") expected_aot_dependencies = { 'cfg(all(target_os = "macos", target_arch = "aarch64"))': "liboliphaunt-wasix-aot-aarch64-apple-darwin", 'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))': "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", @@ -1063,12 +1067,32 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None 'cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))': "oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc", } target_tables = manifest.get("target", {}) - for cfg, crate in {**expected_aot_dependencies, **expected_tools_aot_dependencies}.items(): + for cfg, crate in expected_aot_dependencies.items(): target = target_tables.get(cfg) target_dependencies = target.get("dependencies", {}) if isinstance(target, dict) else {} dependency = target_dependencies.get(crate) if not isinstance(dependency, dict) or dependency.get("version") != f"={wasix_runtime_version}": fail(f"oliphaunt-wasix must depend on {crate} at the exact liboliphaunt-wasix runtime version behind {cfg}") + for cfg, crate in expected_tools_aot_dependencies.items(): + target = target_tables.get(cfg) + target_dependencies = target.get("dependencies", {}) if isinstance(target, dict) else {} + dependency = target_dependencies.get(crate) + if ( + not isinstance(dependency, dict) + or dependency.get("version") != f"={wasix_runtime_version}" + or dependency.get("optional") is not True + ): + fail(f"oliphaunt-wasix must optionally depend on {crate} at the exact liboliphaunt-wasix runtime version behind {cfg}") + expected_tools_feature = { + "dep:oliphaunt-wasix-tools", + "dep:oliphaunt-wasix-tools-aot-aarch64-apple-darwin", + "dep:oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu", + "dep:oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc", + "dep:oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu", + } + tools_feature = set(manifest.get("features", {}).get("tools", [])) + if tools_feature != expected_tools_feature: + fail("oliphaunt-wasix tools feature must select exactly the WASIX pg_dump/psql tool artifact crates") aot_source = read_text("src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs") for cfg in expected_aot_dependencies: rust_cfg = cfg.removeprefix("cfg(").removesuffix(")") diff --git a/tools/xtask/src/asset_checks.rs b/tools/xtask/src/asset_checks.rs index 5f079509..8e0934bc 100644 --- a/tools/xtask/src/asset_checks.rs +++ b/tools/xtask/src/asset_checks.rs @@ -445,7 +445,10 @@ pub(crate) fn verify_asset_manifest_hashes() -> Result<()> { verify_root_asset_metadata(&manifest, &manifest.runtime.module_sha256)?; verify_file_sha256( &pgdata_archive, - &cargo_metadata_value("pgdata-template-archive-sha256")?, + &cargo_metadata_value( + "src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml", + "pgdata-template-archive-sha256", + )?, "PGDATA template archive metadata", )?; } @@ -478,66 +481,75 @@ fn verify_root_asset_metadata( manifest: &AssetManifestOut, runtime_module_sha256: &str, ) -> Result<()> { - verify_metadata_value( + verify_root_metadata_value( "runtime-archive-sha256", &manifest.runtime.sha256, "runtime archive metadata", )?; - verify_metadata_value( + verify_root_metadata_value( "oliphaunt-wasix-sha256", runtime_module_sha256, "runtime module metadata", )?; - verify_metadata_value( + verify_root_metadata_value( "postgres-version", &manifest.runtime.postgres_version, "PostgreSQL version metadata", )?; let pg18 = load_postgres_source_manifest()?; - verify_metadata_value( + verify_root_metadata_value( "postgres-source-url", &pg18.postgresql.url, "PostgreSQL source URL metadata", )?; - verify_metadata_value( + verify_root_metadata_value( "postgres-source-sha256", &pg18.postgresql.sha256, "PostgreSQL source sha256 metadata", )?; - verify_metadata_value( + verify_root_metadata_value( "postgres-patch-count", &pg18.patches.series.len().to_string(), "PostgreSQL patch count metadata", )?; if let Some(pg_dump) = &manifest.pg_dump { - verify_metadata_value("pg-dump-wasix-sha256", &pg_dump.sha256, "pg_dump metadata")?; + verify_tools_metadata_value("pg-dump-wasix-sha256", &pg_dump.sha256, "pg_dump metadata")?; } if let Some(psql) = &manifest.psql { - verify_metadata_value("psql-wasix-sha256", &psql.sha256, "psql metadata")?; + verify_tools_metadata_value("psql-wasix-sha256", &psql.sha256, "psql metadata")?; } if let Some(initdb) = &manifest.initdb { - verify_metadata_value("initdb-wasix-sha256", &initdb.sha256, "initdb metadata")?; + verify_root_metadata_value("initdb-wasix-sha256", &initdb.sha256, "initdb metadata")?; } Ok(()) } -fn verify_metadata_value(key: &str, expected: &str, field: &str) -> Result<()> { - let actual = cargo_metadata_value(key)?; +fn verify_root_metadata_value(key: &str, expected: &str, field: &str) -> Result<()> { + let actual = cargo_metadata_value( + "src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml", + key, + )?; + ensure_eq(&actual, expected, field) +} + +fn verify_tools_metadata_value(key: &str, expected: &str, field: &str) -> Result<()> { + let actual = cargo_metadata_value( + "src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml", + key, + )?; ensure_eq(&actual, expected, field) } -fn cargo_metadata_value(key: &str) -> Result { - let text = fs::read_to_string("src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml") - .context("read src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml")?; +fn cargo_metadata_value(path: &str, key: &str) -> Result { + let text = fs::read_to_string(path).with_context(|| format!("read {path}"))?; let needle = format!("{key} = \""); - let start = text.find(&needle).ok_or_else(|| { - anyhow!( - "src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml metadata key '{key}' is missing" - ) - })? + needle.len(); - let end = text[start..].find('"').ok_or_else(|| { - anyhow!("src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml metadata key '{key}' is unterminated") - })?; + let start = text + .find(&needle) + .ok_or_else(|| anyhow!("{path} metadata key '{key}' is missing"))? + + needle.len(); + let end = text[start..] + .find('"') + .ok_or_else(|| anyhow!("{path} metadata key '{key}' is unterminated"))?; Ok(text[start..start + end].to_owned()) } @@ -1361,8 +1373,6 @@ fn check_root_asset_metadata_keys() -> Result<()> { "runtime-archive-sha256", "oliphaunt-wasix-sha256", "pgdata-template-archive-sha256", - "pg-dump-wasix-sha256", - "psql-wasix-sha256", "initdb-wasix-sha256", ] { let needle = format!("{required} = \""); @@ -1371,6 +1381,16 @@ fn check_root_asset_metadata_keys() -> Result<()> { "{path} is missing WASIX asset metadata key {required}" ); } + let tools_path = "src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml"; + let tools_text = + fs::read_to_string(tools_path).with_context(|| format!("read {tools_path}"))?; + for required in ["pg-dump-wasix-sha256", "psql-wasix-sha256"] { + let needle = format!("{required} = \""); + ensure!( + tools_text.contains(&needle), + "{tools_path} is missing WASIX tools asset metadata key {required}" + ); + } Ok(()) } diff --git a/tools/xtask/src/asset_pipeline.rs b/tools/xtask/src/asset_pipeline.rs index f25f3e05..884c28cb 100644 --- a/tools/xtask/src/asset_pipeline.rs +++ b/tools/xtask/src/asset_pipeline.rs @@ -3229,7 +3229,10 @@ fn update_root_asset_metadata_in( runtime_module_sha256: &str, ) -> Result<()> { let path = workspace.join("src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml"); + let tools_path = workspace.join("src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml"); let mut text = fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?; + let mut tools_text = fs::read_to_string(&tools_path) + .with_context(|| format!("read {}", tools_path.display()))?; let pg18 = load_postgres_source_manifest()?; text = replace_metadata_value(text, "postgres-version", &manifest.runtime.postgres_version); text = replace_metadata_value(text, "postgres-source-url", &pg18.postgresql.url); @@ -3250,15 +3253,16 @@ fn update_root_asset_metadata_in( ); } if let Some(pg_dump) = &manifest.pg_dump { - text = replace_metadata_value(text, "pg-dump-wasix-sha256", &pg_dump.sha256); + tools_text = replace_metadata_value(tools_text, "pg-dump-wasix-sha256", &pg_dump.sha256); } if let Some(psql) = &manifest.psql { - text = replace_metadata_value(text, "psql-wasix-sha256", &psql.sha256); + tools_text = replace_metadata_value(tools_text, "psql-wasix-sha256", &psql.sha256); } if let Some(initdb) = &manifest.initdb { text = replace_metadata_value(text, "initdb-wasix-sha256", &initdb.sha256); } - fs::write(&path, text).with_context(|| format!("write {}", path.display())) + fs::write(&path, text).with_context(|| format!("write {}", path.display()))?; + fs::write(&tools_path, tools_text).with_context(|| format!("write {}", tools_path.display())) } fn replace_metadata_value(mut text: String, key: &str, value: &str) -> String { From bfc3a1fc2a25ab6fbab0eb8603fec8a99a85423b Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Thu, 25 Jun 2026 22:54:08 +0000 Subject: [PATCH 016/137] test: add tauri example webdriver smoke --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 9 +- examples/README.md | 12 ++ examples/tools/check-examples.sh | 4 + examples/tools/run-tauri-webdriver-smoke.sh | 63 ++++++ examples/tools/tauri-webdriver-smoke.mjs | 189 ++++++++++++++++++ 5 files changed, 273 insertions(+), 4 deletions(-) create mode 100755 examples/tools/run-tauri-webdriver-smoke.sh create mode 100755 examples/tools/tauri-webdriver-smoke.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index d3066623..d65e947d 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -16,11 +16,11 @@ review production pipelines, then normalize implementation details. ## Priority 1: Example App Validation -- [ ] Inventory every example app, its package managers, local-registry dependencies, and runtime/tool/extension paths. +- [x] Inventory every example app, its package managers, local-registry dependencies, and runtime/tool/extension paths. - [ ] Ensure each native example uses `oliphaunt-tools-*` from the local registry when it exercises standalone tools. - [x] Ensure each WASIX example uses `oliphaunt-wasix-tools` from the local registry and does not rely on path-only tool assets. - [ ] Add example-app smoke commands that model the desired developer experience and can run on Linux CI. -- [ ] Check frontend build/test flows for the Electron, Electron WASIX, Tauri, Tauri WASIX, and WASIX vanilla examples. +- [x] Check frontend build/test flows for the Electron, Electron WASIX, Tauri, Tauri WASIX, and WASIX vanilla examples. ## Priority 2: CI and Release Shape @@ -50,7 +50,8 @@ review production pipelines, then normalize implementation details. ## Current Notes -- The latest pushed commit is `cb9845d fix: package runtime tools separately`. +- The latest pushed commit is `3bf731e fix: split wasix tools behind feature`. - Local-registry WASIX smoke coverage proves `pg_dump` through the SDK `dump_sql` path and `psql` through `PsqlOptions::command("SELECT 1")`. - Local-registry Cargo payload inspection confirmed `liboliphaunt-native-linux-x64-gnu-part-*` contains `initdb`, `pg_ctl`, and `postgres` only under `runtime/bin`, while `oliphaunt-tools-linux-x64-gnu-part-*` contains only `pg_dump` and `psql` there. -- Full GUI e2e likely needs a headless display/browser harness. Prefer an existing project-native test command if one exists; otherwise evaluate Playwright/WebdriverIO/Tauri-driver style tooling before adding dependencies. +- `examples/tools/run-tauri-webdriver-smoke.sh examples/tauri` and `examples/tools/run-tauri-webdriver-smoke.sh examples/tauri-wasix` now provide repeatable Linux GUI smoke coverage using `tauri-driver`, `WebKitWebDriver`, and `xvfb-run`. +- Electron GUI automation is not yet repeatable: direct `xvfb-run electron --no-sandbox ...` launches, but Playwright `_electron.launch` timed out before returning an Electron application and a CDP probe with `--remote-debugging-port` did not expose `/json/version`. Next pass should add a small Electron test-driver IPC hook or use WebdriverIO Electron service before marking the GUI e2e gate complete. diff --git a/examples/README.md b/examples/README.md index 3633e975..e9a3c964 100644 --- a/examples/README.md +++ b/examples/README.md @@ -56,5 +56,17 @@ The native examples run a SQL backup smoke through `pg_dump` during startup. The WASIX examples run `dump_sql("--schema-only")` and a non-interactive `psql` `SELECT 1` smoke during startup. +Run Tauri GUI smoke tests through WebDriver on Linux: + +```sh +examples/tools/run-tauri-webdriver-smoke.sh examples/tauri +examples/tools/run-tauri-webdriver-smoke.sh examples/tauri-wasix +``` + +The WebDriver smoke builds the selected Tauri app in debug mode, launches it +through `tauri-driver`, creates a todo through the real UI, toggles it done, and +asserts the done filter. It expects `WebKitWebDriver`; on Debian/Ubuntu install +`webkit2gtk-driver`. In headless environments it uses `xvfb-run` when present. + On Linux, SwiftPM artifacts are staged for inspection and skipped for registry publish when `swift` is not installed. diff --git a/examples/tools/check-examples.sh b/examples/tools/check-examples.sh index 2c87eb8d..d2c78ae2 100755 --- a/examples/tools/check-examples.sh +++ b/examples/tools/check-examples.sh @@ -73,6 +73,10 @@ require_text "src/bindings/wasix-rust/moon.yml" '^ example-check:$' require_text "src/bindings/wasix-rust/moon.yml" 'tags: \["examples", "quality", "ci-wasm-regression"\]' require_file "examples/tools/with-local-registries.sh" +require_file "examples/tools/run-tauri-webdriver-smoke.sh" +require_file "examples/tools/tauri-webdriver-smoke.mjs" +require_text "examples/tools/run-tauri-webdriver-smoke.sh" 'cargo install tauri-driver --locked --version 2\.0\.6' +require_text "examples/tools/tauri-webdriver-smoke.mjs" 'tauri webdriver todo smoke passed' for example in tauri tauri-wasix electron electron-wasix; do require_file "examples/$example/package.json" require_file "examples/$example/README.md" diff --git a/examples/tools/run-tauri-webdriver-smoke.sh b/examples/tools/run-tauri-webdriver-smoke.sh new file mode 100755 index 00000000..8d046b0e --- /dev/null +++ b/examples/tools/run-tauri-webdriver-smoke.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +set -euo pipefail + +root="$(git rev-parse --show-toplevel 2>/dev/null)" || { + echo "must run inside the Oliphaunt git checkout" >&2 + exit 1 +} +cd "$root" + +fail() { + echo "run-tauri-webdriver-smoke.sh: $*" >&2 + exit 1 +} + +app_dir="${1:-}" +if [ -z "$app_dir" ]; then + fail "usage: examples/tools/run-tauri-webdriver-smoke.sh " +fi +if [ ! -f "$app_dir/src-tauri/Cargo.toml" ]; then + fail "$app_dir does not look like a Tauri example directory" +fi + +command -v node >/dev/null 2>&1 || fail "missing node" +command -v pnpm >/dev/null 2>&1 || fail "missing pnpm" +command -v WebKitWebDriver >/dev/null 2>&1 || + fail "missing WebKitWebDriver; install webkit2gtk-driver on Debian/Ubuntu" + +driver="$root/target/e2e-tools/bin/tauri-driver" +if [ ! -x "$driver" ]; then + cargo install tauri-driver --locked --version 2.0.6 --root "$root/target/e2e-tools" +fi + +examples/tools/with-local-registries.sh pnpm --dir "$app_dir" tauri build --debug + +package_name="$( + awk -F'"' ' + $0 ~ /^\[package\]/ { in_package = 1; next } + $0 ~ /^\[/ && $0 !~ /^\[package\]/ { in_package = 0 } + in_package && $1 ~ /^name = / { print $2; exit } + ' "$app_dir/src-tauri/Cargo.toml" +)" +if [ -z "$package_name" ]; then + fail "could not read package name from $app_dir/src-tauri/Cargo.toml" +fi +application="$root/$app_dir/src-tauri/target/debug/$package_name" +if [ ! -x "$application" ]; then + fail "missing built Tauri application: $application" +fi + +run_smoke=( + env + "OLIPHAUNT_E2E_TAURI_DRIVER=$driver" + "OLIPHAUNT_E2E_TAURI_APP=$application" + examples/tools/with-local-registries.sh + node + "$root/examples/tools/tauri-webdriver-smoke.mjs" +) + +if command -v xvfb-run >/dev/null 2>&1; then + xvfb-run -a "${run_smoke[@]}" +else + "${run_smoke[@]}" +fi diff --git a/examples/tools/tauri-webdriver-smoke.mjs b/examples/tools/tauri-webdriver-smoke.mjs new file mode 100755 index 00000000..6bb1d456 --- /dev/null +++ b/examples/tools/tauri-webdriver-smoke.mjs @@ -0,0 +1,189 @@ +#!/usr/bin/env node +import { spawn } from "node:child_process"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { createServer } from "node:net"; + +const driverPath = process.env.OLIPHAUNT_E2E_TAURI_DRIVER; +const application = process.env.OLIPHAUNT_E2E_TAURI_APP; + +if (!driverPath || !application) { + throw new Error("OLIPHAUNT_E2E_TAURI_DRIVER and OLIPHAUNT_E2E_TAURI_APP are required"); +} + +const webdriverElement = "element-6066-11e4-a52e-4f735466cecf"; +const port = await freePort(); +const nativePort = await freePort(); +const appData = mkdtempSync(join(tmpdir(), "oliphaunt-tauri-e2e-")); +let driver; +let sessionId; + +try { + driver = spawn(driverPath, ["--port", String(port), "--native-port", String(nativePort)], { + env: { + ...process.env, + XDG_DATA_HOME: appData, + XDG_CONFIG_HOME: appData, + XDG_CACHE_HOME: appData, + }, + detached: process.platform !== "win32", + stdio: ["ignore", "pipe", "pipe"], + }); + driver.stdout.on("data", (chunk) => process.stdout.write(chunk)); + driver.stderr.on("data", (chunk) => process.stderr.write(chunk)); + + await waitForDriver(port); + const session = await request(port, "POST", "/session", { + capabilities: { + alwaysMatch: { + "tauri:options": { application }, + }, + }, + }); + sessionId = session.sessionId ?? session.value?.sessionId; + if (!sessionId) { + throw new Error(`session response did not include sessionId: ${JSON.stringify(session)}`); + } + + await setValue(port, sessionId, "#title", `Ship Tauri e2e ${Date.now()}`); + await setValue(port, sessionId, "#notes", "created by raw WebDriver"); + await setValue(port, sessionId, "#area", "examples"); + await setValue(port, sessionId, "#context", "local registry"); + await click(port, sessionId, "button[type='submit']"); + await waitForText(port, sessionId, "article.todo", "created by raw WebDriver", 60_000); + await click(port, sessionId, "article.todo input[type='checkbox']"); + await click(port, sessionId, "[data-status='done']"); + await waitForText(port, sessionId, "article.todo.done", "created by raw WebDriver", 60_000); + console.log("tauri webdriver todo smoke passed"); +} finally { + if (sessionId) { + await request(port, "DELETE", `/session/${sessionId}`).catch(() => undefined); + } + await stopDriver(driver); + rmSync(appData, { recursive: true, force: true, maxRetries: 5, retryDelay: 250 }); +} + +async function stopDriver(driver) { + if (!driver || driver.exitCode !== null || driver.signalCode !== null) return; + const exited = new Promise((resolve) => driver.once("exit", resolve)); + try { + if (process.platform !== "win32" && driver.pid) { + process.kill(-driver.pid, "SIGTERM"); + } else { + driver.kill("SIGTERM"); + } + } catch { + return; + } + const stopped = await Promise.race([exited.then(() => true), sleep(3_000).then(() => false)]); + if (stopped) return; + try { + if (process.platform !== "win32" && driver.pid) { + process.kill(-driver.pid, "SIGKILL"); + } else { + driver.kill("SIGKILL"); + } + } catch { + // Process already exited. + } +} + +async function setValue(port, sessionId, selector, value) { + const id = await element(port, sessionId, selector); + await request(port, "POST", `/session/${sessionId}/element/${id}/clear`, {}); + await request(port, "POST", `/session/${sessionId}/element/${id}/value`, { + text: value, + value: [...value], + }); +} + +async function click(port, sessionId, selector) { + const id = await element(port, sessionId, selector); + await request(port, "POST", `/session/${sessionId}/element/${id}/click`, {}); +} + +async function element(port, sessionId, selector) { + const response = await request(port, "POST", `/session/${sessionId}/element`, { + using: "css selector", + value: selector, + }); + const value = response.value ?? response; + const id = value[webdriverElement] ?? value.ELEMENT; + if (!id) { + throw new Error(`element ${selector} response missing element id: ${JSON.stringify(response)}`); + } + return id; +} + +async function waitForText(port, sessionId, selector, expected, timeoutMs) { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const text = await execute( + port, + sessionId, + `return document.querySelector(${JSON.stringify(selector)})?.textContent ?? "";`, + ); + if (String(text).includes(expected)) return; + await sleep(500); + } + const body = await execute(port, sessionId, "return document.body?.innerText ?? '';"); + throw new Error(`timed out waiting for ${selector} to contain ${expected}; body was: ${body}`); +} + +async function execute(port, sessionId, script) { + const response = await request(port, "POST", `/session/${sessionId}/execute/sync`, { + script, + args: [], + }); + return response.value; +} + +async function request(port, method, path, body) { + const response = await fetch(`http://127.0.0.1:${port}${path}`, { + method, + headers: { "content-type": "application/json" }, + body: body === undefined ? undefined : JSON.stringify(body), + }); + const text = await response.text(); + const json = text ? JSON.parse(text) : {}; + if (!response.ok) { + throw new Error(`${method} ${path} failed ${response.status}: ${text}`); + } + if (json.value?.error) { + throw new Error(`${method} ${path} failed: ${JSON.stringify(json.value)}`); + } + return json; +} + +async function waitForDriver(port) { + const deadline = Date.now() + 30_000; + while (Date.now() < deadline) { + try { + await request(port, "GET", "/status"); + return; + } catch { + await sleep(250); + } + } + throw new Error("timed out waiting for tauri-driver"); +} + +function freePort() { + return new Promise((resolve, reject) => { + const server = createServer(); + server.listen(0, "127.0.0.1", () => { + const address = server.address(); + if (address && typeof address === "object") { + server.close(() => resolve(address.port)); + } else { + server.close(() => reject(new Error("could not allocate a local port"))); + } + }); + server.on("error", reject); + }); +} + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} From 17fc36ba2caf45f1ab452f272aeddf3625c34aee Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 00:26:16 +0000 Subject: [PATCH 017/137] fix: validate local registry examples --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 8 +- .../examples-ci-release-validation.md | 13 +- examples/README.md | 12 ++ examples/electron-wasix/src/main-process.ts | 33 +++- .../src/{preload.ts => preload.cts} | 0 examples/electron-wasix/tsconfig.main.json | 2 +- examples/electron-wasix/vite.config.ts | 1 + examples/electron/src/main-process.ts | 33 +++- .../electron/src/{preload.ts => preload.cts} | 0 examples/electron/tsconfig.main.json | 2 +- examples/electron/vite.config.ts | 1 + examples/tools/check-examples.sh | 5 + examples/tools/electron-driver-smoke.mjs | 128 ++++++++++++++ examples/tools/electron-test-driver.mjs | 113 +++++++++++++ examples/tools/run-electron-driver-smoke.sh | 46 +++++ examples/tools/with-local-registries.sh | 6 + .../js/src/__tests__/runtime-modes.test.ts | 38 ++++- src/sdks/js/src/runtime/server.ts | 73 +++++++- tools/release/local_registry_publish.py | 158 +++++++++++++++++- tools/release/release.py | 91 ++++++++-- 20 files changed, 715 insertions(+), 48 deletions(-) rename examples/electron-wasix/src/{preload.ts => preload.cts} (100%) rename examples/electron/src/{preload.ts => preload.cts} (100%) create mode 100755 examples/tools/electron-driver-smoke.mjs create mode 100755 examples/tools/electron-test-driver.mjs create mode 100755 examples/tools/run-electron-driver-smoke.sh diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index d65e947d..8b3068c6 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -11,7 +11,7 @@ review production pipelines, then normalize implementation details. - [x] Confirm WASIX example smoke tests install `oliphaunt-wasix-tools` from the local registry and exercise the split tools path with `pg_dump`. - [x] Confirm native and WASIX examples resolve local published runtime, tools, and extension crates with locked installs. - [x] Add direct `psql` execution coverage when the WASIX SDK exposes a public tool runner for it. -- [ ] Run GUI-level e2e for Electron and Tauri examples, or document the exact missing host capabilities if a full GUI run is blocked. +- [x] Run GUI-level e2e for Electron and Tauri examples, or document the exact missing host capabilities if a full GUI run is blocked. - [ ] Verify CI and release workflows produce exactly the package surfaces expected for each registry. ## Priority 1: Example App Validation @@ -19,7 +19,7 @@ review production pipelines, then normalize implementation details. - [x] Inventory every example app, its package managers, local-registry dependencies, and runtime/tool/extension paths. - [ ] Ensure each native example uses `oliphaunt-tools-*` from the local registry when it exercises standalone tools. - [x] Ensure each WASIX example uses `oliphaunt-wasix-tools` from the local registry and does not rely on path-only tool assets. -- [ ] Add example-app smoke commands that model the desired developer experience and can run on Linux CI. +- [x] Add example-app smoke commands that model the desired developer experience and can run on Linux CI. - [x] Check frontend build/test flows for the Electron, Electron WASIX, Tauri, Tauri WASIX, and WASIX vanilla examples. ## Priority 2: CI and Release Shape @@ -50,8 +50,8 @@ review production pipelines, then normalize implementation details. ## Current Notes -- The latest pushed commit is `3bf731e fix: split wasix tools behind feature`. +- The active branch contains the split native/WASIX tools package work and the example GUI smoke coverage. - Local-registry WASIX smoke coverage proves `pg_dump` through the SDK `dump_sql` path and `psql` through `PsqlOptions::command("SELECT 1")`. - Local-registry Cargo payload inspection confirmed `liboliphaunt-native-linux-x64-gnu-part-*` contains `initdb`, `pg_ctl`, and `postgres` only under `runtime/bin`, while `oliphaunt-tools-linux-x64-gnu-part-*` contains only `pg_dump` and `psql` there. - `examples/tools/run-tauri-webdriver-smoke.sh examples/tauri` and `examples/tools/run-tauri-webdriver-smoke.sh examples/tauri-wasix` now provide repeatable Linux GUI smoke coverage using `tauri-driver`, `WebKitWebDriver`, and `xvfb-run`. -- Electron GUI automation is not yet repeatable: direct `xvfb-run electron --no-sandbox ...` launches, but Playwright `_electron.launch` timed out before returning an Electron application and a CDP probe with `--remote-debugging-port` did not expose `/json/version`. Next pass should add a small Electron test-driver IPC hook or use WebdriverIO Electron service before marking the GUI e2e gate complete. +- `examples/tools/run-electron-driver-smoke.sh examples/electron` and `examples/tools/run-electron-driver-smoke.sh examples/electron-wasix` now provide repeatable Linux GUI smoke coverage using the packaged Electron binary, an IPC test-driver hook, and `xvfb-run` when present. diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index 04c5b1bb..179480b1 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -15,17 +15,17 @@ the release/tooling surface after the runtime tool crate split. - `oliphaunt-wasix-tools` - host WASIX AOT and tools-AOT crates - selected WASIX extension crates and extension-AOT crates -- [ ] Publish local npm packages to Verdaccio for root desktop examples. +- [x] Publish local npm packages to Verdaccio for root desktop examples. - [x] Update root examples so their manifests model the registry install path: - native Tauri explicitly resolves the native tools artifact crate - WASIX examples explicitly resolve the WASIX tools and tools-AOT artifact crates - product-local WASIX example no longer uses path dependencies -- [ ] Exercise tool paths in example code, not only in dependency manifests: +- [x] Exercise tool paths in example code, not only in dependency manifests: - native example should execute a flow that requires packaged `pg_dump` - WASIX example should execute a flow that requires packaged `pg_dump` - WASIX example should compile with `psql` available from `oliphaunt-wasix-tools` - [x] Run `examples/tools/with-local-registries.sh` installs/builds for each root example. -- [ ] Run native and WASIX app smoke flows where available. +- [x] Run native and WASIX app smoke flows where available. ## P1: CI and Release Shape @@ -96,6 +96,13 @@ the release/tooling surface after the runtime tool crate split. for native Tauri, Electron WASIX, Tauri WASIX, and the nested WASIX SQLx Tauri example. The WASIX example lockfiles now pin the new `oliphaunt-wasix-tools` and `oliphaunt-wasix-tools-aot-*` registry packages. +- Electron GUI smoke checks passed through + `examples/tools/run-electron-driver-smoke.sh examples/electron` and + `examples/tools/run-electron-driver-smoke.sh examples/electron-wasix`. + Native Electron exercises the published `@oliphaunt/liboliphaunt-*`, + `@oliphaunt/tools-*`, and extension packages through `@oliphaunt/ts`; WASIX + Electron exercises the local Cargo registry sidecar with WASIX tools and + extension crates. - Release and asset guards passed for `xtask assets check --strict-generated`, `check_consumer_shape.py`, and `check_artifact_targets.py`. Native tools are modeled as derived registry package targets from the native runtime release diff --git a/examples/README.md b/examples/README.md index e9a3c964..308432ee 100644 --- a/examples/README.md +++ b/examples/README.md @@ -68,5 +68,17 @@ through `tauri-driver`, creates a todo through the real UI, toggles it done, and asserts the done filter. It expects `WebKitWebDriver`; on Debian/Ubuntu install `webkit2gtk-driver`. In headless environments it uses `xvfb-run` when present. +Run Electron GUI smoke tests through the IPC test driver on Linux: + +```sh +examples/tools/run-electron-driver-smoke.sh examples/electron +examples/tools/run-electron-driver-smoke.sh examples/electron-wasix +``` + +The Electron smoke builds the selected app, launches the packaged Electron +binary with a test-driver IPC channel, creates a todo through the real renderer, +toggles it done, and asserts the done filter. In headless environments it uses +`xvfb-run` when present. + On Linux, SwiftPM artifacts are staged for inspection and skipped for registry publish when `swift` is not installed. diff --git a/examples/electron-wasix/src/main-process.ts b/examples/electron-wasix/src/main-process.ts index 05cd13d9..b62be467 100644 --- a/examples/electron-wasix/src/main-process.ts +++ b/examples/electron-wasix/src/main-process.ts @@ -1,19 +1,23 @@ import { app, BrowserWindow, ipcMain } from "electron"; import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { closeStore, createTodo, deleteTodo, listTodos, toggleTodo } from "./todos.js"; import type { CreateTodoInput, StatusFilter } from "./types.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); +if (process.env.OLIPHAUNT_ELECTRON_E2E_DRIVER) { + process.send?.({ event: "main-start", cwd: process.cwd(), send: typeof process.send }); +} + function createWindow() { const window = new BrowserWindow({ width: 1100, height: 760, title: "Oliphaunt Electron WASIX Todo", webPreferences: { - preload: join(__dirname, "preload.js"), + preload: join(__dirname, "preload.cjs"), contextIsolation: true, nodeIntegration: false, }, @@ -25,6 +29,16 @@ function createWindow() { } else { void window.loadFile(join(__dirname, "../renderer/index.html")); } + return window; +} + +async function installTestDriver(window: BrowserWindow) { + if (!process.env.OLIPHAUNT_ELECTRON_E2E_DRIVER) return; + console.error("Installing Electron todo e2e driver"); + const driver = await import( + pathToFileURL(join(process.cwd(), "../tools/electron-test-driver.mjs")).href + ); + driver.installElectronTodoTestDriver({ app, window, close: closeStore }); } ipcMain.handle( @@ -37,8 +51,19 @@ ipcMain.handle("todos:create", (_event, input: CreateTodoInput) => ipcMain.handle("todos:toggle", (_event, id: number) => toggleTodo(app.getPath("userData"), id)); ipcMain.handle("todos:delete", (_event, id: number) => deleteTodo(app.getPath("userData"), id)); -await app.whenReady(); -createWindow(); +process.env.OLIPHAUNT_ELECTRON_E2E_DRIVER && + process.send?.({ event: "before-when-ready" }); +void app + .whenReady() + .then(async () => { + process.env.OLIPHAUNT_ELECTRON_E2E_DRIVER && + process.send?.({ event: "after-when-ready" }); + await installTestDriver(createWindow()); + }) + .catch((error) => { + console.error(error); + app.exit(1); + }); app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); diff --git a/examples/electron-wasix/src/preload.ts b/examples/electron-wasix/src/preload.cts similarity index 100% rename from examples/electron-wasix/src/preload.ts rename to examples/electron-wasix/src/preload.cts diff --git a/examples/electron-wasix/tsconfig.main.json b/examples/electron-wasix/tsconfig.main.json index 42c05c32..4e16471e 100644 --- a/examples/electron-wasix/tsconfig.main.json +++ b/examples/electron-wasix/tsconfig.main.json @@ -10,5 +10,5 @@ "skipLibCheck": true, "sourceMap": true }, - "include": ["src/main-process.ts", "src/preload.ts", "src/sidecar.ts", "src/todos.ts", "src/types.ts"] + "include": ["src/main-process.ts", "src/preload.cts", "src/sidecar.ts", "src/todos.ts", "src/types.ts"] } diff --git a/examples/electron-wasix/vite.config.ts b/examples/electron-wasix/vite.config.ts index 41b47a44..27152134 100644 --- a/examples/electron-wasix/vite.config.ts +++ b/examples/electron-wasix/vite.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from "vite"; export default defineConfig({ root: ".", + base: "./", clearScreen: false, server: { port: 5175, diff --git a/examples/electron/src/main-process.ts b/examples/electron/src/main-process.ts index 5c1e9dc6..6d608529 100644 --- a/examples/electron/src/main-process.ts +++ b/examples/electron/src/main-process.ts @@ -1,19 +1,23 @@ import { app, BrowserWindow, ipcMain } from "electron"; import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { closeDatabase, createTodo, deleteTodo, listTodos, toggleTodo } from "./todos.js"; import type { CreateTodoInput, StatusFilter } from "./types.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); +if (process.env.OLIPHAUNT_ELECTRON_E2E_DRIVER) { + process.send?.({ event: "main-start", cwd: process.cwd(), send: typeof process.send }); +} + function createWindow() { const window = new BrowserWindow({ width: 1100, height: 760, title: "Oliphaunt Electron Todo", webPreferences: { - preload: join(__dirname, "preload.js"), + preload: join(__dirname, "preload.cjs"), contextIsolation: true, nodeIntegration: false, }, @@ -25,6 +29,16 @@ function createWindow() { } else { void window.loadFile(join(__dirname, "../renderer/index.html")); } + return window; +} + +async function installTestDriver(window: BrowserWindow) { + if (!process.env.OLIPHAUNT_ELECTRON_E2E_DRIVER) return; + console.error("Installing Electron todo e2e driver"); + const driver = await import( + pathToFileURL(join(process.cwd(), "../tools/electron-test-driver.mjs")).href + ); + driver.installElectronTodoTestDriver({ app, window, close: closeDatabase }); } ipcMain.handle( @@ -37,8 +51,19 @@ ipcMain.handle("todos:create", (_event, input: CreateTodoInput) => ipcMain.handle("todos:toggle", (_event, id: number) => toggleTodo(app.getPath("userData"), id)); ipcMain.handle("todos:delete", (_event, id: number) => deleteTodo(app.getPath("userData"), id)); -await app.whenReady(); -createWindow(); +process.env.OLIPHAUNT_ELECTRON_E2E_DRIVER && + process.send?.({ event: "before-when-ready" }); +void app + .whenReady() + .then(async () => { + process.env.OLIPHAUNT_ELECTRON_E2E_DRIVER && + process.send?.({ event: "after-when-ready" }); + await installTestDriver(createWindow()); + }) + .catch((error) => { + console.error(error); + app.exit(1); + }); app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); diff --git a/examples/electron/src/preload.ts b/examples/electron/src/preload.cts similarity index 100% rename from examples/electron/src/preload.ts rename to examples/electron/src/preload.cts diff --git a/examples/electron/tsconfig.main.json b/examples/electron/tsconfig.main.json index 739fb30d..5d26d54a 100644 --- a/examples/electron/tsconfig.main.json +++ b/examples/electron/tsconfig.main.json @@ -10,5 +10,5 @@ "skipLibCheck": true, "sourceMap": true }, - "include": ["src/main-process.ts", "src/preload.ts", "src/todos.ts", "src/types.ts"] + "include": ["src/main-process.ts", "src/preload.cts", "src/todos.ts", "src/types.ts"] } diff --git a/examples/electron/vite.config.ts b/examples/electron/vite.config.ts index d09839f1..f822c83a 100644 --- a/examples/electron/vite.config.ts +++ b/examples/electron/vite.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from "vite"; export default defineConfig({ root: ".", + base: "./", clearScreen: false, server: { port: 5174, diff --git a/examples/tools/check-examples.sh b/examples/tools/check-examples.sh index d2c78ae2..6d6e5141 100755 --- a/examples/tools/check-examples.sh +++ b/examples/tools/check-examples.sh @@ -75,8 +75,13 @@ require_text "src/bindings/wasix-rust/moon.yml" 'tags: \["examples", "quality", require_file "examples/tools/with-local-registries.sh" require_file "examples/tools/run-tauri-webdriver-smoke.sh" require_file "examples/tools/tauri-webdriver-smoke.mjs" +require_file "examples/tools/run-electron-driver-smoke.sh" +require_file "examples/tools/electron-driver-smoke.mjs" +require_file "examples/tools/electron-test-driver.mjs" require_text "examples/tools/run-tauri-webdriver-smoke.sh" 'cargo install tauri-driver --locked --version 2\.0\.6' require_text "examples/tools/tauri-webdriver-smoke.mjs" 'tauri webdriver todo smoke passed' +require_text "examples/tools/electron-driver-smoke.mjs" 'electron driver todo smoke passed' +require_text "examples/tools/electron-test-driver.mjs" 'installElectronTodoTestDriver' for example in tauri tauri-wasix electron electron-wasix; do require_file "examples/$example/package.json" require_file "examples/$example/README.md" diff --git a/examples/tools/electron-driver-smoke.mjs b/examples/tools/electron-driver-smoke.mjs new file mode 100755 index 00000000..37927325 --- /dev/null +++ b/examples/tools/electron-driver-smoke.mjs @@ -0,0 +1,128 @@ +#!/usr/bin/env node +import { spawn } from "node:child_process"; +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const electron = process.env.OLIPHAUNT_E2E_ELECTRON; +const appDir = process.env.OLIPHAUNT_E2E_ELECTRON_APP; +if (!electron || !appDir) { + throw new Error("OLIPHAUNT_E2E_ELECTRON and OLIPHAUNT_E2E_ELECTRON_APP are required"); +} + +const userData = mkdtempSync(join(tmpdir(), "oliphaunt-electron-e2e-")); +const child = spawn( + electron, + [ + "--no-sandbox", + `--user-data-dir=${userData}`, + "dist/main/main-process.js", + ], + { + cwd: appDir, + env: { + ...process.env, + OLIPHAUNT_ELECTRON_E2E_DRIVER: "1", + }, + stdio: ["ignore", "pipe", "pipe", "ipc"], + }, +); + +let nextId = 1; +let driverReady = false; +const pending = new Map(); + +child.stdout.on("data", (chunk) => process.stdout.write(chunk)); +child.stderr.on("data", (chunk) => process.stderr.write(chunk)); +child.on("message", (message) => { + if (!message || typeof message !== "object") return; + if (message.event && process.env.OLIPHAUNT_E2E_DEBUG) { + console.error(`electron event ${JSON.stringify(message)}`); + } + if (message.event === "driver-ready") { + driverReady = true; + pending.get(0)?.resolve("driver-ready"); + pending.delete(0); + return; + } + const id = message.id; + if (typeof id !== "number") return; + const request = pending.get(id); + if (!request) return; + pending.delete(id); + if (message.ok) { + request.resolve(message.value); + } else { + request.reject(new Error(message.error || `Electron driver command ${id} failed`)); + } +}); + +try { + await waitForDriverReady(); + await rpc("ready", 30_000); + await rpc("runTodoSmoke", 150_000); + console.log("electron driver todo smoke passed"); + await rpc("shutdown", 30_000).catch(() => undefined); + await waitForExit(10_000); +} finally { + await stopChild(); + rmSync(userData, { recursive: true, force: true, maxRetries: 5, retryDelay: 250 }); +} + +function waitForDriverReady() { + if (driverReady) return Promise.resolve("driver-ready"); + return withTimeout( + new Promise((resolve, reject) => { + pending.set(0, { resolve, reject }); + child.once("exit", (code, signal) => { + pending.delete(0); + reject(new Error(`Electron exited before driver was ready: ${code ?? signal}`)); + }); + }), + 30_000, + "timed out waiting for Electron test driver", + ); +} + +function rpc(command, timeoutMs) { + if (!child.connected) { + throw new Error("Electron IPC channel is not connected"); + } + const id = nextId++; + const result = withTimeout( + new Promise((resolve, reject) => { + pending.set(id, { resolve, reject }); + }), + timeoutMs, + `timed out waiting for Electron driver command ${command}`, + ).finally(() => pending.delete(id)); + child.send({ id, command }); + return result; +} + +function waitForExit(timeoutMs) { + if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve(); + return withTimeout( + new Promise((resolve) => child.once("exit", resolve)), + timeoutMs, + "timed out waiting for Electron to exit", + ); +} + +async function stopChild() { + if (child.exitCode !== null || child.signalCode !== null) return; + child.kill("SIGTERM"); + try { + await waitForExit(3_000); + } catch { + child.kill("SIGKILL"); + } +} + +function withTimeout(promise, timeoutMs, message) { + let timer; + const timeout = new Promise((_resolve, reject) => { + timer = setTimeout(() => reject(new Error(message)), timeoutMs); + }); + return Promise.race([promise, timeout]).finally(() => clearTimeout(timer)); +} diff --git a/examples/tools/electron-test-driver.mjs b/examples/tools/electron-test-driver.mjs new file mode 100755 index 00000000..6c8cad83 --- /dev/null +++ b/examples/tools/electron-test-driver.mjs @@ -0,0 +1,113 @@ +const webdriverTimeoutMs = 90_000; + +export function installElectronTodoTestDriver({ app, window, close }) { + if (!process.send) { + throw new Error("Electron test driver requires an IPC stdio channel"); + } + + process.on("message", async (message) => { + if (!message || typeof message !== "object") return; + const { id, command } = message; + if (typeof id !== "number" || typeof command !== "string") return; + + try { + let value; + if (command === "ready") { + await waitForWindowLoad(window); + value = window.webContents.getURL(); + } else if (command === "runTodoSmoke") { + await waitForWindowLoad(window); + value = await runTodoSmoke(window); + } else if (command === "shutdown") { + await close(); + process.send?.({ id, ok: true, value: "closed" }); + app.exit(0); + return; + } else { + throw new Error(`unknown Electron test driver command: ${command}`); + } + process.send?.({ id, ok: true, value }); + } catch (error) { + process.send?.({ + id, + ok: false, + error: error instanceof Error ? error.stack || error.message : String(error), + }); + } + }); + + process.send({ event: "driver-ready" }); +} + +async function waitForWindowLoad(window) { + if (!window.webContents.isLoading()) return; + await new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error("timed out waiting for window load")), 30_000); + window.webContents.once("did-finish-load", () => { + clearTimeout(timer); + resolve(); + }); + window.webContents.once("did-fail-load", (_event, _code, description) => { + clearTimeout(timer); + reject(new Error(`window failed to load: ${description}`)); + }); + }); +} + +async function runTodoSmoke(window) { + return window.webContents.executeJavaScript( + `(${rendererTodoSmoke.toString()})(${JSON.stringify(webdriverTimeoutMs)})`, + true, + ); +} + +async function rendererTodoSmoke(timeoutMs) { + const title = `Ship Electron e2e ${Date.now()}`; + const notes = "created by Electron test driver"; + + const required = (selector) => { + const element = document.querySelector(selector); + if (!element) throw new Error(`missing selector: ${selector}`); + return element; + }; + const setValue = (selector, value) => { + const element = required(selector); + element.value = value; + element.dispatchEvent(new Event("input", { bubbles: true })); + element.dispatchEvent(new Event("change", { bubbles: true })); + }; + const waitFor = async (predicate, label) => { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (predicate()) return; + await new Promise((resolve) => setTimeout(resolve, 250)); + } + throw new Error(`timed out waiting for ${label}; body was: ${document.body.innerText}`); + }; + + await waitFor(() => Boolean(window.todos), "preload todo API"); + await waitFor( + () => required("#todo-list").textContent?.includes("No todos match the current filter."), + "initial todo list", + ); + + setValue("#title", title); + setValue("#notes", notes); + setValue("#area", "examples"); + setValue("#context", "local registry"); + setValue("#priority", "1"); + required("button[type='submit']").click(); + + await waitFor(() => document.body.innerText.includes(title), "created todo title"); + await waitFor(() => document.body.innerText.includes(notes), "created todo notes"); + + required("article.todo input[type='checkbox']").click(); + await waitFor(() => required("#open-count").textContent?.includes("0 open"), "todo toggle"); + required("[data-status='done']").click(); + await waitFor( + () => document.querySelector("article.todo.done")?.textContent?.includes(notes) === true, + "done todo filter", + ); + + return document.body.innerText; +} diff --git a/examples/tools/run-electron-driver-smoke.sh b/examples/tools/run-electron-driver-smoke.sh new file mode 100755 index 00000000..1880509d --- /dev/null +++ b/examples/tools/run-electron-driver-smoke.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +set -euo pipefail + +root="$(git rev-parse --show-toplevel 2>/dev/null)" || { + echo "must run inside the Oliphaunt git checkout" >&2 + exit 1 +} +cd "$root" + +fail() { + echo "run-electron-driver-smoke.sh: $*" >&2 + exit 1 +} + +app_dir="${1:-}" +if [ -z "$app_dir" ]; then + fail "usage: examples/tools/run-electron-driver-smoke.sh " +fi +if [ ! -f "$app_dir/package.json" ] || [ ! -f "$app_dir/src/main-process.ts" ]; then + fail "$app_dir does not look like an Electron example directory" +fi + +command -v node >/dev/null 2>&1 || fail "missing node" +command -v pnpm >/dev/null 2>&1 || fail "missing pnpm" + +electron="$root/node_modules/electron/dist/electron" +if [ ! -x "$electron" ]; then + fail "missing Electron executable at $electron; run pnpm install" +fi + +examples/tools/with-local-registries.sh pnpm --dir "$app_dir" build + +run_smoke=( + env + "OLIPHAUNT_E2E_ELECTRON=$electron" + "OLIPHAUNT_E2E_ELECTRON_APP=$root/$app_dir" + examples/tools/with-local-registries.sh + node + "$root/examples/tools/electron-driver-smoke.mjs" +) + +if command -v xvfb-run >/dev/null 2>&1; then + xvfb-run -a "${run_smoke[@]}" +else + "${run_smoke[@]}" +fi diff --git a/examples/tools/with-local-registries.sh b/examples/tools/with-local-registries.sh index 0d195ef6..0cee5124 100755 --- a/examples/tools/with-local-registries.sh +++ b/examples/tools/with-local-registries.sh @@ -22,5 +22,11 @@ fi # Local Verdaccio publishes packages during the example setup; allow those # freshly-published local packages without changing the workspace policy. export PNPM_CONFIG_MINIMUM_RELEASE_AGE=0 +# Local release validation republishes the same package versions into Verdaccio. +# Keep examples off the repository lockfile and global pnpm store so they resolve +# the current local registry bytes instead of stale same-version artifacts. +export PNPM_CONFIG_LOCKFILE=false +export PNPM_CONFIG_STORE_DIR="$root/target/local-registries/pnpm-store" +export PNPM_CONFIG_PREFER_OFFLINE=false exec "$@" diff --git a/src/sdks/js/src/__tests__/runtime-modes.test.ts b/src/sdks/js/src/__tests__/runtime-modes.test.ts index ae8a3528..fe5e8827 100644 --- a/src/sdks/js/src/__tests__/runtime-modes.test.ts +++ b/src/sdks/js/src/__tests__/runtime-modes.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { test } from 'vitest'; -import { chmod, mkdtemp, rm, writeFile } from 'node:fs/promises'; -import { join } from 'node:path'; +import { chmod, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { delimiter, join } from 'node:path'; import { tmpdir } from 'node:os'; import type { NormalizedOpenConfig } from '../config.js'; @@ -25,6 +25,7 @@ import { } from '../runtime/pgwire.js'; import { createServerRuntimeBinding, + nativeServerRuntimeEnv, serverCapabilities, serverConnectionString, serverModeSupport, @@ -37,6 +38,7 @@ async function main(): Promise { testServerCapabilitiesAndConnectionString(); await testServerSupportReportsMissingExecutable(); await testServerStartupTimeoutEnvIsValidatedBeforeProcessSetup(); + await testServerRuntimeEnvIncludesPackagedLibraryDir(); testPgwireStartupCancelAndBackendKeyFrames(); await testNodeAdapterUtilities(); } @@ -193,6 +195,38 @@ async function testServerStartupTimeoutEnvIsValidatedBeforeProcessSetup(): Promi } } +async function testServerRuntimeEnvIncludesPackagedLibraryDir(): Promise { + const root = await mkdtemp(join(tmpdir(), 'oliphaunt-js-server-env-')); + const runtime = join(root, 'runtime'); + const toolDirectory = join(runtime, 'bin'); + const libDirectory = join(runtime, 'lib'); + const envName = + process.platform === 'darwin' + ? 'DYLD_LIBRARY_PATH' + : process.platform === 'win32' + ? 'PATH' + : 'LD_LIBRARY_PATH'; + const previous = process.env[envName]; + try { + await mkdir(toolDirectory, { recursive: true }); + await mkdir(libDirectory, { recursive: true }); + process.env[envName] = 'existing-runtime-path'; + const env = await nativeServerRuntimeEnv(toolDirectory); + const expectedPrefix = + process.platform === 'win32' + ? [toolDirectory, libDirectory, 'existing-runtime-path'] + : [libDirectory, 'existing-runtime-path']; + assert.equal(env[envName], expectedPrefix.join(delimiter)); + } finally { + if (previous === undefined) { + delete process.env[envName]; + } else { + process.env[envName] = previous; + } + await rm(root, { recursive: true, force: true }); + } +} + function normalizedTestConfig( root: string, overrides: Partial = {}, diff --git a/src/sdks/js/src/runtime/server.ts b/src/sdks/js/src/runtime/server.ts index 70e217eb..a840e629 100644 --- a/src/sdks/js/src/runtime/server.ts +++ b/src/sdks/js/src/runtime/server.ts @@ -1,7 +1,7 @@ import { spawn } from 'node:child_process'; import { chmod, mkdir, mkdtemp, stat } from 'node:fs/promises'; import { tmpdir } from 'node:os'; -import { dirname, join } from 'node:path'; +import { delimiter, dirname, join } from 'node:path'; import { createServer } from 'node:net'; import type { NormalizedOpenConfig } from '../config.js'; @@ -17,7 +17,11 @@ import { import { createPhysicalArchive } from './physical-archive.js'; import { PostgresWireClient } from './pgwire.js'; import type { RuntimeBinding, RuntimeHandle } from './types.js'; -import { resolveNodeIcuDataDirectory, resolveNodeNativeInstall } from '../native/assets-node.js'; +import { + materializeNodeExtensionInstall, + resolveNodeIcuDataDirectory, + resolveNodeNativeInstall, +} from '../native/assets-node.js'; const SERVER_HOST = '127.0.0.1'; const SERVER_STARTUP_TIMEOUT_MS_ENV = 'OLIPHAUNT_SERVER_STARTUP_TIMEOUT_MS'; @@ -193,6 +197,7 @@ async function openServer(config: NormalizedOpenConfig): Promise { const tools = await resolveServerTools({ serverExecutable: config.serverExecutable, serverToolDirectory: config.serverToolDirectory, + extensions: config.extensions, }); const executable = tools.executable; const toolDirectory = tools.toolDirectory; @@ -368,6 +373,7 @@ function serverStartupTimeoutMs(): number { async function resolveServerTools(options: { serverExecutable?: string; serverToolDirectory?: string; + extensions?: readonly string[]; }): Promise<{ executable: string; toolDirectory: string }> { const candidates = [ options.serverExecutable, @@ -387,7 +393,10 @@ async function resolveServerTools(options: { if (options.serverExecutable !== undefined || options.serverToolDirectory !== undefined) { throw new Error('set serverExecutable, serverToolDirectory, or OLIPHAUNT_POSTGRES'); } - const install = await resolveNodeNativeInstall(); + const install = await materializeNodeExtensionInstall( + await resolveNodeNativeInstall(), + options.extensions ?? [], + ); if (install.runtimeDirectory !== undefined) { const toolDirectory = join(install.runtimeDirectory, 'bin'); const executable = join(toolDirectory, executableName('postgres')); @@ -431,14 +440,66 @@ async function isDirectory(path: string): Promise { } } -async function nativeServerRuntimeEnv(toolDirectory: string): Promise> { +export async function nativeServerRuntimeEnv(toolDirectory: string): Promise> { const runtimeDirectory = dirname(toolDirectory); + const env: Record = {}; + const dynamicLibraryDirs = await nativeDynamicLibraryDirs(runtimeDirectory); + const dynamicLibraryEnv = prependEnvPaths( + nativeDynamicLibraryEnvName(), + dynamicLibraryDirs, + process.env[nativeDynamicLibraryEnvName()], + ); + if (dynamicLibraryEnv !== undefined) { + env[nativeDynamicLibraryEnvName()] = dynamicLibraryEnv; + } + const icuData = join(runtimeDirectory, 'share/icu'); if (await isDirectory(icuData)) { - return { ICU_DATA: icuData }; + env.ICU_DATA = icuData; + return env; } const packagedIcuData = await resolveNodeIcuDataDirectory(); - return packagedIcuData === undefined ? {} : { ICU_DATA: packagedIcuData }; + if (packagedIcuData !== undefined) { + env.ICU_DATA = packagedIcuData; + } + return env; +} + +function nativeDynamicLibraryEnvName(): 'DYLD_LIBRARY_PATH' | 'LD_LIBRARY_PATH' | 'PATH' { + if (process.platform === 'darwin') { + return 'DYLD_LIBRARY_PATH'; + } + if (process.platform === 'win32') { + return 'PATH'; + } + return 'LD_LIBRARY_PATH'; +} + +async function nativeDynamicLibraryDirs(runtimeDirectory: string): Promise { + const dirs: string[] = []; + if (process.platform === 'win32') { + const bin = join(runtimeDirectory, 'bin'); + if (await isDirectory(bin)) { + dirs.push(bin); + } + } + const lib = join(runtimeDirectory, 'lib'); + if (await isDirectory(lib)) { + dirs.push(lib); + } + return dirs; +} + +function prependEnvPaths( + name: string, + paths: string[], + existing: string | undefined, +): string | undefined { + const entries = paths.filter((path) => path.length > 0); + if (existing !== undefined && existing.length > 0) { + entries.push(existing); + } + return entries.length === 0 ? undefined : entries.join(delimiter); } async function pickPort(): Promise { diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index 3bea9022..19bdcb61 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -63,10 +63,6 @@ "liboliphaunt-native-release-assets-macos-arm64", "liboliphaunt-native-release-assets-windows-x64-msvc", "liboliphaunt-wasix-extension-artifacts-wasix-portable", - "liboliphaunt-wasix-extension-aot-linux-arm64-gnu", - "liboliphaunt-wasix-extension-aot-linux-x64-gnu", - "liboliphaunt-wasix-extension-aot-macos-arm64", - "liboliphaunt-wasix-extension-aot-windows-x64-msvc", "liboliphaunt-wasix-release-assets", "liboliphaunt-wasix-runtime-aot-linux-arm64-gnu", "liboliphaunt-wasix-runtime-aot-linux-x64-gnu", @@ -243,6 +239,44 @@ def discover_files(roots: list[Path], suffixes: tuple[str, ...]) -> list[Path]: return sorted(set(files)) +def file_sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as file: + for chunk in iter(lambda: file.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def copy_release_assets( + roots: list[Path], + destination: Path, + patterns: tuple[str, ...], +) -> list[Path]: + candidates: list[Path] = [] + for root in roots: + if not root.is_dir(): + continue + for pattern in patterns: + candidates.extend(path for path in root.rglob(pattern) if path.is_file()) + if not candidates: + return [] + + shutil.rmtree(destination, ignore_errors=True) + destination.mkdir(parents=True, exist_ok=True) + copied: list[Path] = [] + for source in sorted(candidates): + target = destination / source.name + if target.is_file(): + if file_sha256(target) != file_sha256(source): + raise RuntimeError( + f"conflicting release asset {source.name}: {rel(target)} and {rel(source)} differ" + ) + continue + shutil.copy2(source, target) + copied.append(target) + return copied + + def host_npm_target() -> str | None: machine = host_platform.machine().lower() if sys.platform == "linux" and machine in {"x86_64", "amd64"}: @@ -815,6 +849,7 @@ def write_verdaccio_config(root: Path, port: int) -> tuple[Path, bool]: text = "\n".join( [ f"storage: {storage}", + "max_body_size: 100mb", "auth:", " htpasswd:", f" file: {root / 'htpasswd'}", @@ -1030,8 +1065,118 @@ def npm_package_exists( return completed.returncode == 0 and completed.stdout.strip() == version +def npm_tarball_priority(path: Path, registry_root: Path) -> tuple[int, float, str]: + resolved = path.resolve() + priority = 20 + for root, value in [ + (ROOT / "target" / "release" / "npm-packages", 100), + (ROOT / "target" / "sdk-artifacts", 90), + (registry_root / "npm-extension-packages", 80), + (DEFAULT_ARTIFACT_ROOT, 30), + ]: + try: + resolved.relative_to(root.resolve()) + except ValueError: + continue + priority = value + break + try: + modified = path.stat().st_mtime + except OSError: + modified = 0 + return priority, modified, str(path) + + +def select_npm_tarballs(tarballs: list[Path], registry_root: Path, result: SurfaceResult) -> list[Path]: + selected: dict[tuple[str, str], Path] = {} + unidentified: list[Path] = [] + for tarball in tarballs: + identity = npm_package_identity(tarball) + if identity is None: + unidentified.append(tarball) + continue + current = selected.get(identity) + if current is None: + selected[identity] = tarball + continue + if npm_tarball_priority(tarball, registry_root) > npm_tarball_priority(current, registry_root): + selected[identity] = tarball + result.staged.append( + f"preferred {rel(tarball)} over {rel(current)} for {identity[0]}@{identity[1]}" + ) + else: + result.staged.append( + f"preferred {rel(current)} over {rel(tarball)} for {identity[0]}@{identity[1]}" + ) + return sorted([*unidentified, *selected.values()]) + + +def stage_release_asset_npm_packages( + roots: list[Path], + registry_root: Path, + dry_run: bool, + result: SurfaceResult, +) -> list[Path]: + if dry_run: + result.staged.append("dry-run generated liboliphaunt and broker npm artifact packages") + return [] + + sys.path.insert(0, str(ROOT / "tools" / "release")) + import release # type: ignore + + tarballs: list[Path] = [] + target = host_npm_target() + targets = {target} if target is not None else None + + lib_asset_dir = ROOT / "target" / "liboliphaunt" / "release-assets" + lib_version = release.current_product_version("liboliphaunt-native") + copied_lib = copy_release_assets(roots, lib_asset_dir, (f"liboliphaunt-{lib_version}-*",)) + if copied_lib or release.liboliphaunt_release_assets_ready(): + if copied_lib: + result.staged.append(f"staged {len(copied_lib)} liboliphaunt release asset(s)") + tarballs.extend( + path + for _package_name, path in release.liboliphaunt_npm_tarballs( + lib_version, + validate_assets=False, + targets=targets, + include_icu=False, + ) + ) + else: + result.add_skip("no liboliphaunt release assets found for native npm artifact packages") + + broker_asset_dir = ROOT / "target" / "oliphaunt-broker" / "release-assets" + copied_broker = copy_release_assets( + roots, + broker_asset_dir, + ("oliphaunt-broker-*.tar.gz", "oliphaunt-broker-*.zip"), + ) + if copied_broker or any(broker_asset_dir.glob("oliphaunt-broker-*.tar.gz")) or any( + broker_asset_dir.glob("oliphaunt-broker-*.zip") + ): + if copied_broker: + result.staged.append(f"staged {len(copied_broker)} broker release asset(s)") + version = release.current_product_version("oliphaunt-broker") + tarballs.extend( + path + for _package_name, path in release.broker_npm_tarballs( + version, + validate_assets=False, + targets=targets, + ) + ) + else: + result.add_skip("no broker release assets found for broker npm artifact packages") + + if tarballs: + result.staged.append(f"generated {len(tarballs)} release-asset npm package(s)") + return tarballs + + def publish_npm(roots: list[Path], registry_root: Path, dry_run: bool, strict: bool, port: int) -> SurfaceResult: result = SurfaceResult("npm") + generated_tarballs = stage_release_asset_npm_packages(roots, registry_root, dry_run, result) extension_target = host_npm_target() extension_tarball_root = stage_extension_npm_packages( roots, @@ -1042,7 +1187,7 @@ def publish_npm(roots: list[Path], registry_root: Path, dry_run: bool, strict: b ) if extension_tarball_root is not None: roots = [*roots, extension_tarball_root] - tarballs = discover_files(roots, (".tgz",)) + tarballs = select_npm_tarballs([*discover_files(roots, (".tgz",)), *generated_tarballs], registry_root, result) if not tarballs: result.add_skip("no npm .tgz artifacts found") if strict: @@ -1089,6 +1234,9 @@ def publish_npm(roots: list[Path], registry_root: Path, dry_run: bool, strict: b command.extend(["--userconfig", str(npmrc)]) run(command) result.published.append(rel(tarball)) + pnpm_store = registry_root / "pnpm-store" + shutil.rmtree(pnpm_store, ignore_errors=True) + result.staged.append(f"cleared local pnpm store {rel(pnpm_store)}") return result diff --git a/tools/release/release.py b/tools/release/release.py index ee6a8b6b..ecde065d 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -2182,8 +2182,14 @@ def npm_pack_and_validate( return tarball -def stage_liboliphaunt_npm_payloads(version: str) -> dict[str, Path]: - ensure_liboliphaunt_release_assets() +def stage_liboliphaunt_npm_payloads( + version: str, + *, + validate_assets: bool = True, + targets: set[str] | None = None, +) -> dict[str, Path]: + if validate_assets: + ensure_liboliphaunt_release_assets() asset_dir = liboliphaunt_release_asset_dir() packages = artifact_npm_package_targets( "liboliphaunt-native", @@ -2193,6 +2199,8 @@ def stage_liboliphaunt_npm_payloads(version: str) -> dict[str, Path]: ) stages: dict[str, Path] = {} for package_name, package_dir, target in packages: + if targets is not None and target.target not in targets: + continue if target.library_relative_path is None: fail(f"{target.id} must declare library_relative_path for npm artifact package publication") stage = stage_npm_package_descriptor( @@ -2232,8 +2240,14 @@ def remove_native_tools_from_runtime(stage: Path, target: str) -> None: optimize_native_runtime_payload.prune_empty_dirs(runtime_dir) -def stage_liboliphaunt_tools_npm_payloads(version: str) -> dict[str, Path]: - ensure_liboliphaunt_release_assets() +def stage_liboliphaunt_tools_npm_payloads( + version: str, + *, + validate_assets: bool = True, + targets: set[str] | None = None, +) -> dict[str, Path]: + if validate_assets: + ensure_liboliphaunt_release_assets() asset_dir = liboliphaunt_release_asset_dir() packages = artifact_npm_package_targets( "liboliphaunt-native", @@ -2243,6 +2257,8 @@ def stage_liboliphaunt_tools_npm_payloads(version: str) -> dict[str, Path]: ) stages: dict[str, Path] = {} for package_name, package_dir, target in packages: + if targets is not None and target.target not in targets: + continue stage = stage_npm_package_descriptor( package_name, package_dir, @@ -2262,8 +2278,9 @@ def stage_liboliphaunt_tools_npm_payloads(version: str) -> dict[str, Path]: return stages -def stage_liboliphaunt_icu_npm_payload(version: str) -> Path: - ensure_liboliphaunt_release_assets() +def stage_liboliphaunt_icu_npm_payload(version: str, *, validate_assets: bool = True) -> Path: + if validate_assets: + ensure_liboliphaunt_release_assets() package_name = "@oliphaunt/icu" stage = stage_npm_package_descriptor( package_name, @@ -2280,8 +2297,14 @@ def stage_liboliphaunt_icu_npm_payload(version: str) -> Path: return stage -def stage_broker_npm_payloads(version: str) -> dict[str, Path]: - ensure_broker_release_assets() +def stage_broker_npm_payloads( + version: str, + *, + validate_assets: bool = True, + targets: set[str] | None = None, +) -> dict[str, Path]: + if validate_assets: + ensure_broker_release_assets() asset_dir = ROOT / "target" / "oliphaunt-broker" / "release-assets" packages = artifact_npm_package_targets( "oliphaunt-broker", @@ -2291,6 +2314,8 @@ def stage_broker_npm_payloads(version: str) -> dict[str, Path]: ) stages: dict[str, Path] = {} for package_name, package_dir, target in packages: + if targets is not None and target.target not in targets: + continue if target.executable_relative_path is None: fail(f"{target.id} must declare executable_relative_path for npm artifact package publication") stage = stage_npm_package_descriptor( @@ -2341,16 +2366,32 @@ def node_direct_optional_npm_tarballs(version: str) -> list[tuple[str, Path]]: return tarballs -def liboliphaunt_npm_tarballs(version: str) -> list[tuple[str, Path]]: +def liboliphaunt_npm_tarballs( + version: str, + *, + validate_assets: bool = True, + targets: set[str] | None = None, + include_icu: bool = True, +) -> list[tuple[str, Path]]: packages: list[tuple[str, Path]] = [] - stages = stage_liboliphaunt_npm_payloads(version) - tools_stages = stage_liboliphaunt_tools_npm_payloads(version) + stages = stage_liboliphaunt_npm_payloads( + version, + validate_assets=validate_assets, + targets=targets, + ) + tools_stages = stage_liboliphaunt_tools_npm_payloads( + version, + validate_assets=validate_assets, + targets=targets, + ) for package_name, _package_dir, target in artifact_npm_package_targets( "liboliphaunt-native", "native-runtime", "typescript-native-direct", ROOT / "src/runtimes/liboliphaunt/native/packages", ): + if targets is not None and target.target not in targets: + continue if target.library_relative_path is None: fail(f"{target.id} must declare library_relative_path for npm artifact package publication") runtime_members = optimize_native_runtime_payload.required_runtime_member_paths( @@ -2374,6 +2415,8 @@ def liboliphaunt_npm_tarballs(version: str) -> list[tuple[str, Path]]: "typescript-native-direct", ROOT / "src/runtimes/liboliphaunt/native/tools-packages", ): + if targets is not None and target.target not in targets: + continue runtime_members = optimize_native_runtime_payload.required_tools_member_paths( target.target, prefix="package/runtime/bin", @@ -2387,23 +2430,35 @@ def liboliphaunt_npm_tarballs(version: str) -> list[tuple[str, Path]]: target=target.target, ) packages.append((package_name, tarball)) - icu_package = "@oliphaunt/icu" - icu_stage = stage_liboliphaunt_icu_npm_payload(version) - icu_tarball = pnpm_pack_for_npm_publish(icu_stage) - packed_icu_package_contains(icu_tarball, icu_package, version) - packages.append((icu_package, icu_tarball)) + if include_icu: + icu_package = "@oliphaunt/icu" + icu_stage = stage_liboliphaunt_icu_npm_payload(version, validate_assets=validate_assets) + icu_tarball = pnpm_pack_for_npm_publish(icu_stage) + packed_icu_package_contains(icu_tarball, icu_package, version) + packages.append((icu_package, icu_tarball)) return packages -def broker_npm_tarballs(version: str) -> list[tuple[str, Path]]: +def broker_npm_tarballs( + version: str, + *, + validate_assets: bool = True, + targets: set[str] | None = None, +) -> list[tuple[str, Path]]: packages: list[tuple[str, Path]] = [] - stages = stage_broker_npm_payloads(version) + stages = stage_broker_npm_payloads( + version, + validate_assets=validate_assets, + targets=targets, + ) for package_name, _package_dir, target in artifact_npm_package_targets( "oliphaunt-broker", "broker-helper", "typescript-broker", ROOT / "src/runtimes/broker/packages", ): + if targets is not None and target.target not in targets: + continue if target.executable_relative_path is None: fail(f"{target.id} must declare executable_relative_path for npm artifact package publication") required_members = [f"package/{target.executable_relative_path}"] From 302d3d6bc08aa25b387c6f3a87b573304fc57fea Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 00:33:44 +0000 Subject: [PATCH 018/137] chore: sync release derived files --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 7 ++ .../examples-ci-release-validation.md | 7 ++ ...2026-06-07-transitional-catalog-smoke.json | 2 +- .../generated/docs/extension-evidence.json | 80 +++++++++---------- .../assets/generated/asset-inputs.sha256 | 2 +- 5 files changed, 56 insertions(+), 42 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 8b3068c6..aa402045 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -30,6 +30,7 @@ review production pipelines, then normalize implementation details. - [x] Verify WASIX runtime payloads contain `postgres`, `initdb`; WASIX tools payloads contain `pg_dump`, `psql`, not `pg_ctl`. - [ ] Verify extension packages and runtime tools are published and installed from registries idiomatically. - [ ] Identify duplicated release metadata or package target matrices that can be safely collapsed. +- [x] Keep release-derived files synchronized after the split tool package changes. ## Priority 3: SDK Consistency @@ -37,6 +38,8 @@ review production pipelines, then normalize implementation details. - [ ] Ensure SDKs exercise the same control flows for runtime setup, extension selection, artifact validation, and tool access. - [ ] Identify feature gaps where one SDK exposes a runtime/tool/extension capability differently from the others. - [ ] Add or update parity checks where a documented invariant is not machine-checked. +- [ ] Decide and document whether JS Deno native flows should support packaged native tools and extensions, or fail clearly when those features are requested. +- [ ] Harden Rust native runtime cache validation so split client tools are validated when a flow expects `pg_dump` or `psql`. ## Priority 4: Cleanup and Tooling @@ -55,3 +58,7 @@ review production pipelines, then normalize implementation details. - Local-registry Cargo payload inspection confirmed `liboliphaunt-native-linux-x64-gnu-part-*` contains `initdb`, `pg_ctl`, and `postgres` only under `runtime/bin`, while `oliphaunt-tools-linux-x64-gnu-part-*` contains only `pg_dump` and `psql` there. - `examples/tools/run-tauri-webdriver-smoke.sh examples/tauri` and `examples/tools/run-tauri-webdriver-smoke.sh examples/tauri-wasix` now provide repeatable Linux GUI smoke coverage using `tauri-driver`, `WebKitWebDriver`, and `xvfb-run`. - `examples/tools/run-electron-driver-smoke.sh examples/electron` and `examples/tools/run-electron-driver-smoke.sh examples/electron-wasix` now provide repeatable Linux GUI smoke coverage using the packaged Electron binary, an IPC test-driver hook, and `xvfb-run` when present. +- `tools/release/sync_release_pr.py --check`, `check_release_metadata.py`, `check_consumer_shape.py`, `check_artifact_targets.py`, and the full `tools/release/release.py check` pass after refreshing the WASIX asset input fingerprint and extension evidence digests. +- Subagent CI/release audit mapped the split native runtime/tools crates and WASIX runtime/tools/AOT/tools-AOT crates to their release generation and publication paths. Remaining CI work is to validate Linux workflow lanes locally rather than relying only on static release checks. +- Subagent SDK audit flagged Deno native asset resolution, ICU behavior, mobile static-extension readiness, and Rust native split-tool validation as the next parity risks to resolve or explicitly document. +- Local workflow tooling is available: `act` is installed at v0.2.89, which matches the latest upstream release published on 2026-06-01, Docker is available, and `act -l` parses the CI, Release, and mobile E2E workflow graph. Full Linux lane execution is still pending. diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index 179480b1..f67d027d 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -107,3 +107,10 @@ the release/tooling surface after the runtime tool crate split. `check_consumer_shape.py`, and `check_artifact_targets.py`. Native tools are modeled as derived registry package targets from the native runtime release archive, not as standalone GitHub release assets. +- Release PR derived-file sync now passes after refreshing the WASIX asset input + fingerprint and extension evidence source digests. `tools/release/release.py + check` passes through policy, release-please config, artifact targets, + release metadata, and consumer-shape readiness for the current package set. +- Local GitHub Actions discovery is ready on Linux: `act` v0.2.89, Docker, and + `gh` are installed, and `act -l` parses the CI, Release, and mobile E2E + workflows. Full local lane execution remains a separate validation step. diff --git a/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json b/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json index 144e6d0f..fff6f368 100644 --- a/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json +++ b/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json @@ -514,7 +514,7 @@ } ], "schema": "oliphaunt-extension-evidence-v1", - "sourceDigest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1", + "sourceDigest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2", "sourceDigestInputs": [ "src/postgres/versions/18/source.toml", "src/extensions/catalog/extensions.promoted.toml", diff --git a/src/extensions/generated/docs/extension-evidence.json b/src/extensions/generated/docs/extension-evidence.json index 3118ad62..2a9ff33b 100644 --- a/src/extensions/generated/docs/extension-evidence.json +++ b/src/extensions/generated/docs/extension-evidence.json @@ -20,7 +20,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -56,7 +56,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -92,7 +92,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -128,7 +128,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -164,7 +164,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -200,7 +200,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -236,7 +236,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -272,7 +272,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -308,7 +308,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -344,7 +344,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -380,7 +380,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -416,7 +416,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -452,7 +452,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -488,7 +488,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -524,7 +524,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -560,7 +560,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -596,7 +596,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -632,7 +632,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -668,7 +668,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -704,7 +704,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -740,7 +740,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -776,7 +776,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -812,7 +812,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -848,7 +848,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -884,7 +884,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -920,7 +920,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -956,7 +956,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -992,7 +992,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -1028,7 +1028,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -1064,7 +1064,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -1100,7 +1100,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -1136,7 +1136,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -1172,7 +1172,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -1208,7 +1208,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -1244,7 +1244,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -1280,7 +1280,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -1316,7 +1316,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -1352,7 +1352,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -1388,7 +1388,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1" + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" } ], "platform-targets": [ @@ -1420,7 +1420,7 @@ "path": "src/extensions/evidence/runs" } ], - "source-digest": "sha256:fc269b26f5977fce5a586b962b5053e89467a91d7442ac3acef143e8d293a0b1", + "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2", "source-digest-inputs": [ "src/postgres/versions/18/source.toml", "src/extensions/catalog/extensions.promoted.toml", diff --git a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 index 8c2f53f0..da666ac5 100644 --- a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 +++ b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 @@ -1 +1 @@ -72c65d6de94b4529d2a8e852b10da2de355d86c7ba0ddb9379064b86c794bd84 +a8c6baa38746d74c214b91497fcd6353745c110ece823da080969d3bb39aaf9d From 80f12cac13f72f45961a0a300b9dd042116193e0 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 00:38:53 +0000 Subject: [PATCH 019/137] fix: clarify deno native extension handling --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 6 +- src/sdks/js/ARCHITECTURE.md | 4 + src/sdks/js/README.md | 16 ++- .../js/src/__tests__/native-bindings.test.ts | 128 ++++++++++++++++++ src/sdks/js/src/native/assets-deno.ts | 18 ++- src/sdks/js/src/native/deno.ts | 5 + 6 files changed, 168 insertions(+), 9 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index aa402045..b81a1ebb 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -38,8 +38,8 @@ review production pipelines, then normalize implementation details. - [ ] Ensure SDKs exercise the same control flows for runtime setup, extension selection, artifact validation, and tool access. - [ ] Identify feature gaps where one SDK exposes a runtime/tool/extension capability differently from the others. - [ ] Add or update parity checks where a documented invariant is not machine-checked. -- [ ] Decide and document whether JS Deno native flows should support packaged native tools and extensions, or fail clearly when those features are requested. -- [ ] Harden Rust native runtime cache validation so split client tools are validated when a flow expects `pg_dump` or `psql`. +- [x] Decide and document whether JS Deno native flows should support packaged native tools and extensions, or fail clearly when those features are requested. +- [x] Harden Rust native runtime cache validation so split client tools are validated when a flow expects `pg_dump` or `psql`. ## Priority 4: Cleanup and Tooling @@ -62,3 +62,5 @@ review production pipelines, then normalize implementation details. - Subagent CI/release audit mapped the split native runtime/tools crates and WASIX runtime/tools/AOT/tools-AOT crates to their release generation and publication paths. Remaining CI work is to validate Linux workflow lanes locally rather than relying only on static release checks. - Subagent SDK audit flagged Deno native asset resolution, ICU behavior, mobile static-extension readiness, and Rust native split-tool validation as the next parity risks to resolve or explicitly document. - Local workflow tooling is available: `act` is installed at v0.2.89, which matches the latest upstream release published on 2026-06-01, Docker is available, and `act -l` parses the CI, Release, and mobile E2E workflow graph. Full Linux lane execution is still pending. +- JS Deno direct mode now resolves packaged ICU for explicit-library installs when running inside Deno, and rejects package-managed extension requests without an explicit prepared `runtimeDirectory`. Node and Bun remain the registry-managed extension materialization paths. +- Rust native runtime cache validation already requires both split client tools, with `runtime_validation_requires_split_tools` covering a missing `pg_dump` cache entry. diff --git a/src/sdks/js/ARCHITECTURE.md b/src/sdks/js/ARCHITECTURE.md index 73a8d0ee..37381bbd 100644 --- a/src/sdks/js/ARCHITECTURE.md +++ b/src/sdks/js/ARCHITECTURE.md @@ -129,6 +129,10 @@ When `engine` is omitted, the default is consistent: direct adapter. Bun and Deno use built-in FFI. Node resolves the verified `oliphaunt-node-direct-*` Node-API adapter release asset and loads it without `postinstall`, node-gyp, Rust, Cargo, or third-party FFI packages; +- native direct extension package materialization is shared by Node and Bun. + Deno direct mode may use extensions only with an explicit prepared + `runtimeDirectory`; package-managed Deno extension materialization must remain + a clear unsupported-feature error until it has a real resolver/cache path; - `nativeBroker`: available when the broker helper resolves from an explicit override, package-adjacent executable, or verified Rust SDK release asset, the matching `liboliphaunt` install resolves, and the current runtime can spawn diff --git a/src/sdks/js/README.md b/src/sdks/js/README.md index 504deb50..905bef04 100644 --- a/src/sdks/js/README.md +++ b/src/sdks/js/README.md @@ -64,8 +64,8 @@ and set the runtime ICU data environment before opening liboliphaunt. Do not add `@oliphaunt/icu` for applications that do not use ICU collations. JSR remains protocol/query-only and does not expose native runtime or ICU packages. -PostgreSQL extensions follow the same registry-driven model. Applications add -the extension meta package for every extension they pass to +PostgreSQL extensions follow the same registry-driven model in Node and Bun. +Applications add the extension meta package for every extension they pass to `Oliphaunt.open({ extensions })`; that package installs the matching target payload as an optional dependency. @@ -73,10 +73,14 @@ payload as an optional dependency. pnpm add @oliphaunt/extension-hstore @oliphaunt/extension-pg-trgm ``` -At startup the SDK resolves the current platform package, validates that it was -built for the same liboliphaunt version as `@oliphaunt/ts`, and materializes a -runtime tree containing the selected extension SQL files and native modules. -Do not copy extension release assets into the application bundle by hand. +At startup the Node and Bun bindings resolve the current platform package, +validate that it was built for the same liboliphaunt version as +`@oliphaunt/ts`, and materialize a runtime tree containing the selected +extension SQL files and native modules. Deno nativeDirect does not yet +materialize extension packages automatically; pass an explicit +`runtimeDirectory` that already contains the selected extension assets, or use +Node/Bun for registry-managed extension resolution. Do not copy extension +release assets into the application bundle by hand. ## Compatibility diff --git a/src/sdks/js/src/__tests__/native-bindings.test.ts b/src/sdks/js/src/__tests__/native-bindings.test.ts index a4673e8a..5bea696f 100644 --- a/src/sdks/js/src/__tests__/native-bindings.test.ts +++ b/src/sdks/js/src/__tests__/native-bindings.test.ts @@ -6,6 +6,7 @@ import { tmpdir } from 'node:os'; import Oliphaunt, { createNodeNativeBinding, simpleQuery, type OliphauntClient } from '../index.js'; import { resolveDenoNativeInstall } from '../native/assets-deno.js'; +import { createDenoNativeBinding } from '../native/deno.js'; import { cString, OLIPHAUNT_CONFIG_SIZE, @@ -24,6 +25,7 @@ async function main(): Promise { testFfiLayoutPackingAndBounds(); await testNodeNativeBindingUsesExplicitAssetsAndAddon(); await testDenoAssetResolverHonorsExplicitPaths(); + await testDenoNativeBindingRejectsPackageManagedExtensions(); } function testIndexExportsDefaultClient(): void { @@ -229,6 +231,7 @@ async function testDenoAssetResolverHonorsExplicitPaths(): Promise { assert.deepEqual(await resolveDenoNativeInstall('/tmp/liboliphaunt.dylib'), { libraryPath: '/tmp/liboliphaunt.dylib', runtimeDirectory: '/tmp/oliphaunt-deno-runtime', + icuDataDirectory: undefined, }); await assert.rejects(async () => resolveDenoNativeInstall(), /only be used inside Deno/); } finally { @@ -240,6 +243,131 @@ async function testDenoAssetResolverHonorsExplicitPaths(): Promise { } } +async function testDenoNativeBindingRejectsPackageManagedExtensions(): Promise { + const previousDeno = (globalThis as { Deno?: unknown }).Deno; + const previousLibrary = process.env.LIBOLIPHAUNT_PATH; + const previousRuntime = process.env.OLIPHAUNT_RUNTIME_DIR; + const calls: string[] = []; + try { + process.env.LIBOLIPHAUNT_PATH = '/tmp/liboliphaunt-deno-test.so'; + delete process.env.OLIPHAUNT_RUNTIME_DIR; + (globalThis as { Deno?: unknown }).Deno = { + build: { os: 'linux', arch: 'x86_64' }, + async readTextFile(path: string | URL) { + const text = String(path); + if (text.includes('@oliphaunt/icu')) { + return JSON.stringify({ + name: '@oliphaunt/icu', + version: '0.1.0', + oliphaunt: { + product: 'oliphaunt-icu', + kind: 'icu-data', + target: 'portable', + dataRelativePath: 'share/icu', + }, + }); + } + return JSON.stringify({ + name: '@oliphaunt/ts', + oliphaunt: { + liboliphauntVersion: '0.1.0', + icuPackage: '@oliphaunt/icu', + icuVersion: '0.1.0', + }, + }); + }, + async stat() { + return { isDirectory: true }; + }, + async *readDir() { + yield { name: 'icudt76l.dat', isFile: true }; + }, + dlopen(path: string) { + calls.push(`dlopen:${path}`); + return { + symbols: { + oliphaunt_init() { + calls.push('init'); + return 0; + }, + oliphaunt_exec_protocol() { + return 0; + }, + oliphaunt_exec_simple_query() { + return 0; + }, + oliphaunt_backup() { + return 0; + }, + oliphaunt_restore() { + return 0; + }, + oliphaunt_cancel() { + return 0; + }, + oliphaunt_detach() { + return 0; + }, + oliphaunt_last_error() { + return null; + }, + oliphaunt_version() { + return null; + }, + oliphaunt_capabilities() { + return 0n; + }, + oliphaunt_free_response() {}, + }, + }; + }, + UnsafePointer: { + of() { + throw new Error('Deno extension guard should run before pointer packing'); + }, + value() { + return 0n; + }, + create() { + return null; + }, + }, + UnsafePointerView: class {}, + }; + + const binding = await createDenoNativeBinding(); + assert.throws( + () => + binding.open({ + pgdata: '/tmp/deno-pgdata', + runtimeDirectory: undefined, + username: 'postgres', + database: 'postgres', + extensions: ['hstore'], + startupArgs: [], + }), + /Deno nativeDirect does not automatically materialize extension packages/, + ); + assert.deepEqual(calls, ['dlopen:/tmp/liboliphaunt-deno-test.so']); + } finally { + if (previousDeno === undefined) { + delete (globalThis as { Deno?: unknown }).Deno; + } else { + (globalThis as { Deno?: unknown }).Deno = previousDeno; + } + if (previousLibrary === undefined) { + delete process.env.LIBOLIPHAUNT_PATH; + } else { + process.env.LIBOLIPHAUNT_PATH = previousLibrary; + } + if (previousRuntime === undefined) { + delete process.env.OLIPHAUNT_RUNTIME_DIR; + } else { + process.env.OLIPHAUNT_RUNTIME_DIR = previousRuntime; + } + } +} + test('native bindings', async () => { await main(); }); diff --git a/src/sdks/js/src/native/assets-deno.ts b/src/sdks/js/src/native/assets-deno.ts index 2e2e34cc..5606542c 100644 --- a/src/sdks/js/src/native/assets-deno.ts +++ b/src/sdks/js/src/native/assets-deno.ts @@ -53,9 +53,20 @@ export async function resolveDenoNativeInstall( ): Promise { const explicit = resolveExplicitLibraryPath(libraryPath); if (explicit !== undefined) { + const deno = optionalDenoRuntime(); + const versions = deno === undefined ? undefined : await packageVersions(deno); + const icuDataDirectory = + deno === undefined || versions === undefined + ? undefined + : await resolveDenoIcuDataDirectory( + deno, + versions.icuVersion, + versions.icuPackage, + ); return { libraryPath: explicit, runtimeDirectory: resolveExplicitRuntimeDirectory(), + icuDataDirectory, }; } @@ -235,9 +246,14 @@ async function requireIcuDataDirectory( } function denoRuntime(): DenoRuntime { - const deno = (globalThis as { Deno?: DenoRuntime }).Deno; + const deno = optionalDenoRuntime(); if (deno === undefined) { throw new Error('Deno native binding can only be used inside Deno'); } return deno; } + +function optionalDenoRuntime(): DenoRuntime | undefined { + const deno = (globalThis as { Deno?: DenoRuntime }).Deno; + return deno; +} diff --git a/src/sdks/js/src/native/deno.ts b/src/sdks/js/src/native/deno.ts index bf84802c..9c5f0cdb 100644 --- a/src/sdks/js/src/native/deno.ts +++ b/src/sdks/js/src/native/deno.ts @@ -75,6 +75,11 @@ export async function createDenoNativeBinding( return BigInt(symbols.oliphaunt_capabilities() as bigint | number); }, open(config: NativeOpenConfig): NativeHandle { + if (config.extensions.length > 0 && config.runtimeDirectory === undefined) { + throw new Error( + `Deno nativeDirect does not automatically materialize extension packages; pass runtimeDirectory with the selected extension assets or use Node/Bun nativeDirect. Selected extensions: ${config.extensions.join(', ')}`, + ); + } const packed = packConfigPointers(config, (value) => pointerOf(deno, value)); const out = new Uint8Array(8); const rc = symbols.oliphaunt_init(packed.config, out) as number; From db875c784efcee6354a3d8498de6772f8eac437e Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 01:02:31 +0000 Subject: [PATCH 020/137] fix: split wasix root tools payload --- ...2026-06-07-transitional-catalog-smoke.json | 2 +- .../generated/docs/extension-evidence.json | 80 +++++++++---------- .../assets/generated/asset-inputs.sha256 | 2 +- .../liboliphaunt/wasix/crates/assets/build.rs | 2 + tools/release/check_consumer_shape.py | 34 ++++++++ tools/release/release.py | 5 ++ tools/xtask/src/release_workspace.rs | 52 ++++++++++-- 7 files changed, 129 insertions(+), 48 deletions(-) diff --git a/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json b/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json index fff6f368..a5fd683f 100644 --- a/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json +++ b/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json @@ -514,7 +514,7 @@ } ], "schema": "oliphaunt-extension-evidence-v1", - "sourceDigest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2", + "sourceDigest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d", "sourceDigestInputs": [ "src/postgres/versions/18/source.toml", "src/extensions/catalog/extensions.promoted.toml", diff --git a/src/extensions/generated/docs/extension-evidence.json b/src/extensions/generated/docs/extension-evidence.json index 2a9ff33b..d5bf2252 100644 --- a/src/extensions/generated/docs/extension-evidence.json +++ b/src/extensions/generated/docs/extension-evidence.json @@ -20,7 +20,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -56,7 +56,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -92,7 +92,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -128,7 +128,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -164,7 +164,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -200,7 +200,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -236,7 +236,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -272,7 +272,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -308,7 +308,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -344,7 +344,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -380,7 +380,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -416,7 +416,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -452,7 +452,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -488,7 +488,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -524,7 +524,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -560,7 +560,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -596,7 +596,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -632,7 +632,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -668,7 +668,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -704,7 +704,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -740,7 +740,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -776,7 +776,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -812,7 +812,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -848,7 +848,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -884,7 +884,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -920,7 +920,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -956,7 +956,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -992,7 +992,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -1028,7 +1028,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -1064,7 +1064,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -1100,7 +1100,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -1136,7 +1136,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -1172,7 +1172,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -1208,7 +1208,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -1244,7 +1244,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -1280,7 +1280,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -1316,7 +1316,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -1352,7 +1352,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -1388,7 +1388,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2" + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" } ], "platform-targets": [ @@ -1420,7 +1420,7 @@ "path": "src/extensions/evidence/runs" } ], - "source-digest": "sha256:ede57e750b00c63cc81ab158a47e3fa640e8eadd4febdcf39e839440cfad46c2", + "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d", "source-digest-inputs": [ "src/postgres/versions/18/source.toml", "src/extensions/catalog/extensions.promoted.toml", diff --git a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 index da666ac5..d9b5af4c 100644 --- a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 +++ b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 @@ -1 +1 @@ -a8c6baa38746d74c214b91497fcd6353745c110ece823da080969d3bb39aaf9d +d4a6244ffbb81689848e4090f892c86a34a34453eead7e3eb2f24ef8e91befde diff --git a/src/runtimes/liboliphaunt/wasix/crates/assets/build.rs b/src/runtimes/liboliphaunt/wasix/crates/assets/build.rs index 717c8cee..a3199788 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/assets/build.rs +++ b/src/runtimes/liboliphaunt/wasix/crates/assets/build.rs @@ -580,6 +580,8 @@ fn write_core_manifest( .filter_map(extension_manifest_entry) .collect(), ); + manifest["pg-dump"] = serde_json::Value::Null; + manifest["psql"] = serde_json::Value::Null; let rendered = serde_json::to_string_pretty(&manifest).expect("serialize core WASIX asset manifest"); fs::write(destination, format!("{rendered}\n")).expect("write core WASIX asset manifest"); diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 725a364d..41709f5b 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -1542,6 +1542,9 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: asset_package = asset_manifest.get("package", {}) tools_manifest = read_toml("src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml") tools_package = tools_manifest.get("package", {}) + assets_build_source = read_text("src/runtimes/liboliphaunt/wasix/crates/assets/build.rs") + release_workspace_source = read_text("tools/xtask/src/release_workspace.rs") + tools_build_source = read_text("src/runtimes/liboliphaunt/wasix/crates/tools/build.rs") require( findings, product, @@ -1562,6 +1565,37 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: f"src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml package={tools_package!r}", severity="P0", ) + require( + findings, + product, + "wasix-root-tools-split", + 'manifest["pg-dump"] = serde_json::Value::Null;' in assets_build_source + and 'manifest["psql"] = serde_json::Value::Null;' in assets_build_source + and 'manifest["pg-dump"] = serde_json::Value::Null;' in release_workspace_source + and 'manifest["psql"] = serde_json::Value::Null;' in release_workspace_source + and "remove_split_wasix_tool_payload" in release_workspace_source + and "retain_split_tools" in release_workspace_source + and '"bin/initdb.wasix.wasm"' in assets_build_source + and '"bin/pg_dump.wasix.wasm"' not in assets_build_source + and '"bin/psql.wasix.wasm"' not in assets_build_source, + "WASIX root runtime asset crate must keep postgres/initdb assets only and null split tool manifest entries.", + [ + "src/runtimes/liboliphaunt/wasix/crates/assets/build.rs", + "tools/xtask/src/release_workspace.rs", + ], + severity="P0", + ) + require( + findings, + product, + "wasix-tools-payload", + '"bin/pg_dump.wasix.wasm"' in tools_build_source + and '"bin/psql.wasix.wasm"' in tools_build_source + and "pg_ctl" not in tools_build_source, + "WASIX tools asset crate must package pg_dump and psql only; pg_ctl is intentionally absent on WASIX.", + "src/runtimes/liboliphaunt/wasix/crates/tools/build.rs", + severity="P0", + ) require( findings, product, diff --git a/tools/release/release.py b/tools/release/release.py index ecde065d..18d70883 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -878,6 +878,11 @@ def validate_wasix_portable_release_asset(archive: Path) -> None: extensions = manifest.get("extensions") if extensions != []: fail(f"{archive.relative_to(ROOT)} asset manifest must contain an empty extensions array") + for tool_key in ["pg-dump", "psql"]: + if manifest.get(tool_key) is not None: + fail( + f"{archive.relative_to(ROOT)} asset manifest must not advertise split WASIX tool {tool_key}" + ) icu_sidecar_members = sorted( member for member in members diff --git a/tools/xtask/src/release_workspace.rs b/tools/xtask/src/release_workspace.rs index 1dea2584..95e97d2c 100644 --- a/tools/xtask/src/release_workspace.rs +++ b/tools/xtask/src/release_workspace.rs @@ -15,6 +15,7 @@ const RELEASE_RELEVANT_UNTRACKED_PATHS: &[&str] = &[ "src/runtimes/liboliphaunt/wasix", "tools/xtask", ]; +const SPLIT_WASIX_TOOL_PAYLOAD_FILES: &[&str] = &["bin/pg_dump.wasix.wasm", "bin/psql.wasix.wasm"]; pub(super) fn stage_release_workspace() -> Result<()> { let stage_root = Path::new(RELEASE_STAGE_DIR); @@ -37,8 +38,16 @@ pub(super) fn stage_release_workspace() -> Result<()> { ensure_file(&generated_assets.join("manifest.json"))?; let generated_manifest = read_asset_manifest_from(generated_assets)?; ensure_packaged_asset_matches_source_lane(&generated_manifest, DEFAULT_SOURCE_LANE)?; - copy_core_wasix_asset_payload(generated_assets, &workspace.join(ASSET_CRATE_PAYLOAD_DIR))?; - copy_core_wasix_asset_payload(generated_assets, &workspace.join(GENERATED_ASSETS_DIR))?; + copy_core_wasix_asset_payload( + generated_assets, + &workspace.join(ASSET_CRATE_PAYLOAD_DIR), + false, + )?; + copy_core_wasix_asset_payload( + generated_assets, + &workspace.join(GENERATED_ASSETS_DIR), + true, + )?; update_staged_root_asset_metadata(&workspace)?; for target in supported_aot_targets() { @@ -89,15 +98,32 @@ fn ensure_no_unexpected_untracked_release_files() -> Result<()> { Ok(()) } -fn copy_core_wasix_asset_payload(source: &Path, destination: &Path) -> Result<()> { +fn copy_core_wasix_asset_payload( + source: &Path, + destination: &Path, + retain_split_tools: bool, +) -> Result<()> { copy_dir_all(source, destination)?; let extension_dir = destination.join("extensions"); if extension_dir.exists() { fs::remove_dir_all(&extension_dir) .with_context(|| format!("remove {}", extension_dir.display()))?; } + if !retain_split_tools { + remove_split_wasix_tool_payload(destination)?; + } strip_core_asset_manifest_extensions(&destination.join("manifest.json"))?; - ensure_core_wasix_asset_payload(destination) + ensure_core_wasix_asset_payload(destination, retain_split_tools) +} + +fn remove_split_wasix_tool_payload(root: &Path) -> Result<()> { + for relative in SPLIT_WASIX_TOOL_PAYLOAD_FILES { + let path = root.join(relative); + if path.exists() { + fs::remove_file(&path).with_context(|| format!("remove {}", path.display()))?; + } + } + Ok(()) } fn strip_core_asset_manifest_extensions(manifest_path: &Path) -> Result<()> { @@ -115,6 +141,8 @@ fn strip_core_asset_manifest_extensions(manifest_path: &Path) -> Result<()> { ) })?; extensions.clear(); + manifest["pg-dump"] = serde_json::Value::Null; + manifest["psql"] = serde_json::Value::Null; let rendered = serde_json::to_string_pretty(&manifest).context("serialize core WASIX asset manifest")?; fs::write(manifest_path, format!("{rendered}\n")) @@ -122,8 +150,20 @@ fn strip_core_asset_manifest_extensions(manifest_path: &Path) -> Result<()> { Ok(()) } -fn ensure_core_wasix_asset_payload(root: &Path) -> Result<()> { +fn ensure_core_wasix_asset_payload(root: &Path, retain_split_tools: bool) -> Result<()> { ensure_file(&root.join("manifest.json"))?; + for relative in SPLIT_WASIX_TOOL_PAYLOAD_FILES { + let path = root.join(relative); + if retain_split_tools { + ensure_file(&path)?; + } else { + ensure!( + !path.exists(), + "core WASIX root crate payload must not contain split tool {}", + path.display() + ); + } + } for file in sorted_files(root)? { let relative = file .strip_prefix(root) @@ -334,7 +374,7 @@ fn package_release_portable_assets(output_dir: &Path, version: &str) -> Result

Date: Fri, 26 Jun 2026 01:30:24 +0000 Subject: [PATCH 021/137] test: validate registry backed examples --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 47 +++++++++++++++--- .../examples-ci-release-validation.md | 49 ++++++++++++++++++- examples/tools/check-examples.sh | 2 + .../wasix-rust/tools/check-examples.sh | 10 +++- 4 files changed, 100 insertions(+), 8 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index b81a1ebb..f292d83f 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -12,12 +12,12 @@ review production pipelines, then normalize implementation details. - [x] Confirm native and WASIX examples resolve local published runtime, tools, and extension crates with locked installs. - [x] Add direct `psql` execution coverage when the WASIX SDK exposes a public tool runner for it. - [x] Run GUI-level e2e for Electron and Tauri examples, or document the exact missing host capabilities if a full GUI run is blocked. -- [ ] Verify CI and release workflows produce exactly the package surfaces expected for each registry. +- [ ] Fix the CI/release metadata gaps found by the package-surface audit, then verify CI and release workflows produce exactly the package surfaces expected for each registry. ## Priority 1: Example App Validation - [x] Inventory every example app, its package managers, local-registry dependencies, and runtime/tool/extension paths. -- [ ] Ensure each native example uses `oliphaunt-tools-*` from the local registry when it exercises standalone tools. +- [x] Ensure each native example uses `oliphaunt-tools-*` from the local registry when it exercises standalone tools. - [x] Ensure each WASIX example uses `oliphaunt-wasix-tools` from the local registry and does not rely on path-only tool assets. - [x] Add example-app smoke commands that model the desired developer experience and can run on Linux CI. - [x] Check frontend build/test flows for the Electron, Electron WASIX, Tauri, Tauri WASIX, and WASIX vanilla examples. @@ -29,13 +29,20 @@ review production pipelines, then normalize implementation details. - [x] Verify native runtime payloads contain `postgres`, `initdb`, `pg_ctl`; native tools payloads contain `pg_dump`, `psql`. - [x] Verify WASIX runtime payloads contain `postgres`, `initdb`; WASIX tools payloads contain `pg_dump`, `psql`, not `pg_ctl`. - [ ] Verify extension packages and runtime tools are published and installed from registries idiomatically. -- [ ] Identify duplicated release metadata or package target matrices that can be safely collapsed. +- [ ] Make extension Maven registry surfaces explicit in extension metadata instead of silently appending them in release tooling. +- [ ] Remove or generate duplicated release target lists in workflow downloads, node-direct package dirs, artifact target checks, and release policy checks. +- [ ] Decide whether existing-tag release probes should become a uniform idempotency gate or be removed. - [x] Keep release-derived files synchronized after the split tool package changes. ## Priority 3: SDK Consistency - [ ] Compare SDK install paths and artifact resolution across Rust, JS, React Native, Kotlin, and Swift. - [ ] Ensure SDKs exercise the same control flows for runtime setup, extension selection, artifact validation, and tool access. +- [ ] Add Android split/local runtime validation so selected extensions must exist in the copied runtime tree before manifests are published. +- [ ] Align or explicitly document Deno native runtime/tools/extension resolution versus Node and Bun. +- [ ] Port stronger exact-extension artifact validation into the Android Gradle resolver. +- [ ] Pass mobile `sharedPreloadLibraries` through to startup arguments consistently. +- [ ] Add an explicit WASIX split-tools preflight path before first `pg_dump` or `psql` call. - [ ] Identify feature gaps where one SDK exposes a runtime/tool/extension capability differently from the others. - [ ] Add or update parity checks where a documented invariant is not machine-checked. - [x] Decide and document whether JS Deno native flows should support packaged native tools and extensions, or fail clearly when those features are requested. @@ -56,11 +63,39 @@ review production pipelines, then normalize implementation details. - The active branch contains the split native/WASIX tools package work and the example GUI smoke coverage. - Local-registry WASIX smoke coverage proves `pg_dump` through the SDK `dump_sql` path and `psql` through `PsqlOptions::command("SELECT 1")`. - Local-registry Cargo payload inspection confirmed `liboliphaunt-native-linux-x64-gnu-part-*` contains `initdb`, `pg_ctl`, and `postgres` only under `runtime/bin`, while `oliphaunt-tools-linux-x64-gnu-part-*` contains only `pg_dump` and `psql` there. +- Local registry publication was refreshed with explicit native runtime/tools, + broker, WASIX runtime/tools/AOT, extension, JS SDK, and node-direct artifact + roots. The npm install surface now includes `@oliphaunt/tools-linux-x64-gnu` + from Verdaccio, and its payload contains only `pg_dump` and `psql`. +- Frontend builds passed through `examples/tools/with-local-registries.sh` for + `examples/electron`, `examples/electron-wasix`, `examples/tauri`, + `examples/tauri-wasix`, and + `src/bindings/wasix-rust/examples/tauri-sqlx-vanilla`. +- Rust-side example checks passed through `examples/tools/with-local-registries.sh` + for native Tauri, Tauri WASIX, Electron WASIX, and the nested WASIX SQLx + Tauri example. The nested check needed a harness fix so local-registry runs + use `pnpm install --no-frozen-lockfile` when the wrapper disables lockfile + reads, while normal CI keeps `--frozen-lockfile`. - `examples/tools/run-tauri-webdriver-smoke.sh examples/tauri` and `examples/tools/run-tauri-webdriver-smoke.sh examples/tauri-wasix` now provide repeatable Linux GUI smoke coverage using `tauri-driver`, `WebKitWebDriver`, and `xvfb-run`. - `examples/tools/run-electron-driver-smoke.sh examples/electron` and `examples/tools/run-electron-driver-smoke.sh examples/electron-wasix` now provide repeatable Linux GUI smoke coverage using the packaged Electron binary, an IPC test-driver hook, and `xvfb-run` when present. - `tools/release/sync_release_pr.py --check`, `check_release_metadata.py`, `check_consumer_shape.py`, `check_artifact_targets.py`, and the full `tools/release/release.py check` pass after refreshing the WASIX asset input fingerprint and extension evidence digests. -- Subagent CI/release audit mapped the split native runtime/tools crates and WASIX runtime/tools/AOT/tools-AOT crates to their release generation and publication paths. Remaining CI work is to validate Linux workflow lanes locally rather than relying only on static release checks. -- Subagent SDK audit flagged Deno native asset resolution, ICU behavior, mobile static-extension readiness, and Rust native split-tool validation as the next parity risks to resolve or explicitly document. -- Local workflow tooling is available: `act` is installed at v0.2.89, which matches the latest upstream release published on 2026-06-01, Docker is available, and `act -l` parses the CI, Release, and mobile E2E workflow graph. Full Linux lane execution is still pending. +- Subagent CI/release audit found these next fixes: make extension Maven + registry publication explicit in extension metadata, derive release artifact + downloads from the target graph, remove duplicated node-direct package target + lists, decide whether existing-tag probes are dead or should become a uniform + gate, and collapse literal workflow/policy checks back to generated package + contracts. +- Subagent SDK audit found these next fixes: validate Android copied extension + files before publishing manifests, align or explicitly document Deno native + runtime/tools/extension resolution, port stronger exact-extension validation + into the Android Gradle resolver, pass mobile shared preload libraries into + startup args, and add an explicit WASIX tools preflight. +- Local workflow tooling is available: `act` is installed at v0.2.89, which + matches the latest upstream release published on 2026-06-01, Docker is + available, `act -l` parses the CI, Release, and mobile E2E workflow graph, + and the CI `release-intent` job dry-run selects successfully with + `ghcr.io/catthehacker/ubuntu:act-latest`. Full Linux lane execution should + run from a committed disposable worktree because `actions/checkout` validates + committed HEAD rather than uncommitted local edits. - JS Deno direct mode now resolves packaged ICU for explicit-library installs when running inside Deno, and rejects package-managed extension requests without an explicit prepared `runtimeDirectory`. Node and Bun remain the registry-managed extension materialization paths. - Rust native runtime cache validation already requires both split client tools, with `runtime_validation_requires_split_tools` covering a missing `pg_dump` cache entry. diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index f67d027d..7be07896 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -41,6 +41,12 @@ the release/tooling surface after the runtime tool crate split. - [x] Verify release dry-runs publish the same package families to local registries. - [ ] Keep release checks DRY: generation, validation, and publication should share one package-family model per ecosystem. +- [ ] Make extension Maven registry surfaces explicit in generated extension metadata + instead of silently appending them during release. +- [ ] Derive release workflow artifact downloads and node-direct package dirs from the + same target graph used by CI. +- [ ] Decide whether existing-tag probes are a real idempotency gate or dead workflow + code. - [ ] Validate local Linux CI lanes with a local GitHub Actions runner when practical. - [ ] Document local runner limitations instead of pretending macOS, Windows, iOS, or Android lanes were validated on Linux. @@ -54,6 +60,14 @@ the release/tooling surface after the runtime tool crate split. - [ ] Remove subtle duplicate logic where one SDK has a stronger resolver or validator than another. - [ ] Ensure examples exercise the same control flows the SDKs document. +- [ ] Validate Android split/local runtime extension files before generated manifests + declare the selected extensions. +- [ ] Align Deno native runtime/tools/extension resolution with Node/Bun, or document + and test Deno as intentionally unsupported for registry-managed extensions. +- [ ] Port Rust/JS exact-extension archive validation rules into the Android Gradle + resolver. +- [ ] Thread mobile `sharedPreloadLibraries` from manifests into startup args. +- [ ] Add an explicit WASIX tools preflight before first `pg_dump` or `psql` use. ## P2: Dead Code and Tooling Cleanup @@ -96,6 +110,24 @@ the release/tooling surface after the runtime tool crate split. for native Tauri, Electron WASIX, Tauri WASIX, and the nested WASIX SQLx Tauri example. The WASIX example lockfiles now pin the new `oliphaunt-wasix-tools` and `oliphaunt-wasix-tools-aot-*` registry packages. +- On 2026-06-26, local registry publication was rerun with explicit artifact + roots for native runtime/tools Cargo crates, broker crates, WASIX + runtime/tools/AOT crates, extension package artifacts, the JS SDK package, + and the linux x64 node-direct package. Strict Cargo and npm publication + completed against `target/local-registries`. +- On 2026-06-26, `examples/tools/with-local-registries.sh` frontend installs + and builds passed for `examples/electron`, `examples/electron-wasix`, + `examples/tauri`, `examples/tauri-wasix`, and + `src/bindings/wasix-rust/examples/tauri-sqlx-vanilla`. +- On 2026-06-26, root desktop GUI smokes passed: + `examples/tools/run-electron-driver-smoke.sh examples/electron`, + `examples/tools/run-electron-driver-smoke.sh examples/electron-wasix`, + `examples/tools/run-tauri-webdriver-smoke.sh examples/tauri`, and + `examples/tools/run-tauri-webdriver-smoke.sh examples/tauri-wasix`. +- The nested WASIX SQLx Tauri example check now keeps normal CI on + `pnpm install --frozen-lockfile` but switches to `--no-frozen-lockfile` when + `examples/tools/with-local-registries.sh` has disabled pnpm lockfile reads to + avoid stale same-version local tarball integrity. - Electron GUI smoke checks passed through `examples/tools/run-electron-driver-smoke.sh examples/electron` and `examples/tools/run-electron-driver-smoke.sh examples/electron-wasix`. @@ -113,4 +145,19 @@ the release/tooling surface after the runtime tool crate split. release metadata, and consumer-shape readiness for the current package set. - Local GitHub Actions discovery is ready on Linux: `act` v0.2.89, Docker, and `gh` are installed, and `act -l` parses the CI, Release, and mobile E2E - workflows. Full local lane execution remains a separate validation step. + workflows. `act workflow_dispatch -W .github/workflows/ci.yml -j release-intent + --dryrun -P ubuntu-latest=ghcr.io/catthehacker/ubuntu:act-latest` selects the + expected Linux CI job. Full local lane execution should run from a committed + disposable worktree because `actions/checkout` validates committed HEAD, not + uncommitted edits. +- A read-only CI/release audit found these next issues: extension Maven + publication is hidden from product metadata, release workflow downloads + re-state target lists that the CI graph already knows, node-direct package + dirs duplicate target metadata, existing-tag release probes are not consumed, + and some policy checks compare copied literals instead of generated package + contracts. +- A read-only SDK parity audit found these next issues: Android copied runtime + manifests can declare missing extensions, Deno native resolution does not + follow Node/Bun tools and extension materialization, Android Maven extension + validation is weaker than Rust/JS, mobile shared preload libraries are parsed + but not passed to startup, and WASIX split tools are only validated lazily. diff --git a/examples/tools/check-examples.sh b/examples/tools/check-examples.sh index 6d6e5141..5036a7a3 100755 --- a/examples/tools/check-examples.sh +++ b/examples/tools/check-examples.sh @@ -71,6 +71,8 @@ require_file "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/package.json" require_file "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml" require_text "src/bindings/wasix-rust/moon.yml" '^ example-check:$' require_text "src/bindings/wasix-rust/moon.yml" 'tags: \["examples", "quality", "ci-wasm-regression"\]' +require_text "src/bindings/wasix-rust/tools/check-examples.sh" 'examples/tools/with-local-registries\.sh bash "\$0"' +require_text "src/bindings/wasix-rust/tools/check-examples.sh" 'PNPM_CONFIG_LOCKFILE' require_file "examples/tools/with-local-registries.sh" require_file "examples/tools/run-tauri-webdriver-smoke.sh" diff --git a/src/bindings/wasix-rust/tools/check-examples.sh b/src/bindings/wasix-rust/tools/check-examples.sh index 6ca5b38c..3d6a4f34 100755 --- a/src/bindings/wasix-rust/tools/check-examples.sh +++ b/src/bindings/wasix-rust/tools/check-examples.sh @@ -7,6 +7,10 @@ root="$(git rev-parse --show-toplevel 2>/dev/null)" || { } cd "$root" +if [[ -z "${CARGO_REGISTRIES_OLIPHAUNT_LOCAL_INDEX:-}" ]]; then + exec examples/tools/with-local-registries.sh bash "$0" +fi + run() { printf '\n==> %s\n' "$*" "$@" @@ -67,5 +71,9 @@ allowBuilds: YAML cp pnpm-lock.yaml "$workspace/pnpm-lock.yaml" -run pnpm --dir "$work" install --frozen-lockfile +if [[ "${PNPM_CONFIG_LOCKFILE:-}" == "false" ]]; then + run pnpm --dir "$work" install --no-frozen-lockfile +else + run pnpm --dir "$work" install --frozen-lockfile +fi run pnpm --dir "$work" run build From c109e9979a3cb9b2b59a5e1852547ca3057839f1 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 01:53:54 +0000 Subject: [PATCH 022/137] fix: stage wasix sdk registry dependencies --- Cargo.lock | 2 +- .../crates/oliphaunt-wasix/Cargo.toml | 2 +- src/runtimes/liboliphaunt/icu/Cargo.toml | 2 +- tools/release/build-sdk-ci-artifacts.sh | 1 + tools/release/check_consumer_shape.py | 15 ++++ tools/release/check_release_metadata.py | 9 ++ tools/release/check_staged_artifacts.py | 87 +++++++++++++++++++ tools/release/local_registry_publish.py | 9 +- .../package_oliphaunt_wasix_sdk_crate.py | 32 +++++++ tools/release/release.py | 85 +++++++++++++++++- 10 files changed, 238 insertions(+), 6 deletions(-) create mode 100755 tools/release/package_oliphaunt_wasix_sdk_crate.py diff --git a/Cargo.lock b/Cargo.lock index b1989e6d..42bbeb3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2307,7 +2307,7 @@ dependencies = [ [[package]] name = "oliphaunt-icu" -version = "0.1.0" +version = "0.0.0" dependencies = [ "sha2 0.10.9", "tar", diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml b/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml index 4c5a92b9..50c48d67 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml @@ -97,7 +97,7 @@ dunce = "1" filetime = "0.2" liboliphaunt-wasix-portable = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/assets" } oliphaunt-wasix-tools = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/wasix/crates/tools", optional = true } -oliphaunt-icu = { version = "=0.1.0", path = "../../../../runtimes/liboliphaunt/icu", optional = true } +oliphaunt-icu = { version = "=0.0.0", path = "../../../../runtimes/liboliphaunt/icu", optional = true } tokio = { version = "1", features = ["io-util", "rt-multi-thread"] } wasmer = { version = "7.2.0-alpha.3", default-features = false, features = [ "sys", diff --git a/src/runtimes/liboliphaunt/icu/Cargo.toml b/src/runtimes/liboliphaunt/icu/Cargo.toml index b96f8dc4..d146766e 100644 --- a/src/runtimes/liboliphaunt/icu/Cargo.toml +++ b/src/runtimes/liboliphaunt/icu/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oliphaunt-icu" -version = "0.1.0" +version = "0.0.0" edition = "2024" rust-version = "1.93" description = "Optional ICU data files for Oliphaunt runtimes." diff --git a/tools/release/build-sdk-ci-artifacts.sh b/tools/release/build-sdk-ci-artifacts.sh index 1924bad0..98e1c187 100755 --- a/tools/release/build-sdk-ci-artifacts.sh +++ b/tools/release/build-sdk-ci-artifacts.sh @@ -207,6 +207,7 @@ case "$product" in require python3 package_listing="$root/target/oliphaunt-wasix-rust/package/oliphaunt-wasix.package-files.txt" require_file "$package_listing" + python3 tools/release/package_oliphaunt_wasix_sdk_crate.py --output-dir "$artifact_root" cp "$package_listing" "$artifact_root/cargo-package-files.txt" ;; *) diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 41709f5b..651744fd 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -1401,6 +1401,7 @@ def check_wasm(findings: list[Finding]) -> None: target_tables = manifest.get("target", {}) expected_runtime_dependency = dependencies.get("liboliphaunt-wasix-portable") expected_tools_dependency = dependencies.get("oliphaunt-wasix-tools") + expected_icu_dependency = dependencies.get("oliphaunt-icu") require( findings, product, @@ -1422,6 +1423,20 @@ def check_wasm(findings: list[Finding]) -> None: f"oliphaunt-wasix-tools dependency={expected_tools_dependency!r}", severity="P0", ) + icu_source_manifest = read_toml("src/runtimes/liboliphaunt/icu/Cargo.toml") + icu_source_version = icu_source_manifest.get("package", {}).get("version") + require( + findings, + product, + "wasm-local-icu-dependency", + isinstance(expected_icu_dependency, dict) + and expected_icu_dependency.get("version") == f"={icu_source_version}" + and expected_icu_dependency.get("path") == "../../../../runtimes/liboliphaunt/icu" + and expected_icu_dependency.get("optional") is True, + "WASM source crate must keep the ICU feature wired to the local oliphaunt-icu path crate; release packaging rewrites this edge to the published runtime version.", + f"oliphaunt-icu dependency={expected_icu_dependency!r}", + severity="P0", + ) expected_aot_dependencies = { 'cfg(all(target_os = "macos", target_arch = "aarch64"))': "liboliphaunt-wasix-aot-aarch64-apple-darwin", 'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))': "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 2806303c..2e7d4eca 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1054,6 +1054,15 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None or tools_dependency.get("optional") is not True ): fail("oliphaunt-wasix must optionally depend on oliphaunt-wasix-tools at the exact liboliphaunt-wasix runtime version") + icu_source_version = version_file_value("src/runtimes/liboliphaunt/icu/Cargo.toml") + icu_dependency = dependencies.get("oliphaunt-icu") + if ( + not isinstance(icu_dependency, dict) + or icu_dependency.get("version") != f"={icu_source_version}" + or icu_dependency.get("path") != "../../../../runtimes/liboliphaunt/icu" + or icu_dependency.get("optional") is not True + ): + fail("oliphaunt-wasix source must optionally depend on the local oliphaunt-icu path crate version") expected_aot_dependencies = { 'cfg(all(target_os = "macos", target_arch = "aarch64"))': "liboliphaunt-wasix-aot-aarch64-apple-darwin", 'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))': "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", diff --git a/tools/release/check_staged_artifacts.py b/tools/release/check_staged_artifacts.py index 10a057c0..cbde3235 100755 --- a/tools/release/check_staged_artifacts.py +++ b/tools/release/check_staged_artifacts.py @@ -18,6 +18,7 @@ import re import sys import tarfile +import tomllib import zipfile from collections.abc import Iterable from dataclasses import dataclass @@ -124,6 +125,29 @@ def archive_tar_names(path: Path) -> list[str]: fail(f"{rel(path)} is not a readable tar archive: {error}") +def cargo_crate_manifest(path: Path) -> dict[str, object]: + try: + with tarfile.open(path, "r:*") as archive: + manifests = [ + member + for member in archive.getmembers() + if member.isfile() and member.name.count("/") == 1 and member.name.endswith("/Cargo.toml") + ] + if len(manifests) != 1: + fail(f"{rel(path)} must contain exactly one top-level Cargo.toml") + extracted = archive.extractfile(manifests[0]) + if extracted is None: + fail(f"{rel(path)} Cargo.toml could not be read") + data = tomllib.loads(extracted.read().decode("utf-8")) + except tarfile.TarError as error: + fail(f"{rel(path)} is not a readable Cargo crate archive: {error}") + except (tomllib.TOMLDecodeError, UnicodeDecodeError) as error: + fail(f"{rel(path)} contains an invalid Cargo.toml: {error}") + if not isinstance(data, dict): + fail(f"{rel(path)} Cargo.toml must contain a TOML table") + return data + + def archive_zip_names(path: Path) -> list[str]: try: with zipfile.ZipFile(path) as archive: @@ -132,6 +156,62 @@ def archive_zip_names(path: Path) -> list[str]: fail(f"{rel(path)} is not a readable zip archive: {error}") +def validate_wasix_sdk_crate(crate: Path) -> None: + manifest = cargo_crate_manifest(crate) + package = manifest.get("package") + if not isinstance(package, dict) or package.get("name") != "oliphaunt-wasix": + fail(f"{rel(crate)} must package the oliphaunt-wasix crate") + runtime_version = product_metadata.read_current_version("liboliphaunt-wasix") + dependencies = manifest.get("dependencies") + if not isinstance(dependencies, dict): + fail(f"{rel(crate)} must declare Cargo dependencies") + required_dependencies = { + "liboliphaunt-wasix-portable", + "oliphaunt-wasix-tools", + "oliphaunt-icu", + } + for name in sorted(required_dependencies): + dependency = dependencies.get(name) + if ( + not isinstance(dependency, dict) + or dependency.get("version") != f"={runtime_version}" + or "path" in dependency + ): + fail(f"{rel(crate)} dependency {name} must use registry version ={runtime_version} without a path") + target_tables = manifest.get("target") + if not isinstance(target_tables, dict): + fail(f"{rel(crate)} must declare target-specific WASIX AOT dependencies") + expected_targets = { + 'cfg(all(target_os = "macos", target_arch = "aarch64"))': [ + "liboliphaunt-wasix-aot-aarch64-apple-darwin", + "oliphaunt-wasix-tools-aot-aarch64-apple-darwin", + ], + 'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))': [ + "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", + "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu", + ], + 'cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))': [ + "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu", + "oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu", + ], + 'cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))': [ + "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc", + "oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc", + ], + } + for cfg, crates in expected_targets.items(): + target = target_tables.get(cfg) + target_dependencies = target.get("dependencies", {}) if isinstance(target, dict) else {} + for name in crates: + dependency = target_dependencies.get(name) + if ( + not isinstance(dependency, dict) + or dependency.get("version") != f"={runtime_version}" + or "path" in dependency + ): + fail(f"{rel(crate)} target dependency {cfg}:{name} must use registry version ={runtime_version} without a path") + + def validate_zstd_archive_magic(path: Path) -> None: with path.open("rb") as handle: magic = handle.read(4) @@ -315,6 +395,13 @@ def check_sdk_product(product: str, *, require: bool) -> bool: reject_sdk_runtime_payload(product, crate, archive_tar_names(crate)) checked = True elif product == "oliphaunt-wasix-rust": + crates = sorted(root.glob("*.crate")) + if not crates and require: + fail(f"{product} must stage a Cargo crate under {rel(root)}") + for crate in crates: + reject_sdk_runtime_payload(product, crate, archive_tar_names(crate)) + validate_wasix_sdk_crate(crate) + checked = True listing = root / "cargo-package-files.txt" if not listing.is_file(): if require: diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index 19bdcb61..21ca140f 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -1456,7 +1456,14 @@ def stage_cargo_source_crates( ) generated.append(manual_cargo_package_source(oliphaunt_manifest, output_dir)) - wasix_manifest = ROOT / "src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml" + wasix_manifest = release.prepare_oliphaunt_wasix_release_source( + release.current_product_version("oliphaunt-wasix-rust") + ) + prune_missing_local_artifact_target_dependencies( + wasix_manifest, + available_package_names, + result, + ) generated.append(manual_cargo_package_source(wasix_manifest, output_dir)) for manifest in native_runtime_all_manifests: diff --git a/tools/release/package_oliphaunt_wasix_sdk_crate.py b/tools/release/package_oliphaunt_wasix_sdk_crate.py new file mode 100755 index 00000000..11ff9258 --- /dev/null +++ b/tools/release/package_oliphaunt_wasix_sdk_crate.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +"""Package the WASIX Rust SDK publish-shaped crate without resolving dependencies.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +import local_registry_publish +import release + + +ROOT = Path(__file__).resolve().parents[2] + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--output-dir", required=True, type=Path) + args = parser.parse_args() + + output_dir = args.output_dir + if not output_dir.is_absolute(): + output_dir = ROOT / output_dir + version = release.current_product_version("oliphaunt-wasix-rust") + manifest = release.prepare_oliphaunt_wasix_release_source(version) + crate_path = local_registry_publish.manual_cargo_package_source(manifest, output_dir) + print(crate_path.relative_to(ROOT)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/release/release.py b/tools/release/release.py index 18d70883..14a5cef8 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -7,6 +7,7 @@ import hashlib import json import os +import re import shutil import subprocess import sys @@ -658,6 +659,78 @@ def validate_generated_oliphaunt_release_artifact_coverage(manifest_path: Path) ) +def render_oliphaunt_wasix_release_cargo_toml(source: str, runtime_version: str) -> str: + text = source.replace( + "repository.workspace = true", + 'repository = "https://github.com/f0rr0/oliphaunt"', + ).replace( + "homepage.workspace = true", + 'homepage = "https://oliphaunt.dev"', + ) + text = re.sub(r', path = "[^"]+"', "", text) + artifact_crates = { + package_liboliphaunt_wasix_cargo_artifacts.ICU_PACKAGE, + package_liboliphaunt_wasix_cargo_artifacts.RUNTIME_PACKAGE, + package_liboliphaunt_wasix_cargo_artifacts.TOOLS_PACKAGE, + *package_liboliphaunt_wasix_cargo_artifacts.AOT_PACKAGES.values(), + *package_liboliphaunt_wasix_cargo_artifacts.TOOLS_AOT_PACKAGES.values(), + } + for crate in sorted(artifact_crates): + pattern = rf'(?m)^({re.escape(crate)}\s*=\s*\{{[^}}\n]*version\s*=\s*")=[^"]+("[^}}\n]*\}})$' + text, count = re.subn(pattern, rf"\1={runtime_version}\2", text, count=1) + if count != 1: + fail(f"generated oliphaunt-wasix release source is missing dependency {crate}") + if "\n[workspace]" not in text: + text = text.rstrip() + "\n\n[workspace]\n" + return text + + +def validate_generated_oliphaunt_wasix_release_artifact_coverage(manifest_path: Path) -> None: + manifest = manifest_path.read_text(encoding="utf-8") + if re.search(r'=\s*\{[^}\n]*path\s*=', manifest): + fail("generated oliphaunt-wasix release source must not contain local path dependencies") + runtime_version = current_product_version("liboliphaunt-wasix") + required_crates = { + package_liboliphaunt_wasix_cargo_artifacts.ICU_PACKAGE, + package_liboliphaunt_wasix_cargo_artifacts.RUNTIME_PACKAGE, + package_liboliphaunt_wasix_cargo_artifacts.TOOLS_PACKAGE, + *cargo_registry_packages("liboliphaunt-wasix"), + } + missing = [ + crate + for crate in sorted(required_crates) + if f'{crate} = {{ version = "={runtime_version}"' not in manifest + ] + if missing: + fail( + "generated oliphaunt-wasix release source is missing WASIX artifact dependency pins: " + + ", ".join(missing) + ) + + +def prepare_oliphaunt_wasix_release_source(version: str) -> Path: + runtime_version = current_product_version("liboliphaunt-wasix") + source_dir = ROOT / "src" / "bindings" / "wasix-rust" / "crates" / "oliphaunt-wasix" + stage_dir = ROOT / "target" / "release" / "cargo-package-sources" / "oliphaunt-wasix" + shutil.rmtree(stage_dir, ignore_errors=True) + shutil.copytree( + source_dir, + stage_dir, + ignore=shutil.ignore_patterns("target"), + ) + cargo_toml = stage_dir / "Cargo.toml" + rendered = render_oliphaunt_wasix_release_cargo_toml( + cargo_toml.read_text(encoding="utf-8"), + runtime_version, + ) + cargo_toml.write_text(rendered, encoding="utf-8") + package = rendered.split("[package]", 1)[1].split("[", 1)[0] + if f'version = "{version}"' not in package: + fail(f"generated oliphaunt-wasix release source must keep SDK version {version}") + validate_generated_oliphaunt_wasix_release_artifact_coverage(cargo_toml) + return cargo_toml + + def prepare_oliphaunt_release_source(version: str) -> Path: native_version = current_product_version("liboliphaunt-native") broker_version = current_product_version("oliphaunt-broker") @@ -992,9 +1065,15 @@ def validate_wasix_aot_release_asset(archive: Path) -> None: def run_wasm_release_dry_run(allow_dirty: bool) -> None: _ = allow_dirty + version = current_product_version("oliphaunt-wasix-rust") validate_staged_sdk_package("oliphaunt-wasix-rust") + release_manifest = prepare_oliphaunt_wasix_release_source(version) + validate_generated_oliphaunt_wasix_release_artifact_coverage(release_manifest) + print( + f"validated generated WASIX Rust binding release source: {release_manifest.relative_to(ROOT)}" + ) print( - "validated staged WASIX Rust binding package shape; " + "validated staged WASIX Rust binding package shape and generated publish manifest; " "source publish runs after WASIX artifact crates are published." ) @@ -1021,7 +1100,9 @@ def publish_wasm_crates_io(head_ref: str) -> None: ) version = current_product_version("oliphaunt-wasix-rust") validate_staged_sdk_package("oliphaunt-wasix-rust") - cargo_publish_package("oliphaunt-wasix", version) + release_manifest = prepare_oliphaunt_wasix_release_source(version) + validate_generated_oliphaunt_wasix_release_artifact_coverage(release_manifest) + cargo_publish_manifest("oliphaunt-wasix", version, release_manifest) run( [ "tools/release/check_registry_publication.py", From b4892e518960b743a130c402b6d01ea0e6ea2b36 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 02:05:42 +0000 Subject: [PATCH 023/137] fix: make extension maven packages explicit --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 18 +++-- .../examples-ci-release-validation.md | 18 +++-- src/extensions/contrib/amcheck/release.toml | 10 ++- .../contrib/auto_explain/release.toml | 10 ++- src/extensions/contrib/bloom/release.toml | 10 ++- src/extensions/contrib/btree_gin/release.toml | 10 ++- .../contrib/btree_gist/release.toml | 10 ++- src/extensions/contrib/citext/release.toml | 10 ++- src/extensions/contrib/cube/release.toml | 10 ++- src/extensions/contrib/dict_int/release.toml | 10 ++- src/extensions/contrib/dict_xsyn/release.toml | 10 ++- .../contrib/earthdistance/release.toml | 10 ++- src/extensions/contrib/file_fdw/release.toml | 10 ++- .../contrib/fuzzystrmatch/release.toml | 10 ++- src/extensions/contrib/hstore/release.toml | 10 ++- src/extensions/contrib/intarray/release.toml | 10 ++- src/extensions/contrib/isn/release.toml | 10 ++- src/extensions/contrib/lo/release.toml | 10 ++- src/extensions/contrib/ltree/release.toml | 10 ++- .../contrib/pageinspect/release.toml | 10 ++- .../contrib/pg_buffercache/release.toml | 10 ++- .../contrib/pg_freespacemap/release.toml | 10 ++- .../contrib/pg_surgery/release.toml | 10 ++- src/extensions/contrib/pg_trgm/release.toml | 10 ++- .../contrib/pg_visibility/release.toml | 10 ++- .../contrib/pg_walinspect/release.toml | 10 ++- src/extensions/contrib/pgcrypto/release.toml | 10 ++- src/extensions/contrib/seg/release.toml | 10 ++- src/extensions/contrib/tablefunc/release.toml | 10 ++- src/extensions/contrib/tcn/release.toml | 10 ++- .../contrib/tsm_system_rows/release.toml | 10 ++- .../contrib/tsm_system_time/release.toml | 10 ++- src/extensions/contrib/unaccent/release.toml | 10 ++- src/extensions/contrib/uuid_ossp/release.toml | 10 ++- ...2026-06-07-transitional-catalog-smoke.json | 2 +- .../external/pg_hashids/release.toml | 10 ++- src/extensions/external/pg_ivm/release.toml | 10 ++- .../external/pg_textsearch/release.toml | 10 ++- .../external/pg_uuidv7/release.toml | 10 ++- src/extensions/external/pgtap/release.toml | 10 ++- src/extensions/external/postgis/release.toml | 10 ++- src/extensions/external/vector/release.toml | 10 ++- .../generated/docs/extension-evidence.json | 80 +++++++++---------- .../assets/generated/asset-inputs.sha256 | 2 +- tools/release/check_consumer_shape.py | 11 +-- tools/release/check_registry_publication.py | 17 ++-- tools/release/check_release_metadata.py | 13 ++- tools/release/product_metadata.py | 5 -- tools/release/sync_release_pr.py | 67 ++++++++++++++++ 49 files changed, 462 insertions(+), 161 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index f292d83f..7bd6839d 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -29,7 +29,7 @@ review production pipelines, then normalize implementation details. - [x] Verify native runtime payloads contain `postgres`, `initdb`, `pg_ctl`; native tools payloads contain `pg_dump`, `psql`. - [x] Verify WASIX runtime payloads contain `postgres`, `initdb`; WASIX tools payloads contain `pg_dump`, `psql`, not `pg_ctl`. - [ ] Verify extension packages and runtime tools are published and installed from registries idiomatically. -- [ ] Make extension Maven registry surfaces explicit in extension metadata instead of silently appending them in release tooling. +- [x] Make extension Maven registry surfaces explicit in extension metadata instead of silently appending them in release tooling. - [ ] Remove or generate duplicated release target lists in workflow downloads, node-direct package dirs, artifact target checks, and release policy checks. - [ ] Decide whether existing-tag release probes should become a uniform idempotency gate or be removed. - [x] Keep release-derived files synchronized after the split tool package changes. @@ -79,12 +79,16 @@ review production pipelines, then normalize implementation details. - `examples/tools/run-tauri-webdriver-smoke.sh examples/tauri` and `examples/tools/run-tauri-webdriver-smoke.sh examples/tauri-wasix` now provide repeatable Linux GUI smoke coverage using `tauri-driver`, `WebKitWebDriver`, and `xvfb-run`. - `examples/tools/run-electron-driver-smoke.sh examples/electron` and `examples/tools/run-electron-driver-smoke.sh examples/electron-wasix` now provide repeatable Linux GUI smoke coverage using the packaged Electron binary, an IPC test-driver hook, and `xvfb-run` when present. - `tools/release/sync_release_pr.py --check`, `check_release_metadata.py`, `check_consumer_shape.py`, `check_artifact_targets.py`, and the full `tools/release/release.py check` pass after refreshing the WASIX asset input fingerprint and extension evidence digests. -- Subagent CI/release audit found these next fixes: make extension Maven - registry publication explicit in extension metadata, derive release artifact - downloads from the target graph, remove duplicated node-direct package target - lists, decide whether existing-tag probes are dead or should become a uniform - gate, and collapse literal workflow/policy checks back to generated package - contracts. +- Extension Maven publication is now explicit in each exact-extension + `release.toml`: the metadata lists `maven-central` and the two Android Maven + package coordinates derived from the extension target graph. The old hidden + release-tool synthesis path was removed, and release metadata plus consumer + shape checks now enforce the explicit package surface. +- Subagent CI/release audit found these remaining next fixes: derive release + artifact downloads from the target graph, remove duplicated node-direct + package target lists, decide whether existing-tag probes are dead or should + become a uniform gate, and collapse literal workflow/policy checks back to + generated package contracts. - Subagent SDK audit found these next fixes: validate Android copied extension files before publishing manifests, align or explicitly document Deno native runtime/tools/extension resolution, port stronger exact-extension validation diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index 7be07896..6b3fb302 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -41,7 +41,7 @@ the release/tooling surface after the runtime tool crate split. - [x] Verify release dry-runs publish the same package families to local registries. - [ ] Keep release checks DRY: generation, validation, and publication should share one package-family model per ecosystem. -- [ ] Make extension Maven registry surfaces explicit in generated extension metadata +- [x] Make extension Maven registry surfaces explicit in generated extension metadata instead of silently appending them during release. - [ ] Derive release workflow artifact downloads and node-direct package dirs from the same target graph used by CI. @@ -143,6 +143,11 @@ the release/tooling surface after the runtime tool crate split. fingerprint and extension evidence source digests. `tools/release/release.py check` passes through policy, release-please config, artifact targets, release metadata, and consumer-shape readiness for the current package set. +- Exact-extension `release.toml` metadata now declares `maven-central` and the + Android Maven package coordinates explicitly. The release metadata and + consumer-shape checks enforce that those package names match the generated + Android extension target graph instead of relying on hidden release-time + synthesis. - Local GitHub Actions discovery is ready on Linux: `act` v0.2.89, Docker, and `gh` are installed, and `act -l` parses the CI, Release, and mobile E2E workflows. `act workflow_dispatch -W .github/workflows/ci.yml -j release-intent @@ -150,12 +155,11 @@ the release/tooling surface after the runtime tool crate split. expected Linux CI job. Full local lane execution should run from a committed disposable worktree because `actions/checkout` validates committed HEAD, not uncommitted edits. -- A read-only CI/release audit found these next issues: extension Maven - publication is hidden from product metadata, release workflow downloads - re-state target lists that the CI graph already knows, node-direct package - dirs duplicate target metadata, existing-tag release probes are not consumed, - and some policy checks compare copied literals instead of generated package - contracts. +- A read-only CI/release audit found these remaining next issues: release + workflow downloads re-state target lists that the CI graph already knows, + node-direct package dirs duplicate target metadata, existing-tag release + probes are not consumed, and some policy checks compare copied literals + instead of generated package contracts. - A read-only SDK parity audit found these next issues: Android copied runtime manifests can declare missing extensions, Deno native resolution does not follow Node/Bun tools and extension materialization, Android Maven extension diff --git a/src/extensions/contrib/amcheck/release.toml b/src/extensions/contrib/amcheck/release.toml index 5310e19e..905a4f5b 100644 --- a/src/extensions/contrib/amcheck/release.toml +++ b/src/extensions/contrib/amcheck/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-amcheck" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-amcheck-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-amcheck-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "amcheck" diff --git a/src/extensions/contrib/auto_explain/release.toml b/src/extensions/contrib/auto_explain/release.toml index 69099e09..5ba53f81 100644 --- a/src/extensions/contrib/auto_explain/release.toml +++ b/src/extensions/contrib/auto_explain/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-auto-explain" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-auto-explain-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-auto-explain-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "auto_explain" diff --git a/src/extensions/contrib/bloom/release.toml b/src/extensions/contrib/bloom/release.toml index 99245837..6112d6c5 100644 --- a/src/extensions/contrib/bloom/release.toml +++ b/src/extensions/contrib/bloom/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-bloom" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-bloom-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-bloom-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "bloom" diff --git a/src/extensions/contrib/btree_gin/release.toml b/src/extensions/contrib/btree_gin/release.toml index deac9a51..1c691886 100644 --- a/src/extensions/contrib/btree_gin/release.toml +++ b/src/extensions/contrib/btree_gin/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-btree-gin" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-btree-gin-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-btree-gin-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "btree_gin" diff --git a/src/extensions/contrib/btree_gist/release.toml b/src/extensions/contrib/btree_gist/release.toml index c4f5ecd7..f973dfaf 100644 --- a/src/extensions/contrib/btree_gist/release.toml +++ b/src/extensions/contrib/btree_gist/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-btree-gist" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-btree-gist-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-btree-gist-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "btree_gist" diff --git a/src/extensions/contrib/citext/release.toml b/src/extensions/contrib/citext/release.toml index 53e0c860..c3863599 100644 --- a/src/extensions/contrib/citext/release.toml +++ b/src/extensions/contrib/citext/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-citext" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-citext-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-citext-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "citext" diff --git a/src/extensions/contrib/cube/release.toml b/src/extensions/contrib/cube/release.toml index 22fd67eb..fbab3f7f 100644 --- a/src/extensions/contrib/cube/release.toml +++ b/src/extensions/contrib/cube/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-cube" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-cube-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-cube-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "cube" diff --git a/src/extensions/contrib/dict_int/release.toml b/src/extensions/contrib/dict_int/release.toml index d3045322..c7cd32ed 100644 --- a/src/extensions/contrib/dict_int/release.toml +++ b/src/extensions/contrib/dict_int/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-dict-int" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-dict-int-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-dict-int-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "dict_int" diff --git a/src/extensions/contrib/dict_xsyn/release.toml b/src/extensions/contrib/dict_xsyn/release.toml index b7e2505c..139cb961 100644 --- a/src/extensions/contrib/dict_xsyn/release.toml +++ b/src/extensions/contrib/dict_xsyn/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-dict-xsyn" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-dict-xsyn-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-dict-xsyn-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "dict_xsyn" diff --git a/src/extensions/contrib/earthdistance/release.toml b/src/extensions/contrib/earthdistance/release.toml index a09d8600..dc31bda9 100644 --- a/src/extensions/contrib/earthdistance/release.toml +++ b/src/extensions/contrib/earthdistance/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-earthdistance" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-earthdistance-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-earthdistance-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "earthdistance" diff --git a/src/extensions/contrib/file_fdw/release.toml b/src/extensions/contrib/file_fdw/release.toml index d8e0e63d..3c00ebbf 100644 --- a/src/extensions/contrib/file_fdw/release.toml +++ b/src/extensions/contrib/file_fdw/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-file-fdw" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-file-fdw-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-file-fdw-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "file_fdw" diff --git a/src/extensions/contrib/fuzzystrmatch/release.toml b/src/extensions/contrib/fuzzystrmatch/release.toml index ed8c8785..bfbf5633 100644 --- a/src/extensions/contrib/fuzzystrmatch/release.toml +++ b/src/extensions/contrib/fuzzystrmatch/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-fuzzystrmatch" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-fuzzystrmatch-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-fuzzystrmatch-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "fuzzystrmatch" diff --git a/src/extensions/contrib/hstore/release.toml b/src/extensions/contrib/hstore/release.toml index 04b094bf..8dc8885a 100644 --- a/src/extensions/contrib/hstore/release.toml +++ b/src/extensions/contrib/hstore/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-hstore" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-hstore-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-hstore-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "hstore" diff --git a/src/extensions/contrib/intarray/release.toml b/src/extensions/contrib/intarray/release.toml index a2cfae50..5295cf62 100644 --- a/src/extensions/contrib/intarray/release.toml +++ b/src/extensions/contrib/intarray/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-intarray" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-intarray-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-intarray-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "intarray" diff --git a/src/extensions/contrib/isn/release.toml b/src/extensions/contrib/isn/release.toml index 86284395..9561d231 100644 --- a/src/extensions/contrib/isn/release.toml +++ b/src/extensions/contrib/isn/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-isn" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-isn-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-isn-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "isn" diff --git a/src/extensions/contrib/lo/release.toml b/src/extensions/contrib/lo/release.toml index 00cffc91..4875e683 100644 --- a/src/extensions/contrib/lo/release.toml +++ b/src/extensions/contrib/lo/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-lo" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-lo-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-lo-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "lo" diff --git a/src/extensions/contrib/ltree/release.toml b/src/extensions/contrib/ltree/release.toml index e4baa347..ddfc2939 100644 --- a/src/extensions/contrib/ltree/release.toml +++ b/src/extensions/contrib/ltree/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-ltree" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-ltree-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-ltree-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "ltree" diff --git a/src/extensions/contrib/pageinspect/release.toml b/src/extensions/contrib/pageinspect/release.toml index 2f681930..7a4e93fc 100644 --- a/src/extensions/contrib/pageinspect/release.toml +++ b/src/extensions/contrib/pageinspect/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-pageinspect" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-pageinspect-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-pageinspect-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "pageinspect" diff --git a/src/extensions/contrib/pg_buffercache/release.toml b/src/extensions/contrib/pg_buffercache/release.toml index 087aaf9f..0e5e8ddc 100644 --- a/src/extensions/contrib/pg_buffercache/release.toml +++ b/src/extensions/contrib/pg_buffercache/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-pg-buffercache" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-pg-buffercache-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-pg-buffercache-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "pg_buffercache" diff --git a/src/extensions/contrib/pg_freespacemap/release.toml b/src/extensions/contrib/pg_freespacemap/release.toml index 3233c3f9..5b5dc6c5 100644 --- a/src/extensions/contrib/pg_freespacemap/release.toml +++ b/src/extensions/contrib/pg_freespacemap/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-pg-freespacemap" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-pg-freespacemap-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-pg-freespacemap-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "pg_freespacemap" diff --git a/src/extensions/contrib/pg_surgery/release.toml b/src/extensions/contrib/pg_surgery/release.toml index a5b9e621..7d0ea07b 100644 --- a/src/extensions/contrib/pg_surgery/release.toml +++ b/src/extensions/contrib/pg_surgery/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-pg-surgery" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-pg-surgery-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-pg-surgery-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "pg_surgery" diff --git a/src/extensions/contrib/pg_trgm/release.toml b/src/extensions/contrib/pg_trgm/release.toml index ef520d86..25979899 100644 --- a/src/extensions/contrib/pg_trgm/release.toml +++ b/src/extensions/contrib/pg_trgm/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-pg-trgm" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-pg-trgm-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-pg-trgm-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "pg_trgm" diff --git a/src/extensions/contrib/pg_visibility/release.toml b/src/extensions/contrib/pg_visibility/release.toml index 17bd9a47..9bfea0dc 100644 --- a/src/extensions/contrib/pg_visibility/release.toml +++ b/src/extensions/contrib/pg_visibility/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-pg-visibility" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-pg-visibility-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-pg-visibility-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "pg_visibility" diff --git a/src/extensions/contrib/pg_walinspect/release.toml b/src/extensions/contrib/pg_walinspect/release.toml index c12b6d76..580c4d79 100644 --- a/src/extensions/contrib/pg_walinspect/release.toml +++ b/src/extensions/contrib/pg_walinspect/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-pg-walinspect" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-pg-walinspect-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-pg-walinspect-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "pg_walinspect" diff --git a/src/extensions/contrib/pgcrypto/release.toml b/src/extensions/contrib/pgcrypto/release.toml index d305763e..efdd815c 100644 --- a/src/extensions/contrib/pgcrypto/release.toml +++ b/src/extensions/contrib/pgcrypto/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-pgcrypto" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-pgcrypto-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-pgcrypto-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "pgcrypto" diff --git a/src/extensions/contrib/seg/release.toml b/src/extensions/contrib/seg/release.toml index f07cac6a..c6fe3ec0 100644 --- a/src/extensions/contrib/seg/release.toml +++ b/src/extensions/contrib/seg/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-seg" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-seg-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-seg-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "seg" diff --git a/src/extensions/contrib/tablefunc/release.toml b/src/extensions/contrib/tablefunc/release.toml index b309e41c..086ad03c 100644 --- a/src/extensions/contrib/tablefunc/release.toml +++ b/src/extensions/contrib/tablefunc/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-tablefunc" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-tablefunc-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-tablefunc-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "tablefunc" diff --git a/src/extensions/contrib/tcn/release.toml b/src/extensions/contrib/tcn/release.toml index 45be1e8c..c437c842 100644 --- a/src/extensions/contrib/tcn/release.toml +++ b/src/extensions/contrib/tcn/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-tcn" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-tcn-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-tcn-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "tcn" diff --git a/src/extensions/contrib/tsm_system_rows/release.toml b/src/extensions/contrib/tsm_system_rows/release.toml index f4b29e80..0dca4c20 100644 --- a/src/extensions/contrib/tsm_system_rows/release.toml +++ b/src/extensions/contrib/tsm_system_rows/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-tsm-system-rows" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-tsm-system-rows-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-tsm-system-rows-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "tsm_system_rows" diff --git a/src/extensions/contrib/tsm_system_time/release.toml b/src/extensions/contrib/tsm_system_time/release.toml index 104a1150..cdc4ebad 100644 --- a/src/extensions/contrib/tsm_system_time/release.toml +++ b/src/extensions/contrib/tsm_system_time/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-tsm-system-time" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-tsm-system-time-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-tsm-system-time-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "tsm_system_time" diff --git a/src/extensions/contrib/unaccent/release.toml b/src/extensions/contrib/unaccent/release.toml index 596a8874..e813f22b 100644 --- a/src/extensions/contrib/unaccent/release.toml +++ b/src/extensions/contrib/unaccent/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-unaccent" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-unaccent-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-unaccent-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "unaccent" diff --git a/src/extensions/contrib/uuid_ossp/release.toml b/src/extensions/contrib/uuid_ossp/release.toml index 2010c4e9..7a46a1d0 100644 --- a/src/extensions/contrib/uuid_ossp/release.toml +++ b/src/extensions/contrib/uuid_ossp/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-uuid-ossp" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-uuid-ossp-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-uuid-ossp-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "uuid-ossp" diff --git a/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json b/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json index a5fd683f..639fe860 100644 --- a/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json +++ b/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json @@ -514,7 +514,7 @@ } ], "schema": "oliphaunt-extension-evidence-v1", - "sourceDigest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d", + "sourceDigest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f", "sourceDigestInputs": [ "src/postgres/versions/18/source.toml", "src/extensions/catalog/extensions.promoted.toml", diff --git a/src/extensions/external/pg_hashids/release.toml b/src/extensions/external/pg_hashids/release.toml index 76852ff3..96fd3860 100644 --- a/src/extensions/external/pg_hashids/release.toml +++ b/src/extensions/external/pg_hashids/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-pg-hashids" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-pg-hashids-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-pg-hashids-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "pg_hashids" diff --git a/src/extensions/external/pg_ivm/release.toml b/src/extensions/external/pg_ivm/release.toml index f6a36819..52daf271 100644 --- a/src/extensions/external/pg_ivm/release.toml +++ b/src/extensions/external/pg_ivm/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-pg-ivm" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-pg-ivm-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-pg-ivm-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "pg_ivm" diff --git a/src/extensions/external/pg_textsearch/release.toml b/src/extensions/external/pg_textsearch/release.toml index f81b3ffe..3f0b18e2 100644 --- a/src/extensions/external/pg_textsearch/release.toml +++ b/src/extensions/external/pg_textsearch/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-pg-textsearch" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-pg-textsearch-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-pg-textsearch-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "pg_textsearch" diff --git a/src/extensions/external/pg_uuidv7/release.toml b/src/extensions/external/pg_uuidv7/release.toml index b77560c5..646869fe 100644 --- a/src/extensions/external/pg_uuidv7/release.toml +++ b/src/extensions/external/pg_uuidv7/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-pg-uuidv7" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-pg-uuidv7-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-pg-uuidv7-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "pg_uuidv7" diff --git a/src/extensions/external/pgtap/release.toml b/src/extensions/external/pgtap/release.toml index 76c83f02..ff8e4393 100644 --- a/src/extensions/external/pgtap/release.toml +++ b/src/extensions/external/pgtap/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-pgtap" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-pgtap-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-pgtap-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "pgtap" diff --git a/src/extensions/external/postgis/release.toml b/src/extensions/external/postgis/release.toml index b4896938..b0b7ea38 100644 --- a/src/extensions/external/postgis/release.toml +++ b/src/extensions/external/postgis/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-postgis" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-postgis-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-postgis-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "postgis" diff --git a/src/extensions/external/vector/release.toml b/src/extensions/external/vector/release.toml index 7549aa2d..94e12945 100644 --- a/src/extensions/external/vector/release.toml +++ b/src/extensions/external/vector/release.toml @@ -1,8 +1,14 @@ id = "oliphaunt-extension-vector" owner = "@oliphaunt/extensions" kind = "exact-extension-artifact" -publish_targets = ["github-release-assets"] -registry_packages = [] +publish_targets = [ + "github-release-assets", + "maven-central", +] +registry_packages = [ + "maven:dev.oliphaunt.extensions:oliphaunt-extension-vector-android-arm64-v8a", + "maven:dev.oliphaunt.extensions:oliphaunt-extension-vector-android-x86_64", +] release_artifacts = ["exact-extension-artifacts"] extension_sql_name = "vector" diff --git a/src/extensions/generated/docs/extension-evidence.json b/src/extensions/generated/docs/extension-evidence.json index d5bf2252..ee585d5d 100644 --- a/src/extensions/generated/docs/extension-evidence.json +++ b/src/extensions/generated/docs/extension-evidence.json @@ -20,7 +20,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -56,7 +56,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -92,7 +92,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -128,7 +128,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -164,7 +164,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -200,7 +200,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -236,7 +236,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -272,7 +272,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -308,7 +308,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -344,7 +344,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -380,7 +380,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -416,7 +416,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -452,7 +452,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -488,7 +488,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -524,7 +524,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -560,7 +560,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -596,7 +596,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -632,7 +632,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -668,7 +668,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -704,7 +704,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -740,7 +740,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -776,7 +776,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -812,7 +812,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -848,7 +848,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -884,7 +884,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -920,7 +920,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -956,7 +956,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -992,7 +992,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -1028,7 +1028,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -1064,7 +1064,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -1100,7 +1100,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -1136,7 +1136,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -1172,7 +1172,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -1208,7 +1208,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -1244,7 +1244,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -1280,7 +1280,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -1316,7 +1316,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -1352,7 +1352,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -1388,7 +1388,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d" + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" } ], "platform-targets": [ @@ -1420,7 +1420,7 @@ "path": "src/extensions/evidence/runs" } ], - "source-digest": "sha256:6e5d3f7efaf456cab126e30a30be819ee894fbdd7a8eb3f88e9055b1810b449d", + "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f", "source-digest-inputs": [ "src/postgres/versions/18/source.toml", "src/extensions/catalog/extensions.promoted.toml", diff --git a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 index d9b5af4c..91847a85 100644 --- a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 +++ b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 @@ -1 +1 @@ -d4a6244ffbb81689848e4090f892c86a34a34453eead7e3eb2f24ef8e91befde +d0fc9d49b00d356052ed91846b9f2f8e495fca79411a85a3ca118ed7f5fb478b diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 651744fd..1192eb78 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -239,13 +239,7 @@ def product_registry_packages(product: str) -> list[str]: packages = config.get("registry_packages", []) if not isinstance(packages, list): fail(f"{product}.registry_packages must be a list") - result = [str(package) for package in packages] - if config.get("kind") == "exact-extension-artifact": - result.extend( - f"maven:dev.oliphaunt.extensions:{product}-{target.target}" - for target in extension_artifact_targets.published_android_maven_targets(product) - ) - return result + return [str(package) for package in packages] def product_publish_targets(product: str) -> list[str]: @@ -1717,12 +1711,11 @@ def check_exact_extension(findings: list[Finding], product: str) -> None: "extension-release-metadata", config.get("kind") == "exact-extension-artifact" and {"github-release-assets", "maven-central"}.issubset(set(product_publish_targets(product))) - and config.get("registry_packages") == [] and set(product_registry_packages(product)) == expected_registry_packages and config.get("release_artifacts") == ["exact-extension-artifacts"] and isinstance(sql_name, str) and sql_name, - "Exact-extension release metadata must publish exact GitHub artifacts and derived Android Maven packages by SQL extension name.", + "Exact-extension release metadata must publish exact GitHub artifacts and explicit Android Maven packages by SQL extension name.", f"{package_path}/release.toml registry_packages={sorted(product_registry_packages(product))!r}", severity="P0", ) diff --git a/tools/release/check_registry_publication.py b/tools/release/check_registry_publication.py index 60e1ee4c..a2089677 100755 --- a/tools/release/check_registry_publication.py +++ b/tools/release/check_registry_publication.py @@ -378,16 +378,13 @@ def product_registry_packages( derived_extension_maven = derived_exact_extension_maven_packages(product, version) if derived_extension_maven: graph_maven = [package for package in packages if package.kind == "maven"] - if graph_maven: - derived_names = sorted(package.name for package in derived_extension_maven) - graph_names = sorted(package.name for package in graph_maven) - if graph_names != derived_names: - fail( - f"{product}.registry_packages maven entries {graph_names} " - f"do not match exact-extension Android artifact targets {derived_names}" - ) - else: - packages.extend(derived_extension_maven) + derived_names = sorted(package.name for package in derived_extension_maven) + graph_names = sorted(package.name for package in graph_maven) + if graph_names != derived_names: + fail( + f"{product}.registry_packages maven entries {graph_names} " + f"do not match exact-extension Android artifact targets {derived_names}" + ) missing_kinds = [] for target, kind in expected_kinds.items(): if target in publish_targets and not any(package.kind == kind for package in packages): diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 2e7d4eca..3e2b7c3a 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -231,10 +231,17 @@ def validate_exact_extension_registry_shape(graph: dict) -> None: config = product_metadata.product_config(product, graph) publish_targets = set(product_metadata.string_list(config, "publish_targets", product)) if not {"github-release-assets", "maven-central"}.issubset(publish_targets): - fail(f"{product} must publish exact-extension GitHub assets and derived Android Maven artifacts") + fail(f"{product} must publish exact-extension GitHub assets and Android Maven artifacts") registry_packages = product_metadata.string_list(config, "registry_packages", product) - if registry_packages: - fail(f"{product} must derive Android Maven registry packages from extension target metadata") + expected_registry_packages = { + f"maven:dev.oliphaunt.extensions:{product}-{target.target}" + for target in extension_artifact_targets.published_android_maven_targets(product) + } + if set(registry_packages) != expected_registry_packages: + fail( + f"{product} registry_packages must explicitly match Android Maven artifact targets: " + + ", ".join(sorted(registry_packages)) + ) android_targets = { target.target for target in extension_artifact_targets.published_android_maven_targets(product) diff --git a/tools/release/product_metadata.py b/tools/release/product_metadata.py index 61bf6ad2..1c0e2247 100644 --- a/tools/release/product_metadata.py +++ b/tools/release/product_metadata.py @@ -250,14 +250,9 @@ def _release_metadata(product: str) -> dict[str, Any]: def _effective_release_metadata(product: str) -> dict[str, Any]: metadata = dict(_release_metadata(product)) - if metadata.get("kind") != "exact-extension-artifact": - return metadata - publish_targets = metadata.get("publish_targets", []) if not isinstance(publish_targets, list) or not all(isinstance(item, str) for item in publish_targets): fail(f"{product}.publish_targets must be a string list") - if "maven-central" not in publish_targets: - metadata["publish_targets"] = [*publish_targets, "maven-central"] return metadata diff --git a/tools/release/sync_release_pr.py b/tools/release/sync_release_pr.py index dc550b42..c6832ad2 100755 --- a/tools/release/sync_release_pr.py +++ b/tools/release/sync_release_pr.py @@ -13,6 +13,7 @@ from pathlib import Path from typing import Any, NoReturn +import extension_artifact_targets import product_metadata @@ -181,6 +182,71 @@ def set_rust_const_string(path: Path, const_name: str, expected: str, context: s fail(f"{context} did not find Rust const {const_name!r} in {rel(path)}") +def toml_array_assignment(key: str, values: list[str]) -> str: + if len(values) == 1: + return f'{key} = [{json.dumps(values[0])}]\n' + lines = [f"{key} = [\n"] + lines.extend(f" {json.dumps(value)},\n" for value in values) + lines.append("]\n") + return "".join(lines) + + +def replace_top_level_array_assignment(text: str, key: str, values: list[str], context: str) -> str: + lines = text.splitlines(keepends=True) + output: list[str] = [] + index = 0 + replaced = False + pattern = re.compile(rf"^{re.escape(key)}\s*=\s*\[") + while index < len(lines): + line = lines[index] + if not replaced and pattern.match(line): + replacement = toml_array_assignment(key, values) + output.append(replacement) + replaced = True + if "]" not in line: + index += 1 + while index < len(lines) and "]" not in lines[index]: + index += 1 + index += 1 + continue + output.append(line) + index += 1 + if not replaced: + fail(f"{context} did not find top-level TOML array {key!r}") + return "".join(output) + + +def sync_extension_maven_registry_metadata(changes: list[Change], *, write: bool) -> None: + expected_publish_targets = ["github-release-assets", "maven-central"] + for product in product_metadata.extension_product_ids(): + path = ROOT / product_metadata.package_path(product) / "release.toml" + expected_registry_packages = [ + f"maven:dev.oliphaunt.extensions:{product}-{target.target}" + for target in extension_artifact_targets.published_android_maven_targets(product) + ] + text = path.read_text(encoding="utf-8") + updated = replace_top_level_array_assignment( + text, + "publish_targets", + expected_publish_targets, + product, + ) + updated = replace_top_level_array_assignment( + updated, + "registry_packages", + expected_registry_packages, + product, + ) + if updated != text: + write_text_if_changed( + path, + updated, + changes, + "synced explicit Maven registry metadata", + write=write, + ) + + def sync_compatibility_versions(changes: list[Change], *, write: bool) -> None: for spec_id, (source_product, path_text, parser) in sorted(product_metadata.compatibility_version_links().items()): path = ROOT / path_text @@ -575,6 +641,7 @@ def main() -> int: changes: list[Change] = [] write = not args.check sync_compatibility_versions(changes, write=write) + sync_extension_maven_registry_metadata(changes, write=write) sync_typescript_optional_runtime_dependencies(changes, write=write) sync_pnpm_typescript_optional_runtime_specifiers(changes, write=write) sync_cargo_path_dependency_pins(changes, write=write) From 433a52d3f46a6c0992a3fb3841b46cbc50a21918 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 02:21:27 +0000 Subject: [PATCH 024/137] fix: derive release artifact downloads from targets --- .github/workflows/release.yml | 29 ++++++---- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 15 +++-- .../examples-ci-release-validation.md | 14 +++-- tools/policy/check-release-policy.py | 7 ++- tools/release/artifact_targets.py | 30 ++++++++++ tools/release/check_artifact_targets.py | 8 +-- tools/release/check_consumer_shape.py | 57 +++++++++++++++---- tools/release/local_registry_publish.py | 31 +++++----- tools/release/release.py | 30 +++++++--- 9 files changed, 154 insertions(+), 67 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 308d08b3..93fa7af8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -463,27 +463,31 @@ jobs: CI_RUN_ID: ${{ steps.ci_build_gate.outputs.run_id }} run: | download_helper_artifacts() { - local prefix="$1" - local destination="$2" + local product="$1" + local kind="$2" + local destination="$3" + local artifact_args=() + while IFS= read -r artifact; do + artifact_args+=(--artifact "$artifact") + done < <(tools/release/release.py ci-artifacts --product "$product" --kind "$kind" --family release-assets) .github/scripts/download-build-artifacts.sh \ CI \ "$RELEASE_HEAD_SHA" \ "$destination" \ --run-id "$CI_RUN_ID" \ --job Builds \ - --artifact "${prefix}-macos-arm64" \ - --artifact "${prefix}-linux-x64-gnu" \ - --artifact "${prefix}-linux-arm64-gnu" \ - --artifact "${prefix}-windows-x64-msvc" + "${artifact_args[@]}" } if [ "$PRODUCT_OLIPHAUNT_BROKER" = "true" ]; then download_helper_artifacts \ - oliphaunt-broker-release-assets \ + oliphaunt-broker \ + broker-helper \ target/oliphaunt-broker/release-assets fi if [ "$PRODUCT_OLIPHAUNT_NODE_DIRECT" = "true" ]; then download_helper_artifacts \ - oliphaunt-node-direct-release-assets \ + oliphaunt-node-direct \ + node-direct-addon \ target/oliphaunt-node-direct/release-assets fi @@ -494,16 +498,17 @@ jobs: GH_REPO: ${{ github.repository }} CI_RUN_ID: ${{ steps.ci_build_gate.outputs.run_id }} run: | + artifact_args=() + while IFS= read -r artifact; do + artifact_args+=(--artifact "$artifact") + done < <(tools/release/release.py ci-artifacts --product oliphaunt-node-direct --kind node-direct-addon --family npm-package) .github/scripts/download-build-artifacts.sh \ CI \ "$RELEASE_HEAD_SHA" \ target/oliphaunt-node-direct/npm-packages \ --run-id "$CI_RUN_ID" \ --job Builds \ - --artifact oliphaunt-node-direct-npm-package-macos-arm64 \ - --artifact oliphaunt-node-direct-npm-package-linux-x64-gnu \ - --artifact oliphaunt-node-direct-npm-package-linux-arm64-gnu \ - --artifact oliphaunt-node-direct-npm-package-windows-x64-msvc + "${artifact_args[@]}" - name: Validate selected release product dry-runs if: ${{ steps.release_plan.outputs.has_release_changes == 'true' }} diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 7bd6839d..8fdd61ed 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -30,7 +30,7 @@ review production pipelines, then normalize implementation details. - [x] Verify WASIX runtime payloads contain `postgres`, `initdb`; WASIX tools payloads contain `pg_dump`, `psql`, not `pg_ctl`. - [ ] Verify extension packages and runtime tools are published and installed from registries idiomatically. - [x] Make extension Maven registry surfaces explicit in extension metadata instead of silently appending them in release tooling. -- [ ] Remove or generate duplicated release target lists in workflow downloads, node-direct package dirs, artifact target checks, and release policy checks. +- [x] Remove or generate duplicated release target lists in workflow downloads, node-direct package dirs, artifact target checks, and release policy checks. - [ ] Decide whether existing-tag release probes should become a uniform idempotency gate or be removed. - [x] Keep release-derived files synchronized after the split tool package changes. @@ -84,11 +84,14 @@ review production pipelines, then normalize implementation details. package coordinates derived from the extension target graph. The old hidden release-tool synthesis path was removed, and release metadata plus consumer shape checks now enforce the explicit package surface. -- Subagent CI/release audit found these remaining next fixes: derive release - artifact downloads from the target graph, remove duplicated node-direct - package target lists, decide whether existing-tag probes are dead or should - become a uniform gate, and collapse literal workflow/policy checks back to - generated package contracts. +- Release workflow helper downloads, node-direct optional npm package downloads, + the local-registry download preset, node-direct package directory validation, + artifact-target checks, and release policy checks now derive native/helper + target artifact names from `artifact_targets` instead of restating the + platform list. +- Subagent CI/release audit found these remaining next fixes: decide whether + existing-tag probes are dead or should become a uniform gate, and collapse + remaining literal workflow/policy checks back to generated package contracts. - Subagent SDK audit found these next fixes: validate Android copied extension files before publishing manifests, align or explicitly document Deno native runtime/tools/extension resolution, port stronger exact-extension validation diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index 6b3fb302..7b45ea63 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -43,7 +43,7 @@ the release/tooling surface after the runtime tool crate split. package-family model per ecosystem. - [x] Make extension Maven registry surfaces explicit in generated extension metadata instead of silently appending them during release. -- [ ] Derive release workflow artifact downloads and node-direct package dirs from the +- [x] Derive release workflow artifact downloads and node-direct package dirs from the same target graph used by CI. - [ ] Decide whether existing-tag probes are a real idempotency gate or dead workflow code. @@ -148,6 +148,10 @@ the release/tooling surface after the runtime tool crate split. consumer-shape checks enforce that those package names match the generated Android extension target graph instead of relying on hidden release-time synthesis. +- Release workflow native helper downloads, Node direct optional package + downloads, the local-registry download preset, and Node direct package-dir + validation now derive artifact/package names from `artifact_targets` instead + of copying the platform target list. - Local GitHub Actions discovery is ready on Linux: `act` v0.2.89, Docker, and `gh` are installed, and `act -l` parses the CI, Release, and mobile E2E workflows. `act workflow_dispatch -W .github/workflows/ci.yml -j release-intent @@ -155,11 +159,9 @@ the release/tooling surface after the runtime tool crate split. expected Linux CI job. Full local lane execution should run from a committed disposable worktree because `actions/checkout` validates committed HEAD, not uncommitted edits. -- A read-only CI/release audit found these remaining next issues: release - workflow downloads re-state target lists that the CI graph already knows, - node-direct package dirs duplicate target metadata, existing-tag release - probes are not consumed, and some policy checks compare copied literals - instead of generated package contracts. +- A read-only CI/release audit found these remaining next issues: existing-tag + release probes are not consumed, and some policy checks compare copied + literals instead of generated package contracts. - A read-only SDK parity audit found these next issues: Android copied runtime manifests can declare missing extensions, Deno native resolution does not follow Node/Bun tools and extension materialization, Android Maven extension diff --git a/tools/policy/check-release-policy.py b/tools/policy/check-release-policy.py index 094dbef0..415781c6 100644 --- a/tools/policy/check-release-policy.py +++ b/tools/policy/check-release-policy.py @@ -728,7 +728,8 @@ def check_release_workflow_policy() -> None: "download_sdk_artifact oliphaunt-react-native oliphaunt-react-native-sdk-package-artifacts", "download_sdk_artifact oliphaunt-js oliphaunt-js-sdk-package-artifacts", "download_sdk_artifact oliphaunt-wasix-rust oliphaunt-wasix-rust-package-artifacts", - "--artifact oliphaunt-node-direct-npm-package-macos-arm64", + "tools/release/release.py ci-artifacts --product \"$product\" --kind \"$kind\" --family release-assets", + "tools/release/release.py ci-artifacts --product oliphaunt-node-direct --kind node-direct-addon --family npm-package", "pnpm install --frozen-lockfile", "target/oliphaunt-broker/release-assets", "target/oliphaunt-node-direct/release-assets", @@ -752,9 +753,11 @@ def check_release_workflow_policy() -> None: # Every release artifact download must come from the selected release # workflow and the builds aggregate, even when wrapped in shell # helper functions. - for required in ("CI", '"$RELEASE_HEAD_SHA"', "--run-id", "--job Builds", "--artifact"): + for required in ("CI", '"$RELEASE_HEAD_SHA"', "--run-id", "--job Builds"): if required not in call_text: fail(f"Release artifact download must require {required}: {call_text[:240]}") + if "--artifact" not in call_text and "artifact_args" not in call_text: + fail(f"Release artifact download must require explicit artifact arguments: {call_text[:240]}") build_artifact_script = read_text(".github/scripts/download-build-artifacts.sh") for snippet in ( diff --git a/tools/release/artifact_targets.py b/tools/release/artifact_targets.py index c2683660..ac83de9f 100644 --- a/tools/release/artifact_targets.py +++ b/tools/release/artifact_targets.py @@ -639,3 +639,33 @@ def expected_assets( if not assets: product_metadata.fail(f"{product} has no artifact targets for surface {surface}") return sorted(assets) + + +def ci_release_asset_artifact_names(product: str, kind: str) -> list[str]: + names = [ + f"{product}-release-assets-{target.target}" + for target in artifact_targets( + product=product, + kind=kind, + surface="github-release", + published_only=True, + ) + ] + if not names: + product_metadata.fail(f"{product} has no published {kind} CI release asset targets") + return sorted(names) + + +def ci_npm_package_artifact_names(product: str, kind: str) -> list[str]: + names = [ + f"{product}-npm-package-{target.target}" + for target in artifact_targets( + product=product, + kind=kind, + surface="npm-optional", + published_only=True, + ) + ] + if not names: + product_metadata.fail(f"{product} has no published {kind} CI npm package targets") + return sorted(names) diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py index a8792afa..a8d0dd1b 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -821,8 +821,8 @@ def validate_ci_release_artifacts() -> None: ) require_text( ".github/workflows/release.yml", - "oliphaunt-broker-release-assets", - "release workflow must name the broker CI artifacts it consumes", + "tools/release/release.py ci-artifacts --product \"$product\" --kind \"$kind\" --family release-assets", + "release workflow must derive native helper release artifact names from target metadata", ) require_text( ".github/workflows/release.yml", @@ -836,8 +836,8 @@ def validate_ci_release_artifacts() -> None: ) require_text( ".github/workflows/release.yml", - "oliphaunt-node-direct-release-assets", - "release workflow must name the Node direct CI artifacts it consumes", + "tools/release/release.py ci-artifacts --product oliphaunt-node-direct --kind node-direct-addon --family npm-package", + "release workflow must derive Node direct npm package artifact names from target metadata", ) require_text( ".github/workflows/release.yml", diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 1192eb78..03b1a6f4 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -250,6 +250,21 @@ def product_publish_targets(product: str) -> list[str]: return [str(target) for target in targets] +def npm_package_dirs(root: str) -> dict[str, str]: + packages: dict[str, str] = {} + for package_json_path in sorted((ROOT / root).glob("*/package.json")): + path = relative(package_json_path) + package = read_json(path) + package_name = package.get("name") + if not isinstance(package_name, str) or not package_name: + fail(f"{path} must declare a package name") + package_dir = relative(package_json_path.parent) + if package_name in packages: + fail(f"duplicate npm package name {package_name}: {packages[package_name]} and {package_dir}") + packages[package_name] = package_dir + return packages + + def check_npm_package_common( findings: list[Finding], product: str, @@ -875,38 +890,58 @@ def check_node_direct(findings: list[Finding]) -> None: severity="P0", ) + node_targets = artifact_targets.artifact_targets( + product=product, + kind="node-direct-addon", + surface="npm-optional", + published_only=True, + ) expected_packages = { - "darwin-arm64": ("@oliphaunt/node-direct-darwin-arm64", ("darwin",), ("arm64",), None), - "linux-x64-gnu": ("@oliphaunt/node-direct-linux-x64-gnu", ("linux",), ("x64",), ("glibc",)), - "linux-arm64-gnu": ("@oliphaunt/node-direct-linux-arm64-gnu", ("linux",), ("arm64",), ("glibc",)), - "win32-x64-msvc": ("@oliphaunt/node-direct-win32-x64-msvc", ("win32",), ("x64",), None), + target.npm_package: target + for target in node_targets + if target.npm_package is not None and target.npm_os is not None and target.npm_cpu is not None } require( findings, product, "registry-packages", - set(product_registry_packages(product)) == {f"npm:{name}" for name, _os, _cpu, _libc in expected_packages.values()}, + len(expected_packages) == len(node_targets) + and set(product_registry_packages(product)) == {f"npm:{name}" for name in expected_packages}, "Node direct release metadata must publish exactly the optional platform npm packages.", f"src/runtimes/node-direct/release.toml registry_packages={product_registry_packages(product)!r}", severity="P0", ) - for directory, (package_name, expected_os, expected_cpu, expected_libc) in expected_packages.items(): - package_path = f"src/runtimes/node-direct/packages/{directory}/package.json" + package_dirs = npm_package_dirs("src/runtimes/node-direct/packages") + require( + findings, + product, + "platform-package-dirs", + set(package_dirs) == set(expected_packages), + "Node direct package directories must match published artifact target npm packages exactly.", + f"src/runtimes/node-direct/packages package names={sorted(package_dirs)!r}", + severity="P0", + ) + for package_name, target in expected_packages.items(): + package_dir = package_dirs.get(package_name) + if package_dir is None: + continue + package_path = f"{package_dir}/package.json" optional_package = check_npm_package_common( findings, product, package_path, package_name, - f"src/runtimes/node-direct/packages/{directory}", + package_dir, ) + expected_libc = [target.npm_libc] if target.npm_libc is not None else None require( findings, product, "node-direct-platform-package", optional_package.get("optional") is True - and optional_package.get("os") == list(expected_os) - and optional_package.get("cpu") == list(expected_cpu) - and (expected_libc is None or optional_package.get("libc") == list(expected_libc)), + and optional_package.get("os") == [target.npm_os] + and optional_package.get("cpu") == [target.npm_cpu] + and (expected_libc is None or optional_package.get("libc") == expected_libc), "Node direct platform packages must constrain npm installation to the matching OS, CPU, and libc.", f"{package_path}: os={optional_package.get('os')!r} cpu={optional_package.get('cpu')!r} libc={optional_package.get('libc')!r}", severity="P0", diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index 21ca140f..5d021ecb 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -34,6 +34,8 @@ from pathlib import Path from typing import Any, Iterable +import artifact_targets + ROOT = Path(__file__).resolve().parents[2] DEFAULT_RUN_ID = "28049923289" @@ -53,15 +55,8 @@ "oliphaunt-wasix-aot-x86_64-unknown-linux-gnu", } -LOCAL_PUBLISH_ARTIFACTS = [ +STATIC_LOCAL_PUBLISH_ARTIFACTS = [ "liboliphaunt-native-release-assets", - "liboliphaunt-native-release-assets-android-arm64-v8a", - "liboliphaunt-native-release-assets-android-x86_64", - "liboliphaunt-native-release-assets-ios-xcframework", - "liboliphaunt-native-release-assets-linux-arm64-gnu", - "liboliphaunt-native-release-assets-linux-x64-gnu", - "liboliphaunt-native-release-assets-macos-arm64", - "liboliphaunt-native-release-assets-windows-x64-msvc", "liboliphaunt-wasix-extension-artifacts-wasix-portable", "liboliphaunt-wasix-release-assets", "liboliphaunt-wasix-runtime-aot-linux-arm64-gnu", @@ -81,17 +76,19 @@ "oliphaunt-kotlin-sdk-package-artifacts", "oliphaunt-swift-sdk-package-artifacts", "oliphaunt-mobile-extension-package-artifacts", - "oliphaunt-node-direct-npm-package-linux-x64-gnu", - "oliphaunt-node-direct-npm-package-linux-arm64-gnu", - "oliphaunt-node-direct-npm-package-macos-arm64", - "oliphaunt-node-direct-npm-package-windows-x64-msvc", - "oliphaunt-node-direct-release-assets-linux-arm64-gnu", - "oliphaunt-node-direct-release-assets-linux-x64-gnu", - "oliphaunt-node-direct-release-assets-macos-arm64", - "oliphaunt-node-direct-release-assets-windows-x64-msvc", ] +def local_publish_artifacts() -> list[str]: + return [ + *STATIC_LOCAL_PUBLISH_ARTIFACTS, + *artifact_targets.ci_release_asset_artifact_names("liboliphaunt-native", "native-runtime"), + *artifact_targets.ci_release_asset_artifact_names("oliphaunt-broker", "broker-helper"), + *artifact_targets.ci_release_asset_artifact_names("oliphaunt-node-direct", "node-direct-addon"), + *artifact_targets.ci_npm_package_artifact_names("oliphaunt-node-direct", "node-direct-addon"), + ] + + def rel(path: Path) -> str: try: return str(path.relative_to(ROOT)) @@ -186,7 +183,7 @@ def list_ci_artifacts(repo: str, run_id: str) -> list[dict[str, Any]]: def download_artifacts(args: argparse.Namespace) -> None: artifacts = list(args.artifact) if args.preset == "local-publish": - artifacts.extend(LOCAL_PUBLISH_ARTIFACTS) + artifacts.extend(local_publish_artifacts()) artifacts = sorted(set(artifacts)) if not artifacts: print("No artifacts selected; pass --artifact or --preset local-publish.", file=sys.stderr) diff --git a/tools/release/release.py b/tools/release/release.py index 14a5cef8..7def0407 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -30,12 +30,7 @@ ROOT = Path(__file__).resolve().parents[2] EXTENSION_PRODUCT_PREFIX = "oliphaunt-extension-" -NODE_DIRECT_PACKAGE_DIRS = { - "@oliphaunt/node-direct-darwin-arm64": ROOT / "src/runtimes/node-direct/packages/darwin-arm64", - "@oliphaunt/node-direct-linux-x64-gnu": ROOT / "src/runtimes/node-direct/packages/linux-x64-gnu", - "@oliphaunt/node-direct-linux-arm64-gnu": ROOT / "src/runtimes/node-direct/packages/linux-arm64-gnu", - "@oliphaunt/node-direct-win32-x64-msvc": ROOT / "src/runtimes/node-direct/packages/win32-x64-msvc", -} +NODE_DIRECT_PACKAGE_ROOT = ROOT / "src/runtimes/node-direct/packages" def fail(message: str) -> NoReturn: @@ -1664,6 +1659,20 @@ def command_consumer_shape(args: list[str]) -> None: raise SystemExit(result.returncode) +def command_ci_artifacts(args: list[str]) -> None: + parser = argparse.ArgumentParser(description="Emit CI artifact names derived from release target metadata.") + parser.add_argument("--product", required=True) + parser.add_argument("--kind", required=True) + parser.add_argument("--family", choices=["release-assets", "npm-package"], required=True) + parsed = parser.parse_args(args) + if parsed.family == "release-assets": + names = artifact_targets.ci_release_asset_artifact_names(parsed.product, parsed.kind) + else: + names = artifact_targets.ci_npm_package_artifact_names(parsed.product, parsed.kind) + for name in names: + print(name) + + def consumer_shape_scope_args(args: list[str]) -> list[str]: scoped: list[str] = [] index = 0 @@ -1905,6 +1914,7 @@ def publish_node_direct_release_assets(head_ref: str) -> None: def node_direct_optional_package_targets(version: str) -> list[tuple[str, Path, artifact_targets.ArtifactTarget]]: + package_dirs = npm_package_dirs_under(NODE_DIRECT_PACKAGE_ROOT) packages: list[tuple[str, Path, artifact_targets.ArtifactTarget]] = [] for target in artifact_targets.artifact_targets( product="oliphaunt-node-direct", @@ -1915,7 +1925,7 @@ def node_direct_optional_package_targets(version: str) -> list[tuple[str, Path, package_name = target.npm_package if package_name is None: fail(f"{target.id} must declare npm_package for npm optional package publication") - package_dir = NODE_DIRECT_PACKAGE_DIRS.get(package_name) + package_dir = package_dirs.get(package_name) if package_dir is None: fail(f"{target.id} declares unknown Node direct npm package {package_name}") package_json = json.loads((package_dir / "package.json").read_text(encoding="utf-8")) @@ -1924,7 +1934,7 @@ def node_direct_optional_package_targets(version: str) -> list[tuple[str, Path, if package_json.get("version") != version: fail(f"{package_name} package version must match oliphaunt-node-direct {version}") packages.append((package_name, package_dir, target)) - if sorted(package for package, _, _ in packages) != sorted(NODE_DIRECT_PACKAGE_DIRS): + if sorted(package for package, _, _ in packages) != sorted(package_dirs): fail("Node direct npm optional package metadata must match published artifact targets exactly") return packages @@ -3140,7 +3150,7 @@ def main(argv: list[str]) -> int: parser = argparse.ArgumentParser(description=__doc__) subparsers = parser.add_subparsers(dest="command", required=True) - for name in ["plan", "check", "check-registries", "consumer-shape", "verify-release"]: + for name in ["plan", "check", "check-registries", "consumer-shape", "ci-artifacts", "verify-release"]: subparsers.add_parser(name, add_help=False) dry_run = subparsers.add_parser("publish-dry-run") @@ -3166,6 +3176,8 @@ def main(argv: list[str]) -> int: command_check_registries(passthrough) elif command == "consumer-shape": command_consumer_shape(passthrough) + elif command == "ci-artifacts": + command_ci_artifacts(passthrough) elif command == "verify-release": command_verify_release(passthrough) elif command == "publish-dry-run": From 56e6cc8a31a6c9a3ea27c9132938c6326c101f20 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 02:29:22 +0000 Subject: [PATCH 025/137] fix: remove unused release tag probes --- .github/workflows/release.yml | 15 ------------- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 11 ++++++---- .../examples-ci-release-validation.md | 9 ++++---- tools/release/release.py | 22 +------------------ 4 files changed, 13 insertions(+), 44 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 93fa7af8..2f5d44fa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -339,21 +339,6 @@ jobs: PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} run: tools/release/release.py check-registries --products-json "${PRODUCTS_JSON}" --head-ref "$RELEASE_HEAD_SHA" - - name: Check existing WASIX runtime release tag - if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && steps.release_plan.outputs.product_liboliphaunt_wasix == 'true' }} - id: wasix_runtime_existing_tag - run: tools/release/release.py publish --product liboliphaunt-wasix --step existing-tag --head-ref "$RELEASE_HEAD_SHA" --format github-output >> "$GITHUB_OUTPUT" - - - name: Check existing WASIX Rust binding release tag - if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && steps.release_plan.outputs.product_oliphaunt_wasix_rust == 'true' }} - id: wasix_rust_existing_tag - run: tools/release/release.py publish --product oliphaunt-wasix-rust --step existing-tag --head-ref "$RELEASE_HEAD_SHA" --format github-output >> "$GITHUB_OUTPUT" - - - name: Check existing Rust SDK release tag - if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && steps.release_plan.outputs.product_oliphaunt_rust == 'true' }} - id: rust_existing_tag - run: tools/release/release.py publish --product oliphaunt-rust --step existing-tag --head-ref "$RELEASE_HEAD_SHA" --format github-output >> "$GITHUB_OUTPUT" - - name: Download WASIX runtime build artifacts if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && steps.release_plan.outputs.product_liboliphaunt_wasix == 'true' }} env: diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 8fdd61ed..572eec0a 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -31,7 +31,7 @@ review production pipelines, then normalize implementation details. - [ ] Verify extension packages and runtime tools are published and installed from registries idiomatically. - [x] Make extension Maven registry surfaces explicit in extension metadata instead of silently appending them in release tooling. - [x] Remove or generate duplicated release target lists in workflow downloads, node-direct package dirs, artifact target checks, and release policy checks. -- [ ] Decide whether existing-tag release probes should become a uniform idempotency gate or be removed. +- [x] Decide whether existing-tag release probes should become a uniform idempotency gate or be removed. - [x] Keep release-derived files synchronized after the split tool package changes. ## Priority 3: SDK Consistency @@ -89,9 +89,12 @@ review production pipelines, then normalize implementation details. artifact-target checks, and release policy checks now derive native/helper target artifact names from `artifact_targets` instead of restating the platform list. -- Subagent CI/release audit found these remaining next fixes: decide whether - existing-tag probes are dead or should become a uniform gate, and collapse - remaining literal workflow/policy checks back to generated package contracts. +- Dead existing-tag release workflow probes were removed. Idempotent rerun + behavior stays in the publish handlers that actually own registry/GitHub + publication, such as matching GitHub asset checksum skips and already-published + crates/npm checks. +- Subagent CI/release audit found these remaining next fixes: collapse remaining + literal workflow/policy checks back to generated package contracts. - Subagent SDK audit found these next fixes: validate Android copied extension files before publishing manifests, align or explicitly document Deno native runtime/tools/extension resolution, port stronger exact-extension validation diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index 7b45ea63..b219986e 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -45,7 +45,7 @@ the release/tooling surface after the runtime tool crate split. instead of silently appending them during release. - [x] Derive release workflow artifact downloads and node-direct package dirs from the same target graph used by CI. -- [ ] Decide whether existing-tag probes are a real idempotency gate or dead workflow +- [x] Decide whether existing-tag probes are a real idempotency gate or dead workflow code. - [ ] Validate local Linux CI lanes with a local GitHub Actions runner when practical. - [ ] Document local runner limitations instead of pretending macOS, Windows, iOS, or @@ -152,6 +152,8 @@ the release/tooling surface after the runtime tool crate split. downloads, the local-registry download preset, and Node direct package-dir validation now derive artifact/package names from `artifact_targets` instead of copying the platform target list. +- Dead existing-tag workflow probes were removed; rerun idempotency remains in + the publish handlers that own the actual registry or GitHub publication step. - Local GitHub Actions discovery is ready on Linux: `act` v0.2.89, Docker, and `gh` are installed, and `act -l` parses the CI, Release, and mobile E2E workflows. `act workflow_dispatch -W .github/workflows/ci.yml -j release-intent @@ -159,9 +161,8 @@ the release/tooling surface after the runtime tool crate split. expected Linux CI job. Full local lane execution should run from a committed disposable worktree because `actions/checkout` validates committed HEAD, not uncommitted edits. -- A read-only CI/release audit found these remaining next issues: existing-tag - release probes are not consumed, and some policy checks compare copied - literals instead of generated package contracts. +- A read-only CI/release audit found this remaining issue: some policy checks + compare copied literals instead of generated package contracts. - A read-only SDK parity audit found these next issues: Android copied runtime manifests can declare missing extensions, Deno native resolution does not follow Node/Bun tools and extension materialization, Android Maven extension diff --git a/tools/release/release.py b/tools/release/release.py index 7def0407..25b06475 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -388,11 +388,6 @@ def extension_sql_name(product: str) -> str: return value -def github_output(values: dict[str, str]) -> None: - for key, value in values.items(): - print(f"{key}={value}") - - def current_product_version(product: str) -> str: return product_metadata.read_current_version(product) @@ -1696,18 +1691,6 @@ def command_verify_release(args: list[str]) -> None: run(["tools/release/verify_github_release_attestations.py", *args]) -def publish_existing_tag_outputs(product: str, head_ref: str, fmt: str) -> None: - values = { - "tag": product_tag(product), - "exists_at_head": "true" if published_rerun(product, head_ref) else "false", - } - if fmt == "github-output": - github_output(values) - return - for key, value in values.items(): - print(f"{key}: {value}") - - def publish_liboliphaunt_github_assets(head_ref: str) -> None: verify_release_tag("liboliphaunt-native", head_ref) ensure_liboliphaunt_release_assets() @@ -3067,9 +3050,7 @@ def command_publish_product_step(args: argparse.Namespace) -> None: if product not in known: fail(f"unknown release product: {product}") - if step == "existing-tag": - publish_existing_tag_outputs(product, head_ref, args.format) - elif product == "liboliphaunt-native" and step == "github-release-assets": + if product == "liboliphaunt-native" and step == "github-release-assets": publish_liboliphaunt_github_assets(head_ref) elif product == "liboliphaunt-native" and step == "npm": publish_liboliphaunt_npm_packages(head_ref) @@ -3163,7 +3144,6 @@ def main(argv: list[str]) -> int: publish.add_argument("--product") publish.add_argument("--step") publish.add_argument("--head-ref", default="HEAD") - publish.add_argument("--format", choices=["text", "github-output"], default="text") args, passthrough = parser.parse_known_args(argv) command = args.command From 57b76190acfe8542c58689941a580063b6967a7c Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 02:36:25 +0000 Subject: [PATCH 026/137] fix: derive js optional runtime packages --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 4 ++ .../examples-ci-release-validation.md | 3 ++ tools/release/artifact_targets.py | 33 ++++++++++++ tools/release/check_consumer_shape.py | 19 +------ tools/release/check_release_metadata.py | 19 +------ tools/release/sync_release_pr.py | 50 ++++--------------- 6 files changed, 53 insertions(+), 75 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 572eec0a..1f711c3a 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -93,6 +93,10 @@ review production pipelines, then normalize implementation details. behavior stays in the publish handlers that actually own registry/GitHub publication, such as matching GitHub asset checksum skips and already-published crates/npm checks. +- TypeScript optional runtime package validation and release PR sync now derive + broker, native runtime, native tools, and node-direct optional packages from + `artifact_targets`, instead of maintaining a separate package/version map in + each checker. - Subagent CI/release audit found these remaining next fixes: collapse remaining literal workflow/policy checks back to generated package contracts. - Subagent SDK audit found these next fixes: validate Android copied extension diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index b219986e..c8c11fb1 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -154,6 +154,9 @@ the release/tooling surface after the runtime tool crate split. of copying the platform target list. - Dead existing-tag workflow probes were removed; rerun idempotency remains in the publish handlers that own the actual registry or GitHub publication step. +- TypeScript optional runtime package validation and release PR sync now share + the `artifact_targets` package map for broker, native runtime/tools, and + node-direct optional packages. - Local GitHub Actions discovery is ready on Linux: `act` v0.2.89, Docker, and `gh` are installed, and `act -l` parses the CI, Release, and mobile E2E workflows. `act workflow_dispatch -W .github/workflows/ci.yml -j release-intent diff --git a/tools/release/artifact_targets.py b/tools/release/artifact_targets.py index ac83de9f..d9adde37 100644 --- a/tools/release/artifact_targets.py +++ b/tools/release/artifact_targets.py @@ -669,3 +669,36 @@ def ci_npm_package_artifact_names(product: str, kind: str) -> list[str]: if not names: product_metadata.fail(f"{product} has no published {kind} CI npm package targets") return sorted(names) + + +def typescript_optional_runtime_package_products() -> dict[str, str]: + package_products: dict[str, str] = {} + selectors = [ + ("oliphaunt-broker", "broker-helper", "typescript-broker"), + ("liboliphaunt-native", "native-runtime", "typescript-native-direct"), + ("liboliphaunt-native", "native-tools", "typescript-native-direct"), + ("oliphaunt-node-direct", "node-direct-addon", "npm-optional"), + ] + for product, kind, surface in selectors: + targets = artifact_targets( + product=product, + kind=kind, + surface=surface, + published_only=True, + ) + if not targets: + product_metadata.fail(f"{product} has no published {kind} TypeScript optional package targets") + for target in targets: + if target.npm_package is None: + product_metadata.fail(f"{target.id} must declare npm_package for TypeScript optional dependencies") + if target.npm_package in package_products: + product_metadata.fail(f"duplicate TypeScript optional package target {target.npm_package}") + package_products[target.npm_package] = target.product + return dict(sorted(package_products.items())) + + +def typescript_optional_runtime_package_versions() -> dict[str, str]: + return { + package_name: product_metadata.read_current_version(product) + for package_name, product in typescript_optional_runtime_package_products().items() + } diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 03b1a6f4..de81ab27 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -1268,24 +1268,7 @@ def check_typescript(findings: list[Finding]) -> None: f"src/sdks/js/package.json dependencies={package.get('dependencies')!r}", severity="P0", ) - expected_optional = { - "@oliphaunt/broker-darwin-arm64": product_metadata.read_current_version("oliphaunt-broker"), - "@oliphaunt/broker-linux-x64-gnu": product_metadata.read_current_version("oliphaunt-broker"), - "@oliphaunt/broker-linux-arm64-gnu": product_metadata.read_current_version("oliphaunt-broker"), - "@oliphaunt/broker-win32-x64-msvc": product_metadata.read_current_version("oliphaunt-broker"), - "@oliphaunt/liboliphaunt-darwin-arm64": product_metadata.read_current_version("liboliphaunt-native"), - "@oliphaunt/liboliphaunt-linux-x64-gnu": product_metadata.read_current_version("liboliphaunt-native"), - "@oliphaunt/liboliphaunt-linux-arm64-gnu": product_metadata.read_current_version("liboliphaunt-native"), - "@oliphaunt/liboliphaunt-win32-x64-msvc": product_metadata.read_current_version("liboliphaunt-native"), - "@oliphaunt/node-direct-darwin-arm64": product_metadata.read_current_version("oliphaunt-node-direct"), - "@oliphaunt/node-direct-linux-x64-gnu": product_metadata.read_current_version("oliphaunt-node-direct"), - "@oliphaunt/node-direct-linux-arm64-gnu": product_metadata.read_current_version("oliphaunt-node-direct"), - "@oliphaunt/node-direct-win32-x64-msvc": product_metadata.read_current_version("oliphaunt-node-direct"), - "@oliphaunt/tools-darwin-arm64": product_metadata.read_current_version("liboliphaunt-native"), - "@oliphaunt/tools-linux-x64-gnu": product_metadata.read_current_version("liboliphaunt-native"), - "@oliphaunt/tools-linux-arm64-gnu": product_metadata.read_current_version("liboliphaunt-native"), - "@oliphaunt/tools-win32-x64-msvc": product_metadata.read_current_version("liboliphaunt-native"), - } + expected_optional = artifact_targets.typescript_optional_runtime_package_versions() optional_dependencies = package.get("optionalDependencies", {}) require( findings, diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 3e2b7c3a..00f7d9f0 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -757,24 +757,7 @@ def validate_typescript( dependencies = package.get("dependencies", {}) if dependencies not in ({}, None): fail("TypeScript SDK must not declare regular runtime artifact dependencies") - expected_optional = { - "@oliphaunt/broker-darwin-arm64": broker_version, - "@oliphaunt/broker-linux-x64-gnu": broker_version, - "@oliphaunt/broker-linux-arm64-gnu": broker_version, - "@oliphaunt/broker-win32-x64-msvc": broker_version, - "@oliphaunt/liboliphaunt-darwin-arm64": liboliphaunt_version, - "@oliphaunt/liboliphaunt-linux-x64-gnu": liboliphaunt_version, - "@oliphaunt/liboliphaunt-linux-arm64-gnu": liboliphaunt_version, - "@oliphaunt/liboliphaunt-win32-x64-msvc": liboliphaunt_version, - "@oliphaunt/node-direct-darwin-arm64": node_direct_version, - "@oliphaunt/node-direct-linux-x64-gnu": node_direct_version, - "@oliphaunt/node-direct-linux-arm64-gnu": node_direct_version, - "@oliphaunt/node-direct-win32-x64-msvc": node_direct_version, - "@oliphaunt/tools-darwin-arm64": liboliphaunt_version, - "@oliphaunt/tools-linux-x64-gnu": liboliphaunt_version, - "@oliphaunt/tools-linux-arm64-gnu": liboliphaunt_version, - "@oliphaunt/tools-win32-x64-msvc": liboliphaunt_version, - } + expected_optional = artifact_targets.typescript_optional_runtime_package_versions() optional_dependencies = package.get("optionalDependencies", {}) if not isinstance(optional_dependencies, dict) or set(optional_dependencies) != set(expected_optional): fail("TypeScript package.json must declare exactly the runtime optional platform packages") diff --git a/tools/release/sync_release_pr.py b/tools/release/sync_release_pr.py index c6832ad2..ce74f5ea 100755 --- a/tools/release/sync_release_pr.py +++ b/tools/release/sync_release_pr.py @@ -13,45 +13,12 @@ from pathlib import Path from typing import Any, NoReturn +import artifact_targets import extension_artifact_targets import product_metadata ROOT = Path(__file__).resolve().parents[2] -TYPESCRIPT_OPTIONAL_RUNTIME_PACKAGES_BY_PRODUCT = { - "oliphaunt-broker": [ - "@oliphaunt/broker-darwin-arm64", - "@oliphaunt/broker-linux-arm64-gnu", - "@oliphaunt/broker-linux-x64-gnu", - "@oliphaunt/broker-win32-x64-msvc", - ], - "liboliphaunt-native": [ - "@oliphaunt/liboliphaunt-darwin-arm64", - "@oliphaunt/liboliphaunt-linux-arm64-gnu", - "@oliphaunt/liboliphaunt-linux-x64-gnu", - "@oliphaunt/liboliphaunt-win32-x64-msvc", - "@oliphaunt/tools-darwin-arm64", - "@oliphaunt/tools-linux-arm64-gnu", - "@oliphaunt/tools-linux-x64-gnu", - "@oliphaunt/tools-win32-x64-msvc", - ], - "oliphaunt-node-direct": [ - "@oliphaunt/node-direct-darwin-arm64", - "@oliphaunt/node-direct-linux-arm64-gnu", - "@oliphaunt/node-direct-linux-x64-gnu", - "@oliphaunt/node-direct-win32-x64-msvc", - ], -} -TYPESCRIPT_OPTIONAL_RUNTIME_PACKAGES = [ - package_name - for packages in TYPESCRIPT_OPTIONAL_RUNTIME_PACKAGES_BY_PRODUCT.values() - for package_name in packages -] -TYPESCRIPT_OPTIONAL_RUNTIME_PACKAGE_TO_PRODUCT = { - package_name: product - for product, packages in TYPESCRIPT_OPTIONAL_RUNTIME_PACKAGES_BY_PRODUCT.items() - for package_name in packages -} DEPENDENCY_TABLES = ("dependencies", "dev-dependencies", "build-dependencies") LOCKFILES = [ ROOT / "Cargo.lock", @@ -282,28 +249,33 @@ def sync_compatibility_versions(changes: list[Change], *, write: bool) -> None: def expected_typescript_optional_runtime_versions() -> dict[str, str]: return { package_name: f"workspace:{product_metadata.read_current_version(product)}" - for package_name, product in TYPESCRIPT_OPTIONAL_RUNTIME_PACKAGE_TO_PRODUCT.items() + for package_name, product in artifact_targets.typescript_optional_runtime_package_products().items() } +def typescript_optional_runtime_packages() -> list[str]: + return list(artifact_targets.typescript_optional_runtime_package_products()) + + def sync_typescript_optional_runtime_dependencies(changes: list[Change], *, write: bool) -> None: path = ROOT / "src/sdks/js/package.json" data = read_json_object(path) optional = data.get("optionalDependencies") if not isinstance(optional, dict): fail(f"{rel(path)} must declare optionalDependencies") - expected_keys = set(TYPESCRIPT_OPTIONAL_RUNTIME_PACKAGES) + expected_packages = typescript_optional_runtime_packages() + expected_keys = set(expected_packages) actual_keys = set(optional) if actual_keys != expected_keys: fail( f"{rel(path)} optionalDependencies must be exactly " - f"{', '.join(TYPESCRIPT_OPTIONAL_RUNTIME_PACKAGES)}" + f"{', '.join(expected_packages)}" ) expected_versions = expected_typescript_optional_runtime_versions() changed = False details = [] - for package_name in TYPESCRIPT_OPTIONAL_RUNTIME_PACKAGES: + for package_name in expected_packages: expected_version = expected_versions[package_name] actual = optional.get(package_name) if actual != expected_version: @@ -317,7 +289,7 @@ def sync_typescript_optional_runtime_dependencies(changes: list[Change], *, writ def sync_pnpm_typescript_optional_runtime_specifiers(changes: list[Change], *, write: bool) -> None: expected_versions = expected_typescript_optional_runtime_versions() lines = PNPM_LOCKFILE.read_text(encoding="utf-8").splitlines(keepends=True) - expected_packages = set(TYPESCRIPT_OPTIONAL_RUNTIME_PACKAGES) + expected_packages = set(typescript_optional_runtime_packages()) seen: set[str] = set() file_changes: list[str] = [] From 8bd51107c9f302b5a3ecbde87979f6d0976419b0 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 02:42:46 +0000 Subject: [PATCH 027/137] fix: derive runtime registry package checks --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 4 + .../examples-ci-release-validation.md | 3 + tools/release/check_consumer_shape.py | 92 ++++++++++++------- 3 files changed, 66 insertions(+), 33 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 1f711c3a..667b2557 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -97,6 +97,10 @@ review production pipelines, then normalize implementation details. broker, native runtime, native tools, and node-direct optional packages from `artifact_targets`, instead of maintaining a separate package/version map in each checker. +- Consumer-shape registry package checks for `liboliphaunt-native` and + `oliphaunt-broker` now derive platform target membership and npm package + names from `artifact_targets`, with only registry naming conventions kept in + the checker. - Subagent CI/release audit found these remaining next fixes: collapse remaining literal workflow/policy checks back to generated package contracts. - Subagent SDK audit found these next fixes: validate Android copied extension diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index c8c11fb1..b93c6ad4 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -157,6 +157,9 @@ the release/tooling surface after the runtime tool crate split. - TypeScript optional runtime package validation and release PR sync now share the `artifact_targets` package map for broker, native runtime/tools, and node-direct optional packages. +- Consumer-shape registry package checks for `liboliphaunt-native` and + `oliphaunt-broker` now derive platform target membership and npm package + names from `artifact_targets`. - Local GitHub Actions discovery is ready on Linux: `act` v0.2.89, Docker, and `gh` are installed, and `act -l` parses the CI, Release, and mobile E2E workflows. `act workflow_dispatch -W .github/workflows/ci.yml -j release-intent diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index de81ab27..b34cbefa 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -250,6 +250,63 @@ def product_publish_targets(product: str) -> list[str]: return [str(target) for target in targets] +def npm_registry_packages(product: str, kind: str, surface: str) -> set[str]: + packages = set() + for target in artifact_targets.artifact_targets( + product=product, + kind=kind, + surface=surface, + published_only=True, + ): + if target.npm_package is None: + fail(f"{target.id} must declare npm_package for {surface}") + packages.add(f"npm:{target.npm_package}") + return packages + + +def liboliphaunt_native_expected_registry_packages() -> set[str]: + runtime_targets = artifact_targets.artifact_targets( + product="liboliphaunt-native", + kind="native-runtime", + surface="rust-native-direct", + published_only=True, + ) + tools_targets = artifact_targets.artifact_targets( + product="liboliphaunt-native", + kind="native-tools", + surface="typescript-native-direct", + published_only=True, + ) + android_targets = artifact_targets.artifact_targets( + product="liboliphaunt-native", + kind="native-runtime", + surface="maven", + published_only=True, + ) + return { + "npm:@oliphaunt/icu", + "maven:dev.oliphaunt.runtime:oliphaunt-icu", + "maven:dev.oliphaunt.runtime:liboliphaunt-runtime-resources", + *{f"crates:liboliphaunt-native-{target.target}" for target in runtime_targets}, + *{f"crates:oliphaunt-tools-{target.target}" for target in tools_targets}, + *npm_registry_packages("liboliphaunt-native", "native-runtime", "typescript-native-direct"), + *npm_registry_packages("liboliphaunt-native", "native-tools", "typescript-native-direct"), + *{f"maven:dev.oliphaunt.runtime:liboliphaunt-{target.target}" for target in android_targets}, + } + + +def broker_expected_registry_packages() -> set[str]: + targets = artifact_targets.artifact_targets( + product="oliphaunt-broker", + kind="broker-helper", + published_only=True, + ) + return { + *{f"crates:oliphaunt-broker-{target.target}" for target in targets}, + *npm_registry_packages("oliphaunt-broker", "broker-helper", "typescript-broker"), + } + + def npm_package_dirs(root: str) -> dict[str, str]: packages: dict[str, str] = {} for package_json_path in sorted((ROOT / root).glob("*/package.json")): @@ -340,29 +397,7 @@ def check_liboliphaunt(findings: list[Finding]) -> None: f"src/runtimes/liboliphaunt/native/VERSION={version!r}", severity="P0", ) - expected_registry_packages = { - "crates:liboliphaunt-native-linux-arm64-gnu", - "crates:liboliphaunt-native-linux-x64-gnu", - "crates:liboliphaunt-native-macos-arm64", - "crates:liboliphaunt-native-windows-x64-msvc", - "crates:oliphaunt-tools-linux-arm64-gnu", - "crates:oliphaunt-tools-linux-x64-gnu", - "crates:oliphaunt-tools-macos-arm64", - "crates:oliphaunt-tools-windows-x64-msvc", - "npm:@oliphaunt/icu", - "npm:@oliphaunt/liboliphaunt-darwin-arm64", - "npm:@oliphaunt/liboliphaunt-linux-x64-gnu", - "npm:@oliphaunt/liboliphaunt-linux-arm64-gnu", - "npm:@oliphaunt/liboliphaunt-win32-x64-msvc", - "npm:@oliphaunt/tools-darwin-arm64", - "npm:@oliphaunt/tools-linux-arm64-gnu", - "npm:@oliphaunt/tools-linux-x64-gnu", - "npm:@oliphaunt/tools-win32-x64-msvc", - "maven:dev.oliphaunt.runtime:oliphaunt-icu", - "maven:dev.oliphaunt.runtime:liboliphaunt-runtime-resources", - "maven:dev.oliphaunt.runtime:liboliphaunt-android-arm64-v8a", - "maven:dev.oliphaunt.runtime:liboliphaunt-android-x86_64", - } + expected_registry_packages = liboliphaunt_native_expected_registry_packages() require( findings, product, @@ -759,16 +794,7 @@ def check_broker(findings: list[Finding]) -> None: "src/runtimes/broker/release.toml", severity="P0", ) - expected_registry_packages = { - "crates:oliphaunt-broker-linux-arm64-gnu", - "crates:oliphaunt-broker-linux-x64-gnu", - "crates:oliphaunt-broker-macos-arm64", - "crates:oliphaunt-broker-windows-x64-msvc", - "npm:@oliphaunt/broker-darwin-arm64", - "npm:@oliphaunt/broker-linux-x64-gnu", - "npm:@oliphaunt/broker-linux-arm64-gnu", - "npm:@oliphaunt/broker-win32-x64-msvc", - } + expected_registry_packages = broker_expected_registry_packages() require( findings, product, From 32f445667cac71d3f7d4586e1e6a808b9fc08e3b Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 02:58:42 +0000 Subject: [PATCH 028/137] fix: validate android extension runtime files --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 16 +++++---- .../examples-ci-release-validation.md | 16 +++++---- .../ResolveOliphauntAndroidAssetsTask.java | 34 +++++++++++++----- src/sdks/kotlin/oliphaunt/build.gradle.kts | 25 +++++++++++++ src/sdks/kotlin/tools/check-sdk.sh | 35 ++++++++++++++++++- .../check-sdk-mobile-extension-surface.sh | 6 +++- 6 files changed, 110 insertions(+), 22 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 667b2557..97e6e7a5 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -38,7 +38,7 @@ review production pipelines, then normalize implementation details. - [ ] Compare SDK install paths and artifact resolution across Rust, JS, React Native, Kotlin, and Swift. - [ ] Ensure SDKs exercise the same control flows for runtime setup, extension selection, artifact validation, and tool access. -- [ ] Add Android split/local runtime validation so selected extensions must exist in the copied runtime tree before manifests are published. +- [x] Add Android split/local runtime validation so selected extensions must exist in the copied runtime tree before manifests are published. - [ ] Align or explicitly document Deno native runtime/tools/extension resolution versus Node and Bun. - [ ] Port stronger exact-extension artifact validation into the Android Gradle resolver. - [ ] Pass mobile `sharedPreloadLibraries` through to startup arguments consistently. @@ -103,11 +103,15 @@ review production pipelines, then normalize implementation details. the checker. - Subagent CI/release audit found these remaining next fixes: collapse remaining literal workflow/policy checks back to generated package contracts. -- Subagent SDK audit found these next fixes: validate Android copied extension - files before publishing manifests, align or explicitly document Deno native - runtime/tools/extension resolution, port stronger exact-extension validation - into the Android Gradle resolver, pass mobile shared preload libraries into - startup args, and add an explicit WASIX tools preflight. +- Android split/local runtime packaging now validates selected extension + control and versioned SQL files in the copied runtime tree before generated + manifests can declare those extensions. The public Android Gradle resolver + applies the same check after Maven exact-extension runtime artifacts are + merged. +- Subagent SDK audit found these next fixes: align or explicitly document Deno + native runtime/tools/extension resolution, port stronger exact-extension + validation into the Android Gradle resolver, pass mobile shared preload + libraries into startup args, and add an explicit WASIX tools preflight. - Local workflow tooling is available: `act` is installed at v0.2.89, which matches the latest upstream release published on 2026-06-01, Docker is available, `act -l` parses the CI, Release, and mobile E2E workflow graph, diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index b93c6ad4..8f7180c1 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -60,7 +60,7 @@ the release/tooling surface after the runtime tool crate split. - [ ] Remove subtle duplicate logic where one SDK has a stronger resolver or validator than another. - [ ] Ensure examples exercise the same control flows the SDKs document. -- [ ] Validate Android split/local runtime extension files before generated manifests +- [x] Validate Android split/local runtime extension files before generated manifests declare the selected extensions. - [ ] Align Deno native runtime/tools/extension resolution with Node/Bun, or document and test Deno as intentionally unsupported for registry-managed extensions. @@ -169,8 +169,12 @@ the release/tooling surface after the runtime tool crate split. uncommitted edits. - A read-only CI/release audit found this remaining issue: some policy checks compare copied literals instead of generated package contracts. -- A read-only SDK parity audit found these next issues: Android copied runtime - manifests can declare missing extensions, Deno native resolution does not - follow Node/Bun tools and extension materialization, Android Maven extension - validation is weaker than Rust/JS, mobile shared preload libraries are parsed - but not passed to startup, and WASIX split tools are only validated lazily. +- Android split/local runtime packaging now rejects selected extensions missing + control or versioned SQL files in the copied runtime tree before manifests + declare them. The public Android Gradle resolver performs the same check + after Maven exact-extension runtime artifacts are merged. +- A read-only SDK parity audit found these next issues: Deno native resolution + does not follow Node/Bun tools and extension materialization, Android Maven + extension validation is weaker than Rust/JS, mobile shared preload libraries + are parsed but not passed to startup, and WASIX split tools are only validated + lazily. diff --git a/src/sdks/kotlin/oliphaunt-android-gradle-plugin/src/main/java/dev/oliphaunt/android/ResolveOliphauntAndroidAssetsTask.java b/src/sdks/kotlin/oliphaunt-android-gradle-plugin/src/main/java/dev/oliphaunt/android/ResolveOliphauntAndroidAssetsTask.java index 15b2ebd9..295ba639 100644 --- a/src/sdks/kotlin/oliphaunt-android-gradle-plugin/src/main/java/dev/oliphaunt/android/ResolveOliphauntAndroidAssetsTask.java +++ b/src/sdks/kotlin/oliphaunt-android-gradle-plugin/src/main/java/dev/oliphaunt/android/ResolveOliphauntAndroidAssetsTask.java @@ -500,6 +500,7 @@ private void mergeExtensionRuntimeArtifacts(Map downloaded, List nativeArtifacts = artifacts.stream().filter(artifact -> artifact.nativeModuleStem != null).toList(); String staticRegistrySource = ""; @@ -533,6 +534,18 @@ private File extractExtensionRuntimeArtifact(String sqlName, File archive) { return artifactRoot; } + private static void validateSelectedExtensionRuntimeFiles(File runtimeFiles, List artifacts) { + File extensionDir = new File(runtimeFiles, "share/postgresql/extension"); + for (ExtensionRuntimeArtifact artifact : artifacts) { + File control = new File(extensionDir, artifact.sqlName + ".control"); + if (!control.isFile()) { + throw new GradleException( + "selected extension " + artifact.sqlName + " is missing packaged control file " + control); + } + extensionSqlFiles(runtimeFiles, artifact.sqlName); + } + } + private File extractExtensionArchive(File archive) { if (!archive.getName().endsWith(".tar.gz") && !archive.getName().endsWith(".tgz")) { throw new GradleException( @@ -787,14 +800,7 @@ private void copyMobileStaticTree(File source, File target) { } private static List collectExtensionSqlSymbols(File runtimeFiles, String sqlName) { - File extensionDir = new File(runtimeFiles, "share/postgresql/extension"); - File[] sqlFiles = - extensionDir.listFiles( - file -> file.isFile() && file.getName().startsWith(sqlName + "--") && file.getName().endsWith(".sql")); - if (sqlFiles == null || sqlFiles.length == 0) { - throw new GradleException("selected extension " + sqlName + " has no packaged SQL files in " + extensionDir); - } - Arrays.sort(sqlFiles, java.util.Comparator.comparing(File::getName)); + List sqlFiles = extensionSqlFiles(runtimeFiles, sqlName); TreeSet symbols = new TreeSet<>(); for (File file : sqlFiles) { try { @@ -806,6 +812,18 @@ private static List collectExtensionSqlSymbols(File runtimeFiles, String return new ArrayList<>(symbols); } + private static List extensionSqlFiles(File runtimeFiles, String sqlName) { + File extensionDir = new File(runtimeFiles, "share/postgresql/extension"); + File[] sqlFiles = + extensionDir.listFiles( + file -> file.isFile() && file.getName().startsWith(sqlName + "--") && file.getName().endsWith(".sql")); + if (sqlFiles == null || sqlFiles.length == 0) { + throw new GradleException("selected extension " + sqlName + " has no packaged SQL files in " + extensionDir); + } + Arrays.sort(sqlFiles, java.util.Comparator.comparing(File::getName)); + return Arrays.asList(sqlFiles); + } + private static List modulePathnameCSymbols(String sql) { TreeSet symbols = new TreeSet<>(); for (String statement : splitSqlStatements(stripSqlLineComments(sql))) { diff --git a/src/sdks/kotlin/oliphaunt/build.gradle.kts b/src/sdks/kotlin/oliphaunt/build.gradle.kts index 5a39e6be..b9f474cc 100644 --- a/src/sdks/kotlin/oliphaunt/build.gradle.kts +++ b/src/sdks/kotlin/oliphaunt/build.gradle.kts @@ -342,6 +342,7 @@ abstract class PrepareOliphauntAndroidAssetsTask : DefaultTask() { output.resolve("oliphaunt").toPath(), excludedPrefixes = setOf("static-registry/archives"), ) + validateSelectedExtensionFiles(output.resolve("oliphaunt/runtime/files"), selectedExtensions.get()) return } @@ -416,6 +417,7 @@ abstract class PrepareOliphauntAndroidAssetsTask : DefaultTask() { val filesDir = packageDir.resolve("files") copyTree(source.toPath(), filesDir.toPath()) val extensions = resolveExtensionSelection(requestedExtensions) + validateSelectedExtensionFiles(filesDir, extensions) val nativeModuleStems = nativeModuleStems(extensions) val registeredModuleStems = mobileStaticModuleStems.toSortedSet() val unknownRegisteredStems = registeredModuleStems - nativeModuleStems.toSet() @@ -452,6 +454,29 @@ abstract class PrepareOliphauntAndroidAssetsTask : DefaultTask() { ) } + private fun validateSelectedExtensionFiles( + filesDir: File, + extensions: List, + ) { + if (extensions.isEmpty()) return + val extensionDir = filesDir.resolve("share/postgresql/extension") + for (extension in extensions) { + val control = extensionDir.resolve("$extension.control") + require(control.isFile) { + "Oliphaunt Kotlin Android selected extension '$extension' is missing control file " + + control + } + val sqlFiles = + extensionDir.listFiles { file -> + file.isFile && file.name.startsWith("$extension--") && file.name.endsWith(".sql") + } ?: emptyArray() + require(sqlFiles.isNotEmpty()) { + "Oliphaunt Kotlin Android selected extension '$extension' has no packaged SQL files in " + + extensionDir + } + } + } + private fun resolveExtensionSelection(requestedExtensions: List): List { val extensions = linkedSetOf() for (extension in requestedExtensions) { diff --git a/src/sdks/kotlin/tools/check-sdk.sh b/src/sdks/kotlin/tools/check-sdk.sh index 1e7d1ff9..cafb13af 100755 --- a/src/sdks/kotlin/tools/check-sdk.sh +++ b/src/sdks/kotlin/tools/check-sdk.sh @@ -569,10 +569,16 @@ if [ -n "${ANDROID_HOME:-}" ]; then tmp_split_runtime="$(prepare_scratch_dir kotlin-split-runtime)" tmp_split_template="$(prepare_scratch_dir kotlin-split-template)" mkdir -p \ - "$tmp_split_runtime/share/postgresql" \ + "$tmp_split_runtime/share/postgresql/extension" \ "$tmp_split_runtime/lib/postgresql" \ "$tmp_split_template/base" printf 'runtime split smoke\n' >"$tmp_split_runtime/share/postgresql/README.liboliphaunt-split-smoke" + printf "comment = 'vector split smoke control'\n" >"$tmp_split_runtime/share/postgresql/extension/vector.control" + printf "select 'vector split smoke sql';\n" >"$tmp_split_runtime/share/postgresql/extension/vector--1.0.sql" + printf "comment = 'cube split smoke control'\n" >"$tmp_split_runtime/share/postgresql/extension/cube.control" + printf "select 'cube split smoke sql';\n" >"$tmp_split_runtime/share/postgresql/extension/cube--1.0.sql" + printf "comment = 'earthdistance split smoke control'\n" >"$tmp_split_runtime/share/postgresql/extension/earthdistance.control" + printf "select 'earthdistance split smoke sql';\n" >"$tmp_split_runtime/share/postgresql/extension/earthdistance--1.0.sql" printf '18\n' >"$tmp_split_template/PG_VERSION" printf 'template split smoke\n' >"$tmp_split_template/base/README.liboliphaunt-split-smoke" run "$gradle_cmd" -p "$project_dir" :oliphaunt:prepareOliphauntAndroidAssets \ @@ -613,6 +619,33 @@ if [ -n "${ANDROID_HOME:-}" ]; then require_manifest_line "$split_template_manifest" "mobileStaticRegistrySource=" \ "Kotlin Android split template manifest should not claim generated mobile static-registry source" + tmp_split_incomplete_runtime="$(prepare_scratch_dir kotlin-split-incomplete-extension)" + mkdir -p "$tmp_split_incomplete_runtime/share/postgresql/extension" + printf 'runtime split incomplete smoke\n' >"$tmp_split_incomplete_runtime/share/postgresql/README.liboliphaunt-split-incomplete-smoke" + printf "comment = 'vector split incomplete control'\n" >"$tmp_split_incomplete_runtime/share/postgresql/extension/vector.control" + split_incomplete_extension_log="$scratch_root/kotlin-split-incomplete-extension.log" + rm -f "$split_incomplete_extension_log" + printf '\n==> %s\n' "$gradle_cmd -p $project_dir :oliphaunt:prepareOliphauntAndroidAssets -PoliphauntExtensions=vector" + if "$gradle_cmd" -p "$project_dir" :oliphaunt:prepareOliphauntAndroidAssets \ + "-PoliphauntRuntimeDir=$tmp_split_incomplete_runtime" \ + "-PoliphauntTemplatePgdataDir=$tmp_split_template" \ + "-PoliphauntExtensions=vector" \ + $gradle_scratch_args \ + $gradle_smoke_cache_args >"$split_incomplete_extension_log" 2>&1; then + echo "Kotlin Android split runtime packaging accepted a selected extension without packaged SQL files" >&2 + cat "$split_incomplete_extension_log" >&2 + rm -f "$split_incomplete_extension_log" + exit 1 + fi + if ! grep -Fq "selected extension 'vector' has no packaged SQL files" "$split_incomplete_extension_log"; then + echo "Kotlin Android split runtime packaging failed without the expected selected-extension file diagnostic" >&2 + cat "$split_incomplete_extension_log" >&2 + rm -f "$split_incomplete_extension_log" + exit 1 + fi + rm -f "$split_incomplete_extension_log" + rm -rf "$tmp_split_incomplete_runtime" + split_static_log="$scratch_root/kotlin-split-static.log" rm -f "$split_static_log" printf '\n==> %s\n' "$gradle_cmd -p $project_dir :oliphaunt:prepareOliphauntAndroidAssets -PoliphauntMobileStaticModules=vector" diff --git a/tools/policy/check-sdk-mobile-extension-surface.sh b/tools/policy/check-sdk-mobile-extension-surface.sh index 9ef60a49..b3c5b3b4 100755 --- a/tools/policy/check-sdk-mobile-extension-surface.sh +++ b/tools/policy/check-sdk-mobile-extension-surface.sh @@ -22,6 +22,8 @@ require_text src/sdks/kotlin/oliphaunt/build.gradle.kts "generatedNativeModuleSt "Kotlin Android Gradle packaging must derive native module stems from generated extension metadata" require_text src/sdks/kotlin/oliphaunt/build.gradle.kts "cannot select unknown extension" \ "Kotlin Android split runtime packaging must reject extensions absent from generated metadata" +require_text src/sdks/kotlin/oliphaunt/build.gradle.kts "validateSelectedExtensionFiles" \ + "Kotlin Android split runtime packaging must validate selected extension control and SQL files before publishing manifests" reject_text src/sdks/kotlin/oliphaunt/build.gradle.kts "?: return extension" \ "Kotlin Android Gradle packaging must not infer native module stems for unknown extensions" reject_text src/sdks/kotlin/oliphaunt/build.gradle.kts '"postgis" -> "postgis-3"' \ @@ -46,6 +48,8 @@ require_text src/sdks/kotlin/oliphaunt-android-gradle-plugin/src/main/java/dev/o "Kotlin Android public Gradle plugin must stage mobile static archives from target-scoped extension artifacts" require_text src/sdks/kotlin/oliphaunt-android-gradle-plugin/src/main/java/dev/oliphaunt/android/ResolveOliphauntAndroidAssetsTask.java "mobileStaticDependencyArchives" \ "Kotlin Android public Gradle plugin must stage selected mobile static dependency archives from target-scoped extension artifacts" +require_text src/sdks/kotlin/oliphaunt-android-gradle-plugin/src/main/java/dev/oliphaunt/android/ResolveOliphauntAndroidAssetsTask.java "validateSelectedExtensionRuntimeFiles" \ + "Kotlin Android public Gradle plugin must validate selected extension runtime files before publishing manifests" require_text src/sdks/kotlin/oliphaunt/src/androidMain/cpp/CMakeLists.txt "add_library(oliphaunt_extensions SHARED" \ "Kotlin Android CMake must link a support library from prebuilt static extension archives" require_text src/sdks/kotlin/oliphaunt/src/androidMain/cpp/CMakeLists.txt "oliphaunt_dependency_archives" \ @@ -415,7 +419,7 @@ require_text src/extensions/generated/pgxs-build.tsv "$(printf 'vector\tvector\t "native PGXS build plan must map exact vector artifact builds to the pgvector checkout" require_text src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh "pgxs_extension_source_rel" \ "macOS native PGXS builder must resolve external source checkouts from generated build-plan metadata" -require_text src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh 'BE_DLLLIBS=$be_dllibs -lm' \ +require_text src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh 'be_dllibs="$be_dllibs -lm"' \ "macOS native PGXS builder must keep libm extensions on the Darwin bundle-loader link path" require_text src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh "pgxs_extension_source_rel" \ "Linux native PGXS builder must resolve external source checkouts from generated build-plan metadata" From 763f5e261cba6ca8cfed507591fd75564d0dab27 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 03:12:20 +0000 Subject: [PATCH 029/137] fix: pass mobile shared preload libraries --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 12 +++- .../examples-ci-release-validation.md | 13 +++-- .../oliphaunt/AndroidNativeDirectEngine.kt | 2 +- .../OliphauntAndroidRuntimeAssets.kt | 20 ++++--- .../kotlin/dev/oliphaunt/Oliphaunt.kt | 7 ++- .../dev/oliphaunt/OliphauntDatabaseTest.kt | 28 ++++++++++ .../swift/Sources/Oliphaunt/Oliphaunt.swift | 7 ++- .../Oliphaunt/OliphauntNativeDirect.swift | 28 +++++++--- .../Oliphaunt/OliphauntRuntimeResources.swift | 7 +++ .../Tests/OliphauntTests/OliphauntTests.swift | 55 ++++++++++++++++++- .../check-sdk-mobile-extension-surface.sh | 4 ++ 11 files changed, 154 insertions(+), 29 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 97e6e7a5..93e4750a 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -41,7 +41,7 @@ review production pipelines, then normalize implementation details. - [x] Add Android split/local runtime validation so selected extensions must exist in the copied runtime tree before manifests are published. - [ ] Align or explicitly document Deno native runtime/tools/extension resolution versus Node and Bun. - [ ] Port stronger exact-extension artifact validation into the Android Gradle resolver. -- [ ] Pass mobile `sharedPreloadLibraries` through to startup arguments consistently. +- [x] Pass mobile `sharedPreloadLibraries` through to startup arguments consistently. - [ ] Add an explicit WASIX split-tools preflight path before first `pg_dump` or `psql` call. - [ ] Identify feature gaps where one SDK exposes a runtime/tool/extension capability differently from the others. - [ ] Add or update parity checks where a documented invariant is not machine-checked. @@ -110,8 +110,8 @@ review production pipelines, then normalize implementation details. merged. - Subagent SDK audit found these next fixes: align or explicitly document Deno native runtime/tools/extension resolution, port stronger exact-extension - validation into the Android Gradle resolver, pass mobile shared preload - libraries into startup args, and add an explicit WASIX tools preflight. + validation into the Android Gradle resolver, and add an explicit WASIX tools + preflight. - Local workflow tooling is available: `act` is installed at v0.2.89, which matches the latest upstream release published on 2026-06-01, Docker is available, `act -l` parses the CI, Release, and mobile E2E workflow graph, @@ -121,3 +121,9 @@ review production pipelines, then normalize implementation details. committed HEAD rather than uncommitted local edits. - JS Deno direct mode now resolves packaged ICU for explicit-library installs when running inside Deno, and rejects package-managed extension requests without an explicit prepared `runtimeDirectory`. Node and Bun remain the registry-managed extension materialization paths. - Rust native runtime cache validation already requires both split client tools, with `runtime_validation_requires_split_tools` covering a missing `pg_dump` cache entry. +- Mobile native-direct startup now passes packaged runtime + `sharedPreloadLibraries` through to `shared_preload_libraries=...` startup + args in Kotlin Android/React Native Android and Swift/React Native iOS. + Kotlin static/unit checks, mobile extension policy checks, and release checks + passed locally; Swift-specific test execution was not run because this Linux + host does not have a Swift toolchain. diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index 8f7180c1..2ef4a47b 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -66,7 +66,7 @@ the release/tooling surface after the runtime tool crate split. and test Deno as intentionally unsupported for registry-managed extensions. - [ ] Port Rust/JS exact-extension archive validation rules into the Android Gradle resolver. -- [ ] Thread mobile `sharedPreloadLibraries` from manifests into startup args. +- [x] Thread mobile `sharedPreloadLibraries` from manifests into startup args. - [ ] Add an explicit WASIX tools preflight before first `pg_dump` or `psql` use. ## P2: Dead Code and Tooling Cleanup @@ -173,8 +173,13 @@ the release/tooling surface after the runtime tool crate split. control or versioned SQL files in the copied runtime tree before manifests declare them. The public Android Gradle resolver performs the same check after Maven exact-extension runtime artifacts are merged. +- Mobile native-direct startup now passes packaged runtime + `sharedPreloadLibraries` through to `shared_preload_libraries=...` startup + args in Kotlin Android/React Native Android and Swift/React Native iOS. + Kotlin static/unit checks, mobile extension policy checks, and release checks + passed locally; Swift-specific test execution was not run because this Linux + host does not have a Swift toolchain. - A read-only SDK parity audit found these next issues: Deno native resolution does not follow Node/Bun tools and extension materialization, Android Maven - extension validation is weaker than Rust/JS, mobile shared preload libraries - are parsed but not passed to startup, and WASIX split tools are only validated - lazily. + extension validation is weaker than Rust/JS, and WASIX split tools are only + validated lazily. diff --git a/src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/AndroidNativeDirectEngine.kt b/src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/AndroidNativeDirectEngine.kt index 37d154d3..9c401709 100644 --- a/src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/AndroidNativeDirectEngine.kt +++ b/src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/AndroidNativeDirectEngine.kt @@ -80,7 +80,7 @@ public class AndroidNativeDirectEngine( runtime.runtimeDirectory, effectiveUsername, effectiveDatabase, - config.postgresStartupArgs().toTypedArray(), + config.postgresStartupArgs(runtime.sharedPreloadLibraries).toTypedArray(), ) } return AndroidNativeDirectSession( diff --git a/src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssets.kt b/src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssets.kt index df2d1aa0..4e8cfe9c 100644 --- a/src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssets.kt +++ b/src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssets.kt @@ -42,6 +42,7 @@ public data class OliphauntExtensionSizeReport( internal data class OliphauntAndroidResolvedRuntime( val runtimeDirectory: String, val templatePgdata: OliphauntAndroidAssetPackage?, + val sharedPreloadLibraries: Set = emptySet(), ) internal object OliphauntAndroidRuntimeAssets { @@ -79,12 +80,15 @@ internal object OliphauntAndroidRuntimeAssets { ): OliphauntAndroidResolvedRuntime { val requestedExtensionSet = validateExtensionIds(requestedExtensions) val templatePgdata = packageManifestOrNull(context.assets, TEMPLATE_PGDATA_ASSET_ROOT) + val packagedRuntime = packageManifestOrNull(context.assets, RUNTIME_ASSET_ROOT) + val usePackagedRuntime = explicitRuntimeDirectory?.takeIf(String::isNotEmpty) == null val runtimeDirectory = explicitRuntimeDirectory?.takeIf(String::isNotEmpty) - ?: materializePackagedRuntime(context, requestedExtensionSet) + ?: materializePackagedRuntime(context, requestedExtensionSet, packagedRuntime) return OliphauntAndroidResolvedRuntime( runtimeDirectory = runtimeDirectory, templatePgdata = templatePgdata, + sharedPreloadLibraries = if (usePackagedRuntime) packagedRuntime?.sharedPreloadLibraries.orEmpty() else emptySet(), ) } @@ -171,14 +175,14 @@ internal object OliphauntAndroidRuntimeAssets { private fun materializePackagedRuntime( context: Context, requestedExtensions: Set, + runtimePackage: OliphauntAndroidAssetPackage? = packageManifestOrNull(context.assets, RUNTIME_ASSET_ROOT), ): String { - val runtimePackage = - packageManifestOrNull(context.assets, RUNTIME_ASSET_ROOT) - ?: throw OliphauntException( - "Kotlin Android Oliphaunt runtime resources are not present. " + - "Pass runtimeDirectory for local development or configure Gradle with " + - "-PoliphauntRuntimeDir=.", - ) + val runtimePackage = runtimePackage + ?: throw OliphauntException( + "Kotlin Android Oliphaunt runtime resources are not present. " + + "Pass runtimeDirectory for local development or configure Gradle with " + + "-PoliphauntRuntimeDir=.", + ) requirePackagedExtensions(runtimePackage, requestedExtensions) val runtimeRoot = File( diff --git a/src/sdks/kotlin/oliphaunt/src/commonMain/kotlin/dev/oliphaunt/Oliphaunt.kt b/src/sdks/kotlin/oliphaunt/src/commonMain/kotlin/dev/oliphaunt/Oliphaunt.kt index c373f278..3923003b 100644 --- a/src/sdks/kotlin/oliphaunt/src/commonMain/kotlin/dev/oliphaunt/Oliphaunt.kt +++ b/src/sdks/kotlin/oliphaunt/src/commonMain/kotlin/dev/oliphaunt/Oliphaunt.kt @@ -180,9 +180,12 @@ internal fun validateStartupGucs(gucs: List) { } } -internal fun OliphauntConfig.postgresStartupArgs(): List = runtimeFootprint.postgresStartupArgs() + +internal fun OliphauntConfig.postgresStartupArgs(sharedPreloadLibraries: Collection = emptyList()): List = runtimeFootprint.postgresStartupArgs() + durability.postgresStartupArgs() + - startupGucs.flatMap { guc -> listOf("-c", "${guc.name.trim()}=${guc.value}") } + startupGucs.flatMap { guc -> listOf("-c", "${guc.name.trim()}=${guc.value}") } + + sharedPreloadLibraries.distinct().sorted().takeIf(List::isNotEmpty) + ?.let { libraries -> listOf("-c", "shared_preload_libraries=${libraries.joinToString(",")}") } + .orEmpty() private fun RuntimeFootprintProfile.postgresStartupArgs(): List = when (this) { RuntimeFootprintProfile.Throughput -> listOf( diff --git a/src/sdks/kotlin/oliphaunt/src/commonTest/kotlin/dev/oliphaunt/OliphauntDatabaseTest.kt b/src/sdks/kotlin/oliphaunt/src/commonTest/kotlin/dev/oliphaunt/OliphauntDatabaseTest.kt index fb1abec7..126be063 100644 --- a/src/sdks/kotlin/oliphaunt/src/commonTest/kotlin/dev/oliphaunt/OliphauntDatabaseTest.kt +++ b/src/sdks/kotlin/oliphaunt/src/commonTest/kotlin/dev/oliphaunt/OliphauntDatabaseTest.kt @@ -929,6 +929,34 @@ class OliphauntDatabaseTest { ).postgresStartupArgs(), ), ) + assertEquals( + listOf( + "max_connections=1", + "superuser_reserved_connections=0", + "reserved_connections=0", + "autovacuum_worker_slots=1", + "max_wal_senders=0", + "max_replication_slots=0", + "shared_buffers=32MB", + "wal_buffers=-1", + "min_wal_size=32MB", + "max_wal_size=64MB", + "io_method=sync", + "io_max_concurrency=1", + "fsync=on", + "full_page_writes=on", + "synchronous_commit=off", + "shared_buffers=16MB", + "shared_preload_libraries=auto_explain,pg_search", + ), + startupAssignments( + OliphauntConfig( + durability = DurabilityProfile.Balanced, + runtimeFootprint = RuntimeFootprintProfile.BalancedMobile, + startupGucs = listOf(PostgresStartupGuc(" shared_buffers ", "16MB")), + ).postgresStartupArgs(setOf("pg_search", "auto_explain", "pg_search")), + ), + ) assertEquals( listOf( "max_connections=1", diff --git a/src/sdks/swift/Sources/Oliphaunt/Oliphaunt.swift b/src/sdks/swift/Sources/Oliphaunt/Oliphaunt.swift index 01b5982d..8f4e9d53 100644 --- a/src/sdks/swift/Sources/Oliphaunt/Oliphaunt.swift +++ b/src/sdks/swift/Sources/Oliphaunt/Oliphaunt.swift @@ -773,13 +773,18 @@ public struct OliphauntTransaction: Sendable { extension OliphauntConfiguration { - func postgresStartupArgs() -> [String] { + func postgresStartupArgs(sharedPreloadLibraries: [String] = []) -> [String] { var args = runtimeFootprint.postgresStartupArgs() args.append(contentsOf: durability.postgresStartupArgs()) for guc in startupGUCs { args.append("-c") args.append("\(guc.name.trimmingCharacters(in: .whitespacesAndNewlines))=\(guc.value)") } + let preloadLibraries = Set(sharedPreloadLibraries).sorted() + if !preloadLibraries.isEmpty { + args.append("-c") + args.append("shared_preload_libraries=\(preloadLibraries.joined(separator: ","))") + } return args } } diff --git a/src/sdks/swift/Sources/Oliphaunt/OliphauntNativeDirect.swift b/src/sdks/swift/Sources/Oliphaunt/OliphauntNativeDirect.swift index 19c15e79..5f8afce6 100644 --- a/src/sdks/swift/Sources/Oliphaunt/OliphauntNativeDirect.swift +++ b/src/sdks/swift/Sources/Oliphaunt/OliphauntNativeDirect.swift @@ -43,7 +43,7 @@ public struct OliphauntNativeDirectEngine: OliphauntEngine, OliphauntEngineSuppo let packagedRuntimeResources = try runtimeResources ?? OliphauntRuntimeResources.bundled( containing: configuration.extensions ) - let resolvedRuntimeDirectory = try resolveRuntimeDirectory( + let resolvedRuntime = try resolveRuntime( extensions: configuration.extensions, runtimeResources: packagedRuntimeResources ) @@ -68,9 +68,11 @@ public struct OliphauntNativeDirectEngine: OliphauntEngine, OliphauntEngineSuppo let username = configuration.username ?? self.username let database = configuration.database ?? self.database - let startupArgs = configuration.postgresStartupArgs() + let startupArgs = configuration.postgresStartupArgs( + sharedPreloadLibraries: resolvedRuntime.sharedPreloadLibraries + ) let libraryPath = libraryURL?.path - let runtimePath = resolvedRuntimeDirectory?.path ?? "" + let runtimePath = resolvedRuntime.directory?.path ?? "" var session: OpaquePointer? let rc = withCStringArray(startupArgs) { startupArgPointers in pgdata.path.withCString { pgdataCString in @@ -140,25 +142,33 @@ public struct OliphauntNativeDirectEngine: OliphauntEngine, OliphauntEngineSuppo return request.root } - private func resolveRuntimeDirectory( + private func resolveRuntime( extensions: [String], runtimeResources: OliphauntRuntimeResources? - ) throws -> URL? { + ) throws -> ResolvedNativeRuntime { if let runtimeDirectory { - return runtimeDirectory + return ResolvedNativeRuntime(directory: runtimeDirectory) } if let runtimeResources { - return try runtimeResources.materializeRuntime(requestedExtensions: extensions) + return ResolvedNativeRuntime( + directory: try runtimeResources.materializeRuntime(requestedExtensions: extensions), + sharedPreloadLibraries: try runtimeResources.sharedPreloadLibraries(requestedExtensions: extensions) + ) } if let environmentRuntimeDirectory = Self.environmentRuntimeDirectory() { - return environmentRuntimeDirectory + return ResolvedNativeRuntime(directory: environmentRuntimeDirectory) } if !extensions.isEmpty { throw OliphauntError.engine( "Swift native-direct extensions require runtimeDirectory or packaged OliphauntRuntimeResources built with the selected extensions" ) } - return nil + return ResolvedNativeRuntime() + } + + private struct ResolvedNativeRuntime { + var directory: URL? = nil + var sharedPreloadLibraries: [String] = [] } private static func environmentRuntimeDirectory() -> URL? { diff --git a/src/sdks/swift/Sources/Oliphaunt/OliphauntRuntimeResources.swift b/src/sdks/swift/Sources/Oliphaunt/OliphauntRuntimeResources.swift index f7b2e33d..4cdd1351 100644 --- a/src/sdks/swift/Sources/Oliphaunt/OliphauntRuntimeResources.swift +++ b/src/sdks/swift/Sources/Oliphaunt/OliphauntRuntimeResources.swift @@ -510,6 +510,13 @@ public struct OliphauntRuntimeResources: Sendable { return target } + func sharedPreloadLibraries(requestedExtensions: [String] = []) throws -> [String] { + let requested = try Self.validateExtensionIds(requestedExtensions) + let runtime = try assetPackage(kind: .runtime) + try require(runtime: runtime, contains: requested) + return runtime.sharedPreloadLibraries.sorted() + } + func hasPackagedResources(containing requestedExtensions: Set = []) throws -> Bool { guard FileManager.default.fileExists( atPath: resourceRoot.appendingPathComponent("runtime/manifest.properties").path diff --git a/src/sdks/swift/Tests/OliphauntTests/OliphauntTests.swift b/src/sdks/swift/Tests/OliphauntTests/OliphauntTests.swift index 76cd69fa..963b29b7 100644 --- a/src/sdks/swift/Tests/OliphauntTests/OliphauntTests.swift +++ b/src/sdks/swift/Tests/OliphauntTests/OliphauntTests.swift @@ -609,6 +609,33 @@ func runtimeFootprintProfilesBuildTheMobileStartupGUCContract() { "shared_buffers=16MB", ] ) + #expect( + startupAssignments( + OliphauntConfiguration( + durability: .balanced, + runtimeFootprint: .balancedMobile, + startupGUCs: [OliphauntStartupGUC(" shared_buffers ", "16MB")] + ).postgresStartupArgs(sharedPreloadLibraries: ["pg_search", "auto_explain", "pg_search"]) + ) == [ + "max_connections=1", + "superuser_reserved_connections=0", + "reserved_connections=0", + "autovacuum_worker_slots=1", + "max_wal_senders=0", + "max_replication_slots=0", + "shared_buffers=32MB", + "wal_buffers=-1", + "min_wal_size=32MB", + "max_wal_size=64MB", + "io_method=sync", + "io_max_concurrency=1", + "fsync=on", + "full_page_writes=on", + "synchronous_commit=off", + "shared_buffers=16MB", + "shared_preload_libraries=auto_explain,pg_search", + ] + ) #expect( startupAssignments( OliphauntConfiguration(runtimeFootprint: .smallMobile).postgresStartupArgs() @@ -1110,6 +1137,7 @@ func runtimeResourcesMaterializeRuntimeAndPrepareTemplatePgdata() throws { #expect(!FileManager.default.fileExists( atPath: runtime.appendingPathComponent("share/postgresql/extension/hstore.control").path )) + #expect(try resources.sharedPreloadLibraries(requestedExtensions: ["vector"]).isEmpty) let pgdata = fixture.root.appendingPathComponent("app-root/pgdata", isDirectory: true) #expect(try resources.preparePgdata(at: pgdata)) @@ -1120,6 +1148,23 @@ func runtimeResourcesMaterializeRuntimeAndPrepareTemplatePgdata() throws { #expect(try posixPermissions(pgdata.appendingPathComponent("PG_VERSION")) == 0o600) } +@Test +func runtimeResourcesExposeManifestSharedPreloadLibraries() throws { + let fixture = try makeRuntimeResourceFixture(sharedPreloadLibraries: "pg_search,auto_explain") + defer { + try? FileManager.default.removeItem(at: fixture.root) + } + let resources = OliphauntRuntimeResources( + resourceRoot: fixture.resourceRoot, + cacheRoot: fixture.cacheRoot + ) + + #expect(try resources.sharedPreloadLibraries(requestedExtensions: ["vector"]) == [ + "auto_explain", + "pg_search", + ]) +} + @Test func runtimeResourcesDiscoverBundledResourceDirectoryCandidates() throws { let fixture = try makeRuntimeResourceFixture() @@ -2193,6 +2238,14 @@ private func makeRuntimeResourceFixture() throws -> ( root: URL, resourceRoot: URL, cacheRoot: URL +) { + return try makeRuntimeResourceFixture(sharedPreloadLibraries: "") +} + +private func makeRuntimeResourceFixture(sharedPreloadLibraries: String) throws -> ( + root: URL, + resourceRoot: URL, + cacheRoot: URL ) { let root = uniqueTempURL("liboliphaunt-swift-resources") let resourceRoot = root.appendingPathComponent("resources/oliphaunt", isDirectory: true) @@ -2205,7 +2258,7 @@ private func makeRuntimeResourceFixture() throws -> ( layout=postgres-runtime-files-v1 cacheKey=test-runtime-v1 extensions=vector - sharedPreloadLibraries= + sharedPreloadLibraries=\(sharedPreloadLibraries) mobileStaticRegistryState=complete mobileStaticRegistryRegistered=vector mobileStaticRegistryPending= diff --git a/tools/policy/check-sdk-mobile-extension-surface.sh b/tools/policy/check-sdk-mobile-extension-surface.sh index b3c5b3b4..8a5897b7 100755 --- a/tools/policy/check-sdk-mobile-extension-surface.sh +++ b/tools/policy/check-sdk-mobile-extension-surface.sh @@ -12,6 +12,8 @@ require_text src/sdks/kotlin/oliphaunt/build.gradle.kts "mobileStaticRegistryPen "Kotlin Android Gradle packaging must emit mobile static-registry metadata" require_text src/sdks/kotlin/oliphaunt/build.gradle.kts "sharedPreloadLibraries=" \ "Kotlin Android Gradle packaging must emit shared-preload metadata" +require_text src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/AndroidNativeDirectEngine.kt "config.postgresStartupArgs(runtime.sharedPreloadLibraries)" \ + "Kotlin Android native-direct startup must pass packaged shared-preload libraries to liboliphaunt" require_text src/sdks/kotlin/oliphaunt/build.gradle.kts "nativeModuleStems=" \ "Kotlin Android Gradle packaging must emit expected native module stems" require_text src/sdks/kotlin/oliphaunt/build.gradle.kts "generatedExtensionMetadata.from(layout.projectDirectory.file(\"src/generated/extensions.json\"))" \ @@ -146,6 +148,8 @@ require_text tools/release/check_staged_artifacts.py "liboliphaunt_extension_[A- "staged mobile artifact checks must reject unselected iOS extension framework link inputs" require_text src/sdks/swift/Sources/Oliphaunt/OliphauntRuntimeResources.swift "available extensions" \ "Swift resource parser must validate exact extension availability" +require_text src/sdks/swift/Sources/Oliphaunt/OliphauntNativeDirect.swift "sharedPreloadLibraries: resolvedRuntime.sharedPreloadLibraries" \ + "Swift native-direct startup must pass packaged shared-preload libraries to liboliphaunt" require_text src/sdks/swift/Sources/COliphaunt/bridge.c "liboliphaunt_selected_static_extensions" \ "Swift native bridge must register generated static extension rows before open" require_text src/sdks/rust/src/runtime_resources.rs "oliphaunt-static-registry-v1" \ From 44c42f2be6bf6098601b555f1b316176d93a5c72 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 03:37:14 +0000 Subject: [PATCH 030/137] fix: preflight split wasix tools --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 6 +- .../examples-ci-release-validation.md | 2 +- examples/electron-wasix/src-wasix/Cargo.lock | 88 +------------------ examples/electron-wasix/src-wasix/src/main.rs | 3 + examples/tauri-wasix/src-tauri/Cargo.lock | 88 +------------------ examples/tauri-wasix/src-tauri/src/lib.rs | 3 + examples/tools/check-examples.sh | 4 + examples/tools/with-local-registries.sh | 6 ++ .../crates/oliphaunt-wasix/src/lib.rs | 2 +- .../oliphaunt-wasix/src/oliphaunt/mod.rs | 2 +- .../oliphaunt-wasix/src/oliphaunt/pg_dump.rs | 61 +++++++++++-- .../oliphaunt-wasix/src/oliphaunt/server.rs | 13 ++- .../tauri-sqlx-vanilla/src-tauri/Cargo.lock | 4 +- .../tauri-sqlx-vanilla/src-tauri/src/bench.rs | 3 + tools/release/check_consumer_shape.py | 23 +++++ tools/release/check_release_metadata.py | 11 +++ 16 files changed, 134 insertions(+), 185 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 93e4750a..9679ede5 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -42,7 +42,7 @@ review production pipelines, then normalize implementation details. - [ ] Align or explicitly document Deno native runtime/tools/extension resolution versus Node and Bun. - [ ] Port stronger exact-extension artifact validation into the Android Gradle resolver. - [x] Pass mobile `sharedPreloadLibraries` through to startup arguments consistently. -- [ ] Add an explicit WASIX split-tools preflight path before first `pg_dump` or `psql` call. +- [x] Add an explicit WASIX split-tools preflight path before first `pg_dump` or `psql` call. - [ ] Identify feature gaps where one SDK exposes a runtime/tool/extension capability differently from the others. - [ ] Add or update parity checks where a documented invariant is not machine-checked. - [x] Decide and document whether JS Deno native flows should support packaged native tools and extensions, or fail clearly when those features are requested. @@ -121,6 +121,10 @@ review production pipelines, then normalize implementation details. committed HEAD rather than uncommitted local edits. - JS Deno direct mode now resolves packaged ICU for explicit-library installs when running inside Deno, and rejects package-managed extension requests without an explicit prepared `runtimeDirectory`. Node and Bun remain the registry-managed extension materialization paths. - Rust native runtime cache validation already requires both split client tools, with `runtime_validation_requires_split_tools` covering a missing `pg_dump` cache entry. +- WASIX Rust now exposes `preflight_wasix_tools` plus + `OliphauntServer::preflight_tools()`, and each WASIX example calls the server + preflight before its `pg_dump`/`psql` smoke. Release checks require the + preflight API to load both split WASM payloads and their target AOT artifacts. - Mobile native-direct startup now passes packaged runtime `sharedPreloadLibraries` through to `shared_preload_libraries=...` startup args in Kotlin Android/React Native Android and Swift/React Native iOS. diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index 2ef4a47b..9ab33b02 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -67,7 +67,7 @@ the release/tooling surface after the runtime tool crate split. - [ ] Port Rust/JS exact-extension archive validation rules into the Android Gradle resolver. - [x] Thread mobile `sharedPreloadLibraries` from manifests into startup args. -- [ ] Add an explicit WASIX tools preflight before first `pg_dump` or `psql` use. +- [x] Add an explicit WASIX tools preflight before first `pg_dump` or `psql` use. ## P2: Dead Code and Tooling Cleanup diff --git a/examples/electron-wasix/src-wasix/Cargo.lock b/examples/electron-wasix/src-wasix/Cargo.lock index fbd49591..3eb38927 100644 --- a/examples/electron-wasix/src-wasix/Cargo.lock +++ b/examples/electron-wasix/src-wasix/Cargo.lock @@ -1589,23 +1589,11 @@ dependencies = [ name = "liboliphaunt-wasix-portable" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "74e4a84c8db15e4be7945d7b3a2ab1cb30a687b155367f32a25155891f604e77" +checksum = "67857a0fbca85a256e60c4ea9901958cad8fb28b7d1ee4033dbdbc0385ab9baa" dependencies = [ "oliphaunt-extension-hstore-wasix", - "oliphaunt-extension-hstore-wasix-aot-aarch64-apple-darwin", - "oliphaunt-extension-hstore-wasix-aot-aarch64-unknown-linux-gnu", - "oliphaunt-extension-hstore-wasix-aot-x86_64-pc-windows-msvc", - "oliphaunt-extension-hstore-wasix-aot-x86_64-unknown-linux-gnu", "oliphaunt-extension-pg-trgm-wasix", - "oliphaunt-extension-pg-trgm-wasix-aot-aarch64-apple-darwin", - "oliphaunt-extension-pg-trgm-wasix-aot-aarch64-unknown-linux-gnu", - "oliphaunt-extension-pg-trgm-wasix-aot-x86_64-pc-windows-msvc", - "oliphaunt-extension-pg-trgm-wasix-aot-x86_64-unknown-linux-gnu", "oliphaunt-extension-unaccent-wasix", - "oliphaunt-extension-unaccent-wasix-aot-aarch64-apple-darwin", - "oliphaunt-extension-unaccent-wasix-aot-aarch64-unknown-linux-gnu", - "oliphaunt-extension-unaccent-wasix-aot-x86_64-pc-windows-msvc", - "oliphaunt-extension-unaccent-wasix-aot-x86_64-unknown-linux-gnu", "serde", "serde_json", "sha2 0.10.9", @@ -1933,95 +1921,23 @@ version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "1d0b20fd2a03b45880974241e3443d9e324de637fefa4f43859efce70089812b" -[[package]] -name = "oliphaunt-extension-hstore-wasix-aot-aarch64-apple-darwin" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "004e128d02237a749af8e0219532f4af55b65de588709b0cf2bbef99e7fa6292" - -[[package]] -name = "oliphaunt-extension-hstore-wasix-aot-aarch64-unknown-linux-gnu" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "ae54c87147a7b4adba32fc6519a68937a8fb5155c4da28dcf36bd66b3e7e98ad" - -[[package]] -name = "oliphaunt-extension-hstore-wasix-aot-x86_64-pc-windows-msvc" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "98af804e5514ba341aa03e630320e135f7761b60104d4592743d68b324923fa9" - -[[package]] -name = "oliphaunt-extension-hstore-wasix-aot-x86_64-unknown-linux-gnu" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "b71adb2ca0f694aac91994c099572ae14906d333279e7bf91662431f86b8a06f" - [[package]] name = "oliphaunt-extension-pg-trgm-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "6ea075c13c8283d2eb26526c63061b116ffc515899fa59478a8a6c570539a312" -[[package]] -name = "oliphaunt-extension-pg-trgm-wasix-aot-aarch64-apple-darwin" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "0c5c91b06e0a5101433533753876dac7aee89936212967606175c9f141976a14" - -[[package]] -name = "oliphaunt-extension-pg-trgm-wasix-aot-aarch64-unknown-linux-gnu" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "c14ce6cbf988af1eb13f567b9a975f5bf566076688514133c093971f5a737aa6" - -[[package]] -name = "oliphaunt-extension-pg-trgm-wasix-aot-x86_64-pc-windows-msvc" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "d4e164a68f4047ac3c268ef71b9807d33242e06f61bf862bf60df9cb9a47b4ae" - -[[package]] -name = "oliphaunt-extension-pg-trgm-wasix-aot-x86_64-unknown-linux-gnu" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "96f7d7cd8ba652876f221b37e4f290a84d054e2c50625c243803224ce3e12b03" - [[package]] name = "oliphaunt-extension-unaccent-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "9ab06b4d61878a87b53afc7b047d09f5f2fd794528acb5e40d359e599b0fc956" -[[package]] -name = "oliphaunt-extension-unaccent-wasix-aot-aarch64-apple-darwin" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "37e5978c9d6e020c01336f58c8922ebaed2f4dfd6ae4568b5f91b5d416fc7cdb" - -[[package]] -name = "oliphaunt-extension-unaccent-wasix-aot-aarch64-unknown-linux-gnu" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "4ae9dd2c37edc58bf3dc34b88314e5f012221f74c96e9c538133ed162a12509e" - -[[package]] -name = "oliphaunt-extension-unaccent-wasix-aot-x86_64-pc-windows-msvc" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "f869c3c96abb7169927c921e92e44401f148e6de6138213ead88d1208462685d" - -[[package]] -name = "oliphaunt-extension-unaccent-wasix-aot-x86_64-unknown-linux-gnu" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "5c4389eaa071ac1e9bc837958ec1f5caf7f9d44a75a789b576a4938f3f0ec7cc" - [[package]] name = "oliphaunt-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "987e82c9952421633cc7d31e3ec3615856ff3833e503cac02f5b88930e7d23fc" +checksum = "3c8629c9eecf4f01df1985c1690799f0bb40c5b843b41492340d2d6d5b560b01" dependencies = [ "anyhow", "async-trait", diff --git a/examples/electron-wasix/src-wasix/src/main.rs b/examples/electron-wasix/src-wasix/src/main.rs index 61138298..92b053c9 100644 --- a/examples/electron-wasix/src-wasix/src/main.rs +++ b/examples/electron-wasix/src-wasix/src/main.rs @@ -38,6 +38,9 @@ fn start_server(root: PathBuf) -> Result { } fn validate_wasix_tools(server: &OliphauntServer) -> Result<()> { + server + .preflight_tools() + .context("preflight split WASIX pg_dump and psql tools")?; let dump = server.dump_sql(PgDumpOptions::new().arg("--schema-only"))?; anyhow::ensure!( dump.contains("PostgreSQL database dump"), diff --git a/examples/tauri-wasix/src-tauri/Cargo.lock b/examples/tauri-wasix/src-tauri/Cargo.lock index 3cdd28fd..45152e28 100644 --- a/examples/tauri-wasix/src-tauri/Cargo.lock +++ b/examples/tauri-wasix/src-tauri/Cargo.lock @@ -2782,23 +2782,11 @@ dependencies = [ name = "liboliphaunt-wasix-portable" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "74e4a84c8db15e4be7945d7b3a2ab1cb30a687b155367f32a25155891f604e77" +checksum = "67857a0fbca85a256e60c4ea9901958cad8fb28b7d1ee4033dbdbc0385ab9baa" dependencies = [ "oliphaunt-extension-hstore-wasix", - "oliphaunt-extension-hstore-wasix-aot-aarch64-apple-darwin", - "oliphaunt-extension-hstore-wasix-aot-aarch64-unknown-linux-gnu", - "oliphaunt-extension-hstore-wasix-aot-x86_64-pc-windows-msvc", - "oliphaunt-extension-hstore-wasix-aot-x86_64-unknown-linux-gnu", "oliphaunt-extension-pg-trgm-wasix", - "oliphaunt-extension-pg-trgm-wasix-aot-aarch64-apple-darwin", - "oliphaunt-extension-pg-trgm-wasix-aot-aarch64-unknown-linux-gnu", - "oliphaunt-extension-pg-trgm-wasix-aot-x86_64-pc-windows-msvc", - "oliphaunt-extension-pg-trgm-wasix-aot-x86_64-unknown-linux-gnu", "oliphaunt-extension-unaccent-wasix", - "oliphaunt-extension-unaccent-wasix-aot-aarch64-apple-darwin", - "oliphaunt-extension-unaccent-wasix-aot-aarch64-unknown-linux-gnu", - "oliphaunt-extension-unaccent-wasix-aot-x86_64-pc-windows-msvc", - "oliphaunt-extension-unaccent-wasix-aot-x86_64-unknown-linux-gnu", "serde", "serde_json", "sha2 0.10.9", @@ -3406,95 +3394,23 @@ version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "1d0b20fd2a03b45880974241e3443d9e324de637fefa4f43859efce70089812b" -[[package]] -name = "oliphaunt-extension-hstore-wasix-aot-aarch64-apple-darwin" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "004e128d02237a749af8e0219532f4af55b65de588709b0cf2bbef99e7fa6292" - -[[package]] -name = "oliphaunt-extension-hstore-wasix-aot-aarch64-unknown-linux-gnu" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "ae54c87147a7b4adba32fc6519a68937a8fb5155c4da28dcf36bd66b3e7e98ad" - -[[package]] -name = "oliphaunt-extension-hstore-wasix-aot-x86_64-pc-windows-msvc" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "98af804e5514ba341aa03e630320e135f7761b60104d4592743d68b324923fa9" - -[[package]] -name = "oliphaunt-extension-hstore-wasix-aot-x86_64-unknown-linux-gnu" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "b71adb2ca0f694aac91994c099572ae14906d333279e7bf91662431f86b8a06f" - [[package]] name = "oliphaunt-extension-pg-trgm-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "6ea075c13c8283d2eb26526c63061b116ffc515899fa59478a8a6c570539a312" -[[package]] -name = "oliphaunt-extension-pg-trgm-wasix-aot-aarch64-apple-darwin" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "0c5c91b06e0a5101433533753876dac7aee89936212967606175c9f141976a14" - -[[package]] -name = "oliphaunt-extension-pg-trgm-wasix-aot-aarch64-unknown-linux-gnu" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "c14ce6cbf988af1eb13f567b9a975f5bf566076688514133c093971f5a737aa6" - -[[package]] -name = "oliphaunt-extension-pg-trgm-wasix-aot-x86_64-pc-windows-msvc" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "d4e164a68f4047ac3c268ef71b9807d33242e06f61bf862bf60df9cb9a47b4ae" - -[[package]] -name = "oliphaunt-extension-pg-trgm-wasix-aot-x86_64-unknown-linux-gnu" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "96f7d7cd8ba652876f221b37e4f290a84d054e2c50625c243803224ce3e12b03" - [[package]] name = "oliphaunt-extension-unaccent-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "9ab06b4d61878a87b53afc7b047d09f5f2fd794528acb5e40d359e599b0fc956" -[[package]] -name = "oliphaunt-extension-unaccent-wasix-aot-aarch64-apple-darwin" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "37e5978c9d6e020c01336f58c8922ebaed2f4dfd6ae4568b5f91b5d416fc7cdb" - -[[package]] -name = "oliphaunt-extension-unaccent-wasix-aot-aarch64-unknown-linux-gnu" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "4ae9dd2c37edc58bf3dc34b88314e5f012221f74c96e9c538133ed162a12509e" - -[[package]] -name = "oliphaunt-extension-unaccent-wasix-aot-x86_64-pc-windows-msvc" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "f869c3c96abb7169927c921e92e44401f148e6de6138213ead88d1208462685d" - -[[package]] -name = "oliphaunt-extension-unaccent-wasix-aot-x86_64-unknown-linux-gnu" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "5c4389eaa071ac1e9bc837958ec1f5caf7f9d44a75a789b576a4938f3f0ec7cc" - [[package]] name = "oliphaunt-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "987e82c9952421633cc7d31e3ec3615856ff3833e503cac02f5b88930e7d23fc" +checksum = "3c8629c9eecf4f01df1985c1690799f0bb40c5b843b41492340d2d6d5b560b01" dependencies = [ "anyhow", "async-trait", diff --git a/examples/tauri-wasix/src-tauri/src/lib.rs b/examples/tauri-wasix/src-tauri/src/lib.rs index e9b75576..0cfd3f15 100644 --- a/examples/tauri-wasix/src-tauri/src/lib.rs +++ b/examples/tauri-wasix/src-tauri/src/lib.rs @@ -181,6 +181,9 @@ async fn init_schema(pool: &PgPool) -> Result<()> { } fn validate_wasix_tools(server: &OliphauntServer) -> Result<()> { + server + .preflight_tools() + .context("preflight split WASIX pg_dump and psql tools")?; let dump = server.dump_sql(PgDumpOptions::new().arg("--schema-only"))?; anyhow::ensure!( dump.contains("PostgreSQL database dump"), diff --git a/examples/tools/check-examples.sh b/examples/tools/check-examples.sh index 5036a7a3..08a0e78b 100755 --- a/examples/tools/check-examples.sh +++ b/examples/tools/check-examples.sh @@ -75,6 +75,7 @@ require_text "src/bindings/wasix-rust/tools/check-examples.sh" 'examples/tools/w require_text "src/bindings/wasix-rust/tools/check-examples.sh" 'PNPM_CONFIG_LOCKFILE' require_file "examples/tools/with-local-registries.sh" +require_text "examples/tools/with-local-registries.sh" 'export CARGO_HOME="\$cargo_home"' require_file "examples/tools/run-tauri-webdriver-smoke.sh" require_file "examples/tools/tauri-webdriver-smoke.mjs" require_file "examples/tools/run-electron-driver-smoke.sh" @@ -111,15 +112,18 @@ require_text "examples/tauri-wasix/src-tauri/Cargo.toml" '"tools"' require_text "examples/tauri-wasix/src-tauri/Cargo.toml" 'oliphaunt-wasix-tools' require_text "examples/tauri-wasix/src-tauri/Cargo.toml" 'liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu' require_text "examples/tauri-wasix/src-tauri/Cargo.toml" 'oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu' +require_text "examples/tauri-wasix/src-tauri/src/lib.rs" 'preflight_tools\(\)' require_text "examples/electron-wasix/src-wasix/Cargo.toml" 'registry = "oliphaunt-local"' require_text "examples/electron-wasix/src-wasix/Cargo.toml" '"tools"' require_text "examples/electron-wasix/src-wasix/Cargo.toml" 'oliphaunt-wasix-tools' require_text "examples/electron-wasix/src-wasix/Cargo.toml" 'liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu' require_text "examples/electron-wasix/src-wasix/Cargo.toml" 'oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu' +require_text "examples/electron-wasix/src-wasix/src/main.rs" 'preflight_tools\(\)' require_text "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml" 'registry = "oliphaunt-local"' require_text "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml" '"tools"' require_text "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml" 'oliphaunt-wasix-tools' require_text "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml" 'oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu' +require_text "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs" 'preflight_tools\(\)' reject_text "examples/electron/package.json" '"@oliphaunt/ts": "workspace:\*"' reject_text "examples/tauri/src-tauri/Cargo.toml" 'path = "../../../src/sdks/rust' reject_text "examples/tauri-wasix/src-tauri/Cargo.toml" 'path = "../../../src/bindings/wasix-rust' diff --git a/examples/tools/with-local-registries.sh b/examples/tools/with-local-registries.sh index 0cee5124..0d557261 100755 --- a/examples/tools/with-local-registries.sh +++ b/examples/tools/with-local-registries.sh @@ -7,6 +7,7 @@ root="$(git rev-parse --show-toplevel 2>/dev/null)" || { } cargo_index="$root/target/local-registries/cargo/index" +cargo_home="$root/target/local-registries/cargo-home" npmrc="$root/target/local-registries/verdaccio/npmrc" if [[ ! -d "$cargo_index" ]]; then @@ -16,6 +17,11 @@ if [[ ! -d "$cargo_index" ]]; then fi export CARGO_REGISTRIES_OLIPHAUNT_LOCAL_INDEX="file://$cargo_index" +mkdir -p "$cargo_home" +# Local release validation republishes the same Cargo package versions into the +# file registry. Keep Cargo's package cache local so same-version republishes do +# not reuse stale sources from ~/.cargo/registry/src. +export CARGO_HOME="$cargo_home" if [[ -f "$npmrc" ]]; then export NPM_CONFIG_USERCONFIG="$npmrc" fi diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/lib.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/lib.rs index 2dd271fc..0122fe47 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/lib.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/lib.rs @@ -16,7 +16,7 @@ pub use oliphaunt::{ Transaction, TypeParser, format_query, quote_identifier, }; #[cfg(feature = "tools")] -pub use oliphaunt::{PgDumpOptions, PsqlOptions}; +pub use oliphaunt::{PgDumpOptions, PsqlOptions, preflight_wasix_tools}; pub use protocol::messages::{BackendMessage, DatabaseError, NoticeMessage}; #[doc(hidden)] diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/mod.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/mod.rs index 8e0a4860..d1d1d5b3 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/mod.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/mod.rs @@ -44,7 +44,7 @@ pub use interface::{ ParserMap, QueryOptions, Results, RowMode, Serializer, SerializerMap, TypeParser, }; #[cfg(feature = "tools")] -pub use pg_dump::{PgDumpOptions, PsqlOptions}; +pub use pg_dump::{PgDumpOptions, PsqlOptions, preflight_wasix_tools}; #[doc(hidden)] pub use postgres_mod::{FsTraceSnapshot, fs_trace_snapshot, reset_fs_trace}; pub use proxy::{ diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/pg_dump.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/pg_dump.rs index 508b062f..27328575 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/pg_dump.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/pg_dump.rs @@ -277,6 +277,50 @@ pub(crate) fn run_server_psql(addr: SocketAddr, options: &PsqlOptions) -> Result run_psql_with_networking(addr, options, LocalNetworking::new()) } +/// Validate that the split WASIX `pg_dump` and `psql` tools are bundled and +/// loadable before invoking either tool. +pub fn preflight_wasix_tools() -> Result<()> { + preflight_pg_dump_tool().context("preflight split WASIX pg_dump tool")?; + preflight_psql_tool().context("preflight split WASIX psql tool")?; + Ok(()) +} + +fn preflight_pg_dump_tool() -> Result<()> { + let _ = pg_dump_wasm_asset()?; + let engine = aot::headless_engine(); + let _ = aot::load_pg_dump_module(&engine) + .context("load pg_dump AOT artifact from oliphaunt-wasix-tools-aot-*")?; + Ok(()) +} + +fn preflight_psql_tool() -> Result<()> { + let _ = psql_wasm_asset()?; + let engine = aot::headless_engine(); + let _ = aot::load_psql_module(&engine) + .context("load psql AOT artifact from oliphaunt-wasix-tools-aot-*")?; + Ok(()) +} + +fn pg_dump_wasm_asset() -> Result<&'static [u8]> { + assets::pg_dump_wasm() + .filter(|bytes| !bytes.is_empty()) + .ok_or_else(|| { + anyhow!( + "WASIX pg_dump asset is not bundled; enable the oliphaunt-wasix `tools` feature so Cargo installs oliphaunt-wasix-tools" + ) + }) +} + +fn psql_wasm_asset() -> Result<&'static [u8]> { + assets::psql_wasm() + .filter(|bytes| !bytes.is_empty()) + .ok_or_else(|| { + anyhow!( + "WASIX psql asset is not bundled; enable the oliphaunt-wasix `tools` feature so Cargo installs oliphaunt-wasix-tools" + ) + }) +} + pub(crate) type PgDumpVirtualSocket = TcpSocketHalf; pub(crate) fn dump_direct_sql(options: &PgDumpOptions, serve: F) -> Result @@ -323,13 +367,13 @@ where let _phase = timing::phase("pg_dump"); let wasm = { let _phase = timing::phase("pg_dump.load_embedded_module"); - assets::pg_dump_wasm() - .ok_or_else(|| anyhow!("WASIX pg_dump asset is not bundled in this build"))? + pg_dump_wasm_asset()? }; let engine = aot::headless_engine(); let module = { let _phase = timing::phase("pg_dump.load_aot"); - aot::load_pg_dump_module(&engine)? + aot::load_pg_dump_module(&engine) + .context("load pg_dump AOT artifact from oliphaunt-wasix-tools-aot-*")? }; let _store = Store::new(engine.clone()); @@ -472,13 +516,13 @@ where let _phase = timing::phase("psql"); let wasm = { let _phase = timing::phase("psql.load_embedded_module"); - assets::psql_wasm() - .ok_or_else(|| anyhow!("WASIX psql asset is not bundled in this build"))? + psql_wasm_asset()? }; let engine = aot::headless_engine(); let module = { let _phase = timing::phase("psql.load_aot"); - aot::load_psql_module(&engine)? + aot::load_psql_module(&engine) + .context("load psql AOT artifact from oliphaunt-wasix-tools-aot-*")? }; let _store = Store::new(engine.clone()); @@ -1070,6 +1114,11 @@ mod tests { PsqlOptions::new().arg("-tA").command("SELECT 1").validate() } + #[test] + fn preflight_wasix_tools_loads_split_artifacts() -> Result<()> { + preflight_wasix_tools() + } + #[test] fn pg_dump_sql_strips_only_pg18_restrict_meta_commands() { let script = "\\restrict AbC123\n\ diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/server.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/server.rs index d0e0bd4b..30ff0aa2 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/server.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/server.rs @@ -20,7 +20,9 @@ use crate::oliphaunt::config::{PostgresConfig, StartupConfig}; use crate::oliphaunt::extensions::{Extension, resolve_extension_set}; use crate::oliphaunt::interface::DebugLevel; #[cfg(feature = "tools")] -use crate::oliphaunt::pg_dump::{PgDumpOptions, PsqlOptions, dump_server_sql, run_server_psql}; +use crate::oliphaunt::pg_dump::{ + PgDumpOptions, PsqlOptions, dump_server_sql, preflight_wasix_tools, run_server_psql, +}; use crate::oliphaunt::proxy::OliphauntProxy; use crate::oliphaunt::timing; @@ -116,6 +118,15 @@ impl OliphauntServer { dump_server_sql(addr, &options) } + /// Validate that split WASIX `pg_dump` and `psql` artifacts are installed + /// and loadable for this server before invoking either tool. + #[cfg(feature = "tools")] + pub fn preflight_tools(&self) -> Result<()> { + self.tcp_addr() + .context("WASIX pg_dump and psql currently require a TCP OliphauntServer endpoint")?; + preflight_wasix_tools() + } + /// Run the bundled WASIX `pg_dump` and return UTF-8 SQL bytes. #[cfg(feature = "tools")] pub fn dump_bytes(&self, options: PgDumpOptions) -> Result> { diff --git a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock index eb2a285a..811a5a89 100644 --- a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock +++ b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock @@ -3528,7 +3528,7 @@ dependencies = [ name = "oliphaunt-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "987e82c9952421633cc7d31e3ec3615856ff3833e503cac02f5b88930e7d23fc" +checksum = "3c8629c9eecf4f01df1985c1690799f0bb40c5b843b41492340d2d6d5b560b01" dependencies = [ "anyhow", "async-trait", @@ -3607,7 +3607,7 @@ dependencies = [ name = "liboliphaunt-wasix-portable" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "74e4a84c8db15e4be7945d7b3a2ab1cb30a687b155367f32a25155891f604e77" +checksum = "67857a0fbca85a256e60c4ea9901958cad8fb28b7d1ee4033dbdbc0385ab9baa" dependencies = [ "serde", "serde_json", diff --git a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs index e59363be..1f00ed73 100644 --- a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs +++ b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs @@ -337,6 +337,9 @@ fn validate_wasix_tools(server: &OliphauntServer) -> Result<()> { if server.tcp_addr().is_none() { return Ok(()); } + server + .preflight_tools() + .context("preflight split WASIX pg_dump and psql tools")?; let dump = server.dump_sql(PgDumpOptions::new().arg("--schema-only"))?; anyhow::ensure!( dump.contains("PostgreSQL database dump"), diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index b34cbefa..8b7b6b39 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -1434,6 +1434,29 @@ def check_wasm(findings: list[Finding]) -> None: f"oliphaunt-wasix Cargo.toml tools={features.get('tools')!r}", severity="P0", ) + pg_dump_source = read_text( + "src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/pg_dump.rs" + ) + server_source = read_text( + "src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/server.rs" + ) + require( + findings, + product, + "wasm-tools-preflight-api", + "pub fn preflight_wasix_tools() -> Result<()>" in pg_dump_source + and "pub fn preflight_tools(&self) -> Result<()>" in server_source + and "preflight_wasix_tools" in lib_rs + and "load_pg_dump_module(&engine)" in pg_dump_source + and "load_psql_module(&engine)" in pg_dump_source, + "WASM Rust SDK must expose an explicit split pg_dump/psql tools preflight that validates WASM payloads and target AOT artifacts before first tool use.", + [ + "src/bindings/wasix-rust/crates/oliphaunt-wasix/src/lib.rs", + "src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/server.rs", + "src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/pg_dump.rs", + ], + severity="P0", + ) runtime_version = product_metadata.read_current_version("liboliphaunt-wasix") dependencies = manifest.get("dependencies", {}) target_tables = manifest.get("target", {}) diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 00f7d9f0..a9ea547e 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1092,6 +1092,17 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None tools_feature = set(manifest.get("features", {}).get("tools", [])) if tools_feature != expected_tools_feature: fail("oliphaunt-wasix tools feature must select exactly the WASIX pg_dump/psql tool artifact crates") + sdk_lib_source = read_text("src/bindings/wasix-rust/crates/oliphaunt-wasix/src/lib.rs") + sdk_server_source = read_text("src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/server.rs") + sdk_pg_dump_source = read_text("src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/pg_dump.rs") + if ( + "pub fn preflight_wasix_tools() -> Result<()>" not in sdk_pg_dump_source + or "pub fn preflight_tools(&self) -> Result<()>" not in sdk_server_source + or "preflight_wasix_tools" not in sdk_lib_source + or "load_pg_dump_module(&engine)" not in sdk_pg_dump_source + or "load_psql_module(&engine)" not in sdk_pg_dump_source + ): + fail("oliphaunt-wasix must expose an explicit split pg_dump/psql tools preflight that validates payload and AOT artifacts") aot_source = read_text("src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs") for cfg in expected_aot_dependencies: rust_cfg = cfg.removeprefix("cfg(").removesuffix(")") From d568c0f4c19d7b73def63599d212c6a1d38b53d5 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 03:48:38 +0000 Subject: [PATCH 031/137] test: enforce android extension artifact validation --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 13 ++++++----- .../examples-ci-release-validation.md | 15 ++++++++----- tools/release/check_consumer_shape.py | 21 ++++++++++++++++++ tools/release/check_release_metadata.py | 22 ++++++++++++++++++- 4 files changed, 58 insertions(+), 13 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 9679ede5..a3fc0efe 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -40,7 +40,7 @@ review production pipelines, then normalize implementation details. - [ ] Ensure SDKs exercise the same control flows for runtime setup, extension selection, artifact validation, and tool access. - [x] Add Android split/local runtime validation so selected extensions must exist in the copied runtime tree before manifests are published. - [ ] Align or explicitly document Deno native runtime/tools/extension resolution versus Node and Bun. -- [ ] Port stronger exact-extension artifact validation into the Android Gradle resolver. +- [x] Port stronger exact-extension artifact validation into the Android Gradle resolver. - [x] Pass mobile `sharedPreloadLibraries` through to startup arguments consistently. - [x] Add an explicit WASIX split-tools preflight path before first `pg_dump` or `psql` call. - [ ] Identify feature gaps where one SDK exposes a runtime/tool/extension capability differently from the others. @@ -107,11 +107,12 @@ review production pipelines, then normalize implementation details. control and versioned SQL files in the copied runtime tree before generated manifests can declare those extensions. The public Android Gradle resolver applies the same check after Maven exact-extension runtime artifacts are - merged. -- Subagent SDK audit found these next fixes: align or explicitly document Deno - native runtime/tools/extension resolution, port stronger exact-extension - validation into the Android Gradle resolver, and add an explicit WASIX tools - preflight. + merged, and release metadata plus consumer-shape checks now enforce that + resolver behavior. +- Subagent SDK audit found these remaining next fixes: continue the broader SDK + artifact-resolution comparison, keep Deno native extension handling explicit, + identify any remaining feature gaps across SDKs, and add parity checks for + invariants that are still documented only in prose. - Local workflow tooling is available: `act` is installed at v0.2.89, which matches the latest upstream release published on 2026-06-01, Docker is available, `act -l` parses the CI, Release, and mobile E2E workflow graph, diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index 9ab33b02..5c291f2b 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -64,7 +64,7 @@ the release/tooling surface after the runtime tool crate split. declare the selected extensions. - [ ] Align Deno native runtime/tools/extension resolution with Node/Bun, or document and test Deno as intentionally unsupported for registry-managed extensions. -- [ ] Port Rust/JS exact-extension archive validation rules into the Android Gradle +- [x] Port Rust/JS exact-extension archive validation rules into the Android Gradle resolver. - [x] Thread mobile `sharedPreloadLibraries` from manifests into startup args. - [x] Add an explicit WASIX tools preflight before first `pg_dump` or `psql` use. @@ -172,14 +172,17 @@ the release/tooling surface after the runtime tool crate split. - Android split/local runtime packaging now rejects selected extensions missing control or versioned SQL files in the copied runtime tree before manifests declare them. The public Android Gradle resolver performs the same check - after Maven exact-extension runtime artifacts are merged. + after Maven exact-extension runtime artifacts are merged. Release metadata + and consumer-shape checks now enforce that the resolver extracts the selected + Maven artifact, merges its `files/` payload, and validates both the selected + `.control` file and versioned SQL files before updating generated manifests. - Mobile native-direct startup now passes packaged runtime `sharedPreloadLibraries` through to `shared_preload_libraries=...` startup args in Kotlin Android/React Native Android and Swift/React Native iOS. Kotlin static/unit checks, mobile extension policy checks, and release checks passed locally; Swift-specific test execution was not run because this Linux host does not have a Swift toolchain. -- A read-only SDK parity audit found these next issues: Deno native resolution - does not follow Node/Bun tools and extension materialization, Android Maven - extension validation is weaker than Rust/JS, and WASIX split tools are only - validated lazily. +- A read-only SDK parity audit found these remaining issues: Deno native + resolution does not follow Node/Bun extension materialization, broader + SDK resolver/control-flow parity still needs a full pass, and any remaining + prose-only invariants should gain policy checks. diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 8b7b6b39..59fabf31 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -1109,6 +1109,27 @@ def check_kotlin(findings: list[Finding]) -> None: f"ResolveOliphauntAndroidAssetsTask.java missing {required}", severity="P0", ) + android_extension_validation_fragments = [ + "extractExtensionRuntimeArtifact(sqlName, artifact)", + 'copyTree(new File(artifactRoot, "files").toPath(), runtimeFiles.toPath())', + "validateSelectedExtensionRuntimeFiles(runtimeFiles, artifacts);", + "private static void validateSelectedExtensionRuntimeFiles", + 'artifact.sqlName + ".control"', + '" is missing packaged control file "', + "extensionSqlFiles(runtimeFiles, artifact.sqlName);", + 'file.getName().startsWith(sqlName + "--")', + 'file.getName().endsWith(".sql")', + '" has no packaged SQL files in "', + ] + require( + findings, + product, + "android-exact-extension-runtime-validation", + all(fragment in resolver_source for fragment in android_extension_validation_fragments), + "Android exact-extension resolver must validate selected Maven runtime artifacts by SQL name and reject manifests unless the merged runtime contains the selected control file and versioned SQL files.", + "src/sdks/kotlin/oliphaunt-android-gradle-plugin/src/main/java/dev/oliphaunt/android/ResolveOliphauntAndroidAssetsTask.java", + severity="P0", + ) maven_artifact_publisher = read_text("src/sdks/kotlin/oliphaunt-maven-artifacts/build.gradle.kts") release_cli = read_text("tools/release/release.py") release_workflow = read_text(".github/workflows/release.yml") diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index a9ea547e..af5dc2fb 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -570,9 +570,29 @@ def validate_kotlin(kotlin_version: str, liboliphaunt_version: str) -> None: "dev.oliphaunt.runtime:oliphaunt-icu", "Kotlin README must document the optional ICU Maven artifact", ) + android_resolver = ( + "src/sdks/kotlin/oliphaunt-android-gradle-plugin/src/main/java/dev/oliphaunt/android/ResolveOliphauntAndroidAssetsTask.java" + ) + for needle in [ + "extractExtensionRuntimeArtifact(sqlName, artifact)", + 'copyTree(new File(artifactRoot, "files").toPath(), runtimeFiles.toPath())', + "validateSelectedExtensionRuntimeFiles(runtimeFiles, artifacts);", + "private static void validateSelectedExtensionRuntimeFiles", + 'artifact.sqlName + ".control"', + '" is missing packaged control file "', + "extensionSqlFiles(runtimeFiles, artifact.sqlName);", + 'file.getName().startsWith(sqlName + "--")', + 'file.getName().endsWith(".sql")', + '" has no packaged SQL files in "', + ]: + require_text( + android_resolver, + needle, + "Android Gradle resolver must validate selected exact-extension runtime artifacts before generated manifests declare them", + ) for path in [ "src/sdks/kotlin/oliphaunt-android-gradle-plugin/src/main/java/dev/oliphaunt/android/OliphauntAndroidPlugin.java", - "src/sdks/kotlin/oliphaunt-android-gradle-plugin/src/main/java/dev/oliphaunt/android/ResolveOliphauntAndroidAssetsTask.java", + android_resolver, "src/sdks/kotlin/oliphaunt/build.gradle.kts", ]: for forbidden in [ From 1913ed7a2453bdd75c6ed1222b20d1d289f5ca68 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 03:57:33 +0000 Subject: [PATCH 032/137] test: derive wasix cargo package checks --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 11 ++++- .../examples-ci-release-validation.md | 10 ++++- tools/release/check_consumer_shape.py | 42 ++++++------------- tools/release/check_release_metadata.py | 42 ++++++------------- ...kage_liboliphaunt_wasix_cargo_artifacts.py | 31 ++++++++++++++ tools/release/release.py | 10 ++--- 6 files changed, 75 insertions(+), 71 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index a3fc0efe..a5d7e34d 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -101,8 +101,15 @@ review production pipelines, then normalize implementation details. `oliphaunt-broker` now derive platform target membership and npm package names from `artifact_targets`, with only registry naming conventions kept in the checker. -- Subagent CI/release audit found these remaining next fixes: collapse remaining - literal workflow/policy checks back to generated package contracts. +- WASIX Cargo artifact package-family checks now derive the portable runtime, + tools, ICU, root AOT, and tools-AOT crate names from + `package_liboliphaunt_wasix_cargo_artifacts.public_cargo_package_names()`. + The same packager helper also drives the WASIX AOT target-cfg dependency maps + and `tools` feature dependency expectations used by release metadata, + consumer-shape, and release publication checks. +- CI/release DRY audit still needs a pass over broader workflow topology string + checks to distinguish legitimate job-shape assertions from remaining copied + package-surface contracts. - Android split/local runtime packaging now validates selected extension control and versioned SQL files in the copied runtime tree before generated manifests can declare those extensions. The public Android Gradle resolver diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index 5c291f2b..3d047556 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -160,6 +160,11 @@ the release/tooling surface after the runtime tool crate split. - Consumer-shape registry package checks for `liboliphaunt-native` and `oliphaunt-broker` now derive platform target membership and npm package names from `artifact_targets`. +- WASIX Cargo artifact checks now derive the public portable runtime, tools, + ICU, root AOT, and tools-AOT package family from the WASIX Cargo packager + helper used by release publication. The same helper drives the WASIX target + AOT Cargo dependency maps and the `oliphaunt-wasix` `tools` feature + expectations in release metadata and consumer-shape checks. - Local GitHub Actions discovery is ready on Linux: `act` v0.2.89, Docker, and `gh` are installed, and `act -l` parses the CI, Release, and mobile E2E workflows. `act workflow_dispatch -W .github/workflows/ci.yml -j release-intent @@ -167,8 +172,9 @@ the release/tooling surface after the runtime tool crate split. expected Linux CI job. Full local lane execution should run from a committed disposable worktree because `actions/checkout` validates committed HEAD, not uncommitted edits. -- A read-only CI/release audit found this remaining issue: some policy checks - compare copied literals instead of generated package contracts. +- CI/release DRY audit still needs a pass over broader workflow topology string + checks to separate legitimate job-shape assertions from remaining copied + package-surface contracts. - Android split/local runtime packaging now rejects selected extensions missing control or versioned SQL files in the copied runtime tree before manifests declare them. The public Android Gradle resolver performs the same check diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 59fabf31..bf7fe327 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -20,6 +20,7 @@ import artifact_targets import product_metadata import extension_artifact_targets +import package_liboliphaunt_wasix_cargo_artifacts ROOT = Path(__file__).resolve().parents[2] @@ -1439,13 +1440,9 @@ def check_wasm(findings: list[Finding]) -> None: f"oliphaunt-wasix Cargo.toml default={features.get('default')!r}", severity="P0", ) - expected_tools_feature = { - "dep:oliphaunt-wasix-tools", - "dep:oliphaunt-wasix-tools-aot-aarch64-apple-darwin", - "dep:oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu", - "dep:oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc", - "dep:oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu", - } + expected_tools_feature = ( + package_liboliphaunt_wasix_cargo_artifacts.public_tools_feature_dependencies() + ) require( findings, product, @@ -1519,18 +1516,12 @@ def check_wasm(findings: list[Finding]) -> None: f"oliphaunt-icu dependency={expected_icu_dependency!r}", severity="P0", ) - expected_aot_dependencies = { - 'cfg(all(target_os = "macos", target_arch = "aarch64"))': "liboliphaunt-wasix-aot-aarch64-apple-darwin", - 'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))': "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", - 'cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))': "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu", - 'cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))': "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc", - } - expected_tools_aot_dependencies = { - 'cfg(all(target_os = "macos", target_arch = "aarch64"))': "oliphaunt-wasix-tools-aot-aarch64-apple-darwin", - 'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))': "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu", - 'cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))': "oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu", - 'cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))': "oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc", - } + expected_aot_dependencies = ( + package_liboliphaunt_wasix_cargo_artifacts.public_aot_cargo_dependencies() + ) + expected_tools_aot_dependencies = ( + package_liboliphaunt_wasix_cargo_artifacts.public_tools_aot_cargo_dependencies() + ) missing_aot_dependencies = [] for cfg, crate in expected_aot_dependencies.items(): target = target_tables.get(cfg) @@ -1704,17 +1695,8 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: ) registry_packages = set(product_registry_packages(product)) expected_registry_packages = { - "crates:oliphaunt-icu", - "crates:liboliphaunt-wasix-portable", - "crates:oliphaunt-wasix-tools", - "crates:liboliphaunt-wasix-aot-aarch64-apple-darwin", - "crates:liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu", - "crates:liboliphaunt-wasix-aot-x86_64-pc-windows-msvc", - "crates:liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", - "crates:oliphaunt-wasix-tools-aot-aarch64-apple-darwin", - "crates:oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu", - "crates:oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc", - "crates:oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu", + f"crates:{name}" + for name in package_liboliphaunt_wasix_cargo_artifacts.public_cargo_package_names() } require( findings, diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index af5dc2fb..303edcd8 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -13,6 +13,7 @@ import artifact_targets import extension_artifact_targets import optimize_native_runtime_payload +import package_liboliphaunt_wasix_cargo_artifacts import product_metadata @@ -1073,18 +1074,12 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None or icu_dependency.get("optional") is not True ): fail("oliphaunt-wasix source must optionally depend on the local oliphaunt-icu path crate version") - expected_aot_dependencies = { - 'cfg(all(target_os = "macos", target_arch = "aarch64"))': "liboliphaunt-wasix-aot-aarch64-apple-darwin", - 'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))': "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", - 'cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))': "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu", - 'cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))': "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc", - } - expected_tools_aot_dependencies = { - 'cfg(all(target_os = "macos", target_arch = "aarch64"))': "oliphaunt-wasix-tools-aot-aarch64-apple-darwin", - 'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))': "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu", - 'cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))': "oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu", - 'cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))': "oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc", - } + expected_aot_dependencies = ( + package_liboliphaunt_wasix_cargo_artifacts.public_aot_cargo_dependencies() + ) + expected_tools_aot_dependencies = ( + package_liboliphaunt_wasix_cargo_artifacts.public_tools_aot_cargo_dependencies() + ) target_tables = manifest.get("target", {}) for cfg, crate in expected_aot_dependencies.items(): target = target_tables.get(cfg) @@ -1102,13 +1097,9 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None or dependency.get("optional") is not True ): fail(f"oliphaunt-wasix must optionally depend on {crate} at the exact liboliphaunt-wasix runtime version behind {cfg}") - expected_tools_feature = { - "dep:oliphaunt-wasix-tools", - "dep:oliphaunt-wasix-tools-aot-aarch64-apple-darwin", - "dep:oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu", - "dep:oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc", - "dep:oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu", - } + expected_tools_feature = ( + package_liboliphaunt_wasix_cargo_artifacts.public_tools_feature_dependencies() + ) tools_feature = set(manifest.get("features", {}).get("tools", [])) if tools_feature != expected_tools_feature: fail("oliphaunt-wasix tools feature must select exactly the WASIX pg_dump/psql tool artifact crates") @@ -1147,17 +1138,8 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None fail("liboliphaunt-wasix must publish GitHub release assets and crates.io WASIX artifact crates") registry_packages = set(product_metadata.string_list(runtime_config, "registry_packages", "liboliphaunt-wasix")) expected_registry_packages = { - "crates:oliphaunt-icu", - "crates:liboliphaunt-wasix-portable", - "crates:oliphaunt-wasix-tools", - "crates:liboliphaunt-wasix-aot-aarch64-apple-darwin", - "crates:liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu", - "crates:liboliphaunt-wasix-aot-x86_64-pc-windows-msvc", - "crates:liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", - "crates:oliphaunt-wasix-tools-aot-aarch64-apple-darwin", - "crates:oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu", - "crates:oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc", - "crates:oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu", + f"crates:{name}" + for name in package_liboliphaunt_wasix_cargo_artifacts.public_cargo_package_names() } if registry_packages != expected_registry_packages: fail( diff --git a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py index ce0155f8..27be7763 100644 --- a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py +++ b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py @@ -64,6 +64,37 @@ EXPECTED_EXTENSION_AOT_TARGETS = frozenset(AOT_TARGET_TRIPLES.values()) +def public_cargo_package_names() -> tuple[str, ...]: + return ( + ICU_PACKAGE, + RUNTIME_PACKAGE, + TOOLS_PACKAGE, + *AOT_PACKAGES.values(), + *TOOLS_AOT_PACKAGES.values(), + ) + + +def public_aot_cargo_dependencies() -> dict[str, str]: + return { + AOT_TARGET_CFGS[AOT_TARGET_TRIPLES[target]]: package + for target, package in AOT_PACKAGES.items() + } + + +def public_tools_aot_cargo_dependencies() -> dict[str, str]: + return { + AOT_TARGET_CFGS[AOT_TARGET_TRIPLES[target]]: package + for target, package in TOOLS_AOT_PACKAGES.items() + } + + +def public_tools_feature_dependencies() -> set[str]: + return { + f"dep:{TOOLS_PACKAGE}", + *(f"dep:{package}" for package in TOOLS_AOT_PACKAGES.values()), + } + + @dataclass(frozen=True) class PackageSpec: name: str diff --git a/tools/release/release.py b/tools/release/release.py index 25b06475..4612801a 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -2712,13 +2712,9 @@ def liboliphaunt_wasix_cargo_artifact_crates(version: str) -> list[tuple[str, Pa if data.get("schema") != package_liboliphaunt_wasix_cargo_artifacts.SCHEMA or not isinstance(packages_data, list): fail(f"{manifest_path.relative_to(ROOT)} has an invalid schema") - expected_base_crates = { - package_liboliphaunt_wasix_cargo_artifacts.ICU_PACKAGE, - package_liboliphaunt_wasix_cargo_artifacts.RUNTIME_PACKAGE, - package_liboliphaunt_wasix_cargo_artifacts.TOOLS_PACKAGE, - *package_liboliphaunt_wasix_cargo_artifacts.AOT_PACKAGES.values(), - *package_liboliphaunt_wasix_cargo_artifacts.TOOLS_AOT_PACKAGES.values(), - } + expected_base_crates = set( + package_liboliphaunt_wasix_cargo_artifacts.public_cargo_package_names() + ) configured_crates = set(check_cratesio_publication.product_crates("liboliphaunt-wasix")) if configured_crates != expected_base_crates: fail( From 54857b0e5a81997c4d3b3c483a8cb3cf27988c0d Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 04:05:35 +0000 Subject: [PATCH 033/137] test: validate wasix runtime tool split --- docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 2 ++ docs/maintainers/examples-ci-release-validation.md | 3 +++ tools/release/check_consumer_shape.py | 13 +++++++++++++ tools/release/release.py | 12 +++++++++++- 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index a5d7e34d..ec046117 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -63,6 +63,8 @@ review production pipelines, then normalize implementation details. - The active branch contains the split native/WASIX tools package work and the example GUI smoke coverage. - Local-registry WASIX smoke coverage proves `pg_dump` through the SDK `dump_sql` path and `psql` through `PsqlOptions::command("SELECT 1")`. - Local-registry Cargo payload inspection confirmed `liboliphaunt-native-linux-x64-gnu-part-*` contains `initdb`, `pg_ctl`, and `postgres` only under `runtime/bin`, while `oliphaunt-tools-linux-x64-gnu-part-*` contains only `pg_dump` and `psql` there. +- Release dry-run validation now inspects the nested WASIX runtime archive for + `postgres` and `initdb`, and rejects `pg_ctl`, `pg_dump`, or `psql` there. - Local registry publication was refreshed with explicit native runtime/tools, broker, WASIX runtime/tools/AOT, extension, JS SDK, and node-direct artifact roots. The npm install surface now includes `@oliphaunt/tools-linux-x64-gnu` diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index 3d047556..a1936568 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -98,6 +98,9 @@ the release/tooling surface after the runtime tool crate split. - WASIX portable assets were rebuilt with the runtime root limited to `postgres` and `initdb`; `pg_ctl` is not bundled for WASIX, and `pg_dump` plus `psql` are split into standalone tool payloads. +- Release validation now checks the nested WASIX runtime archive for + `postgres` and `initdb`, and fails if `pg_ctl`, `pg_dump`, or `psql` are + present there. - WASIX Cargo artifact generation now emits `liboliphaunt-wasix-portable`, `oliphaunt-wasix-tools`, per-target `liboliphaunt-wasix-aot-*`, and per-target `oliphaunt-wasix-tools-aot-*` crates. The root portable crate, diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index bf7fe327..4714dfb7 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -1721,6 +1721,19 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: ["tools/release/release.py", ".github/workflows/release.yml"], severity="P0", ) + require( + findings, + product, + "wasix-portable-runtime-tool-contract", + "oliphaunt/bin/initdb" in release_source + and "oliphaunt/bin/postgres" in release_source + and "oliphaunt/bin/pg_ctl" in release_source + and "oliphaunt/bin/pg_dump" in release_source + and "oliphaunt/bin/psql" in release_source, + "Release validation must require postgres/initdb in the WASIX runtime archive and reject pg_ctl/pg_dump/psql there.", + "tools/release/release.py", + severity="P0", + ) require( findings, product, diff --git a/tools/release/release.py b/tools/release/release.py index 4612801a..a696e7e9 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -961,6 +961,16 @@ def validate_wasix_portable_release_asset(archive: Path) -> None: "target/oliphaunt-wasix/assets/oliphaunt.wasix.tar.zst", ) runtime_members = {normalized_tar_member(member) for member in tar_zstd_bytes_members(runtime_archive, "WASIX runtime archive")} + missing_runtime_tools = sorted( + member + for member in {"oliphaunt/bin/initdb", "oliphaunt/bin/postgres"} + if member not in runtime_members + ) + if missing_runtime_tools: + fail( + f"{archive.relative_to(ROOT)} must bundle core WASIX runtime binaries inside target/oliphaunt-wasix/assets/oliphaunt.wasix.tar.zst: " + + ", ".join(missing_runtime_tools) + ) bundled_icu = sorted( member for member in runtime_members @@ -974,7 +984,7 @@ def validate_wasix_portable_release_asset(archive: Path) -> None: bundled_tools = sorted( member for member in runtime_members - if member in {"oliphaunt/bin/pg_dump", "oliphaunt/bin/psql"} + if member in {"oliphaunt/bin/pg_ctl", "oliphaunt/bin/pg_dump", "oliphaunt/bin/psql"} ) if bundled_tools: fail( From f049800c3ec7cb0fd492c6a16d862ab8120c3936 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 04:17:15 +0000 Subject: [PATCH 034/137] ci: derive sdk package artifact handoff --- .github/workflows/release.yml | 19 +++++---- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 5 +++ .../examples-ci-release-validation.md | 4 ++ .../crates/oliphaunt-wasix/release.toml | 2 +- tools/policy/check-release-policy.py | 11 +++-- tools/release/artifact_targets.py | 23 +++++++++++ tools/release/check_artifact_targets.py | 41 ++++++------------- tools/release/local_registry_publish.py | 7 +--- tools/release/release.py | 14 +++++-- 9 files changed, 74 insertions(+), 52 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2f5d44fa..51a3ef14 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -391,21 +391,24 @@ jobs: run: | download_sdk_artifact() { local product="$1" - local artifact="$2" + local artifact_args=() + while IFS= read -r artifact; do + artifact_args+=(--artifact "$artifact") + done < <(tools/release/release.py ci-artifacts --product "$product" --family sdk-package) .github/scripts/download-build-artifacts.sh \ CI \ "$RELEASE_HEAD_SHA" \ "target/sdk-artifacts/$product" \ --run-id "$CI_RUN_ID" \ --job Builds \ - --artifact "$artifact" + "${artifact_args[@]}" } - [ "$PRODUCT_OLIPHAUNT_RUST" != "true" ] || download_sdk_artifact oliphaunt-rust oliphaunt-rust-sdk-package-artifacts - [ "$PRODUCT_OLIPHAUNT_SWIFT" != "true" ] || download_sdk_artifact oliphaunt-swift oliphaunt-swift-sdk-package-artifacts - [ "$PRODUCT_OLIPHAUNT_KOTLIN" != "true" ] || download_sdk_artifact oliphaunt-kotlin oliphaunt-kotlin-sdk-package-artifacts - [ "$PRODUCT_OLIPHAUNT_REACT_NATIVE" != "true" ] || download_sdk_artifact oliphaunt-react-native oliphaunt-react-native-sdk-package-artifacts - [ "$PRODUCT_OLIPHAUNT_JS" != "true" ] || download_sdk_artifact oliphaunt-js oliphaunt-js-sdk-package-artifacts - [ "$PRODUCT_OLIPHAUNT_WASIX_RUST" != "true" ] || download_sdk_artifact oliphaunt-wasix-rust oliphaunt-wasix-rust-package-artifacts + [ "$PRODUCT_OLIPHAUNT_RUST" != "true" ] || download_sdk_artifact oliphaunt-rust + [ "$PRODUCT_OLIPHAUNT_SWIFT" != "true" ] || download_sdk_artifact oliphaunt-swift + [ "$PRODUCT_OLIPHAUNT_KOTLIN" != "true" ] || download_sdk_artifact oliphaunt-kotlin + [ "$PRODUCT_OLIPHAUNT_REACT_NATIVE" != "true" ] || download_sdk_artifact oliphaunt-react-native + [ "$PRODUCT_OLIPHAUNT_JS" != "true" ] || download_sdk_artifact oliphaunt-js + [ "$PRODUCT_OLIPHAUNT_WASIX_RUST" != "true" ] || download_sdk_artifact oliphaunt-wasix-rust - name: Download liboliphaunt release assets if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && steps.release_plan.outputs.product_liboliphaunt_native == 'true' }} diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index ec046117..7fd9e85d 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -109,6 +109,11 @@ review production pipelines, then normalize implementation details. The same packager helper also drives the WASIX AOT target-cfg dependency maps and `tools` feature dependency expectations used by release metadata, consumer-shape, and release publication checks. +- SDK CI package artifact names now derive from release products marked + `kind = "sdk"`. The release workflow and local registry publisher use + `release.py ci-artifacts --family sdk-package` instead of repeating + per-product artifact names, and the WASIX Rust binding is normalized to the + same SDK release kind. - CI/release DRY audit still needs a pass over broader workflow topology string checks to distinguish legitimate job-shape assertions from remaining copied package-surface contracts. diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index a1936568..945af3d8 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -168,6 +168,10 @@ the release/tooling surface after the runtime tool crate split. helper used by release publication. The same helper drives the WASIX target AOT Cargo dependency maps and the `oliphaunt-wasix` `tools` feature expectations in release metadata and consumer-shape checks. +- SDK package artifact names now derive from release products with + `kind = "sdk"`. Release downloads and local registry publication ask + `release.py ci-artifacts --family sdk-package` for the artifact name, and + the WASIX Rust binding uses the same SDK release kind as the other SDKs. - Local GitHub Actions discovery is ready on Linux: `act` v0.2.89, Docker, and `gh` are installed, and `act -l` parses the CI, Release, and mobile E2E workflows. `act workflow_dispatch -W .github/workflows/ci.yml -j release-intent diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/release.toml b/src/bindings/wasix-rust/crates/oliphaunt-wasix/release.toml index 72f14e82..23a406a2 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/release.toml +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/release.toml @@ -1,6 +1,6 @@ id = "oliphaunt-wasix-rust" owner = "@oliphaunt/wasix-rust" -kind = "wasix-rust-binding" +kind = "sdk" publish_targets = ["crates-io"] registry_packages = ["crates:oliphaunt-wasix"] release_artifacts = ["cargo-crate"] diff --git a/tools/policy/check-release-policy.py b/tools/policy/check-release-policy.py index 415781c6..9734044c 100644 --- a/tools/policy/check-release-policy.py +++ b/tools/policy/check-release-policy.py @@ -722,12 +722,7 @@ def check_release_workflow_policy() -> None: "--artifact oliphaunt-extension-package-artifacts", "--artifact liboliphaunt-native-release-assets", "--artifact \"$artifact\"", - "download_sdk_artifact oliphaunt-rust oliphaunt-rust-sdk-package-artifacts", - "download_sdk_artifact oliphaunt-swift oliphaunt-swift-sdk-package-artifacts", - "download_sdk_artifact oliphaunt-kotlin oliphaunt-kotlin-sdk-package-artifacts", - "download_sdk_artifact oliphaunt-react-native oliphaunt-react-native-sdk-package-artifacts", - "download_sdk_artifact oliphaunt-js oliphaunt-js-sdk-package-artifacts", - "download_sdk_artifact oliphaunt-wasix-rust oliphaunt-wasix-rust-package-artifacts", + "tools/release/release.py ci-artifacts --product \"$product\" --family sdk-package", "tools/release/release.py ci-artifacts --product \"$product\" --kind \"$kind\" --family release-assets", "tools/release/release.py ci-artifacts --product oliphaunt-node-direct --kind node-direct-addon --family npm-package", "pnpm install --frozen-lockfile", @@ -738,6 +733,10 @@ def check_release_workflow_policy() -> None: ): if snippet not in publish_block: fail(f"Release workflow dry-run handoff is missing {snippet!r}") + for product in artifact_targets.sdk_package_products(): + snippet = f"download_sdk_artifact {product}" + if snippet not in publish_block: + fail(f"Release workflow dry-run handoff is missing {snippet!r}") if "target/release-assets/native" in publish_block: fail("Release workflow must download native helper artifacts into product-owned release asset roots") diff --git a/tools/release/artifact_targets.py b/tools/release/artifact_targets.py index d9adde37..33f39f64 100644 --- a/tools/release/artifact_targets.py +++ b/tools/release/artifact_targets.py @@ -671,6 +671,29 @@ def ci_npm_package_artifact_names(product: str, kind: str) -> list[str]: return sorted(names) +def ci_sdk_package_artifact_name(product: str) -> str: + config = product_metadata.product_config(product) + if config.get("kind") != "sdk": + product_metadata.fail(f"{product} is not an SDK release product") + if product == "oliphaunt-wasix-rust": + return f"{product}-package-artifacts" + return f"{product}-sdk-package-artifacts" + + +def sdk_package_products() -> tuple[str, ...]: + return tuple( + product + for product, config in product_metadata.graph_products().items() + if config.get("kind") == "sdk" + ) + + +def ci_sdk_package_artifact_names(product: str | None = None) -> list[str]: + if product is not None: + return [ci_sdk_package_artifact_name(product)] + return [ci_sdk_package_artifact_name(sdk_product) for sdk_product in sdk_package_products()] + + def typescript_optional_runtime_package_products() -> dict[str, str]: package_products: dict[str, str] = {} selectors = [ diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py index a8d0dd1b..33785140 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -330,20 +330,8 @@ def validate_ci_release_artifacts() -> None: ".github/scripts/run-planned-moon-job.sh node-direct": "CI must invoke the planned Node direct Moon job that includes release-shaped addon artifacts", "oliphaunt-node-direct-release-assets-${{ matrix.target }}": "CI must upload Node direct release-shaped artifacts per target", "oliphaunt-node-direct-npm-package-${{ matrix.target }}": "CI must upload Node direct optional npm package artifacts per target", - "oliphaunt-rust-sdk-package-artifacts": "CI must upload Rust SDK package artifacts", - "oliphaunt-swift-sdk-package-artifacts": "CI must upload Swift SDK package artifacts", - "oliphaunt-kotlin-sdk-package-artifacts": "CI must upload Kotlin SDK package artifacts", - "oliphaunt-react-native-sdk-package-artifacts": "CI must upload React Native SDK package artifacts", - "oliphaunt-js-sdk-package-artifacts": "CI must upload TypeScript SDK package artifacts", - "oliphaunt-wasix-rust-package-artifacts": "CI must upload WASIX Rust binding package artifacts", "oliphaunt-extension-package-artifacts": "CI must upload exact-extension package artifacts", "oliphaunt-mobile-extension-package-artifacts": "CI must upload target-scoped mobile exact-extension package artifacts", - "target/sdk-artifacts/oliphaunt-rust": "CI must use the shared SDK artifact staging layout for Rust", - "target/sdk-artifacts/oliphaunt-swift": "CI must use the shared SDK artifact staging layout for Swift", - "target/sdk-artifacts/oliphaunt-kotlin": "CI must use the shared SDK artifact staging layout for Kotlin", - "target/sdk-artifacts/oliphaunt-react-native": "CI must use the shared SDK artifact staging layout for React Native", - "target/sdk-artifacts/oliphaunt-js": "CI must use the shared SDK artifact staging layout for TypeScript", - "target/sdk-artifacts/oliphaunt-wasix-rust": "CI must use the shared SDK artifact staging layout for the WASIX Rust binding", "target/extension-artifacts": "CI must use the shared exact-extension package staging layout", ".github/scripts/run-planned-moon-job.sh extension-packages": "CI must invoke the Moon-modeled exact-extension package builder", ".github/scripts/run-planned-moon-job.sh mobile-extension-packages": "CI must invoke the Moon-modeled mobile exact-extension package builder", @@ -413,6 +401,17 @@ def validate_ci_release_artifacts() -> None: for snippet, message in required_ci_snippets.items(): if snippet not in ci: fail(message) + for artifact in artifact_targets.ci_sdk_package_artifact_names(): + if artifact not in ci: + fail(f"CI must upload SDK package artifact {artifact}") + for product in artifact_targets.sdk_package_products(): + if f"target/sdk-artifacts/{product}" not in ci: + fail(f"CI must use the shared SDK artifact staging layout for {product}") + require_text( + ".github/workflows/release.yml", + 'tools/release/release.py ci-artifacts --product "$product" --family sdk-package', + "release workflow must derive SDK package artifact names from release metadata", + ) require_text( "src/runtimes/broker/moon.yml", 'tags: ["release", "artifact", "ci-broker-runtime"]', @@ -448,14 +447,7 @@ def validate_ci_release_artifacts() -> None: 'run(["npm", "publish", str(tarball), "--access", "public", "--provenance"])', "Node direct optional npm publish must publish CI-built tarballs directly", ) - for project_id in ( - "oliphaunt-rust", - "oliphaunt-swift", - "oliphaunt-kotlin", - "oliphaunt-react-native", - "oliphaunt-js", - "oliphaunt-wasix-rust", - ): + for project_id in artifact_targets.sdk_package_products(): moon_file = ( "src/bindings/wasix-rust/moon.yml" if project_id == "oliphaunt-wasix-rust" @@ -639,14 +631,7 @@ def validate_ci_release_artifacts() -> None: "def validate_staged_sdk_package", "release dry-runs must validate staged SDK package artifacts before publish checks", ) - for product_id in ( - "oliphaunt-rust", - "oliphaunt-swift", - "oliphaunt-kotlin", - "oliphaunt-react-native", - "oliphaunt-js", - "oliphaunt-wasix-rust", - ): + for product_id in artifact_targets.sdk_package_products(): require_text( "tools/release/release.py", f'validate_staged_sdk_package("{product_id}")', diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index 5d021ecb..740735a3 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -69,12 +69,6 @@ "oliphaunt-broker-release-assets-macos-arm64", "oliphaunt-broker-release-assets-windows-x64-msvc", "oliphaunt-extension-package-artifacts", - "oliphaunt-rust-sdk-package-artifacts", - "oliphaunt-wasix-rust-package-artifacts", - "oliphaunt-js-sdk-package-artifacts", - "oliphaunt-react-native-sdk-package-artifacts", - "oliphaunt-kotlin-sdk-package-artifacts", - "oliphaunt-swift-sdk-package-artifacts", "oliphaunt-mobile-extension-package-artifacts", ] @@ -86,6 +80,7 @@ def local_publish_artifacts() -> list[str]: *artifact_targets.ci_release_asset_artifact_names("oliphaunt-broker", "broker-helper"), *artifact_targets.ci_release_asset_artifact_names("oliphaunt-node-direct", "node-direct-addon"), *artifact_targets.ci_npm_package_artifact_names("oliphaunt-node-direct", "node-direct-addon"), + *artifact_targets.ci_sdk_package_artifact_names(), ] diff --git a/tools/release/release.py b/tools/release/release.py index a696e7e9..f1fdea9f 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -1667,13 +1667,21 @@ def command_consumer_shape(args: list[str]) -> None: def command_ci_artifacts(args: list[str]) -> None: parser = argparse.ArgumentParser(description="Emit CI artifact names derived from release target metadata.") parser.add_argument("--product", required=True) - parser.add_argument("--kind", required=True) - parser.add_argument("--family", choices=["release-assets", "npm-package"], required=True) + parser.add_argument("--kind") + parser.add_argument("--family", choices=["release-assets", "npm-package", "sdk-package"], required=True) parsed = parser.parse_args(args) if parsed.family == "release-assets": + if parsed.kind is None: + fail("ci-artifacts --family release-assets requires --kind") names = artifact_targets.ci_release_asset_artifact_names(parsed.product, parsed.kind) - else: + elif parsed.family == "npm-package": + if parsed.kind is None: + fail("ci-artifacts --family npm-package requires --kind") names = artifact_targets.ci_npm_package_artifact_names(parsed.product, parsed.kind) + else: + if parsed.kind is not None: + fail("ci-artifacts --family sdk-package does not accept --kind") + names = artifact_targets.ci_sdk_package_artifact_names(parsed.product) for name in names: print(name) From c1601e25cd6c47daf32e68aec3ca5ca28e1d9a37 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 04:41:59 +0000 Subject: [PATCH 035/137] fix: enforce split runtime tools packages --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 11 ++++-- .../examples-ci-release-validation.md | 10 +++-- tools/policy/check-crate-package.sh | 37 ++++++++++++++++++- tools/policy/check-release-policy.py | 14 +++++++ tools/release/check_consumer_shape.py | 14 ++++++- tools/release/check_release_metadata.py | 10 +++++ ...kage_liboliphaunt_wasix_cargo_artifacts.py | 22 +++++++++-- 7 files changed, 102 insertions(+), 16 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 7fd9e85d..51526384 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -39,7 +39,7 @@ review production pipelines, then normalize implementation details. - [ ] Compare SDK install paths and artifact resolution across Rust, JS, React Native, Kotlin, and Swift. - [ ] Ensure SDKs exercise the same control flows for runtime setup, extension selection, artifact validation, and tool access. - [x] Add Android split/local runtime validation so selected extensions must exist in the copied runtime tree before manifests are published. -- [ ] Align or explicitly document Deno native runtime/tools/extension resolution versus Node and Bun. +- [x] Align or explicitly document Deno native runtime/tools/extension resolution versus Node and Bun. - [x] Port stronger exact-extension artifact validation into the Android Gradle resolver. - [x] Pass mobile `sharedPreloadLibraries` through to startup arguments consistently. - [x] Add an explicit WASIX split-tools preflight path before first `pg_dump` or `psql` call. @@ -124,9 +124,9 @@ review production pipelines, then normalize implementation details. merged, and release metadata plus consumer-shape checks now enforce that resolver behavior. - Subagent SDK audit found these remaining next fixes: continue the broader SDK - artifact-resolution comparison, keep Deno native extension handling explicit, - identify any remaining feature gaps across SDKs, and add parity checks for - invariants that are still documented only in prose. + artifact-resolution comparison, identify any remaining feature gaps across + SDKs, and add parity checks for invariants that are still documented only in + prose. - Local workflow tooling is available: `act` is installed at v0.2.89, which matches the latest upstream release published on 2026-06-01, Docker is available, `act -l` parses the CI, Release, and mobile E2E workflow graph, @@ -135,6 +135,9 @@ review production pipelines, then normalize implementation details. run from a committed disposable worktree because `actions/checkout` validates committed HEAD rather than uncommitted local edits. - JS Deno direct mode now resolves packaged ICU for explicit-library installs when running inside Deno, and rejects package-managed extension requests without an explicit prepared `runtimeDirectory`. Node and Bun remain the registry-managed extension materialization paths. +- Release metadata checks now require the Deno package-managed extension + rejection guard and its unit test, so the documented Deno limitation cannot + silently drift from Node/Bun behavior. - Rust native runtime cache validation already requires both split client tools, with `runtime_validation_requires_split_tools` covering a missing `pg_dump` cache entry. - WASIX Rust now exposes `preflight_wasix_tools` plus `OliphauntServer::preflight_tools()`, and each WASIX example calls the server diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index 945af3d8..81b54dc1 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -62,7 +62,7 @@ the release/tooling surface after the runtime tool crate split. - [ ] Ensure examples exercise the same control flows the SDKs document. - [x] Validate Android split/local runtime extension files before generated manifests declare the selected extensions. -- [ ] Align Deno native runtime/tools/extension resolution with Node/Bun, or document +- [x] Align Deno native runtime/tools/extension resolution with Node/Bun, or document and test Deno as intentionally unsupported for registry-managed extensions. - [x] Port Rust/JS exact-extension archive validation rules into the Android Gradle resolver. @@ -195,7 +195,9 @@ the release/tooling surface after the runtime tool crate split. Kotlin static/unit checks, mobile extension policy checks, and release checks passed locally; Swift-specific test execution was not run because this Linux host does not have a Swift toolchain. -- A read-only SDK parity audit found these remaining issues: Deno native - resolution does not follow Node/Bun extension materialization, broader - SDK resolver/control-flow parity still needs a full pass, and any remaining +- A read-only SDK parity audit found these remaining issues: broader SDK + resolver/control-flow parity still needs a full pass, and any remaining prose-only invariants should gain policy checks. +- Deno nativeDirect is now documented and tested as intentionally unsupported + for registry-managed extension materialization without an explicit prepared + `runtimeDirectory`; release metadata checks require the guard and test. diff --git a/tools/policy/check-crate-package.sh b/tools/policy/check-crate-package.sh index 8d17c3a8..4a105799 100755 --- a/tools/policy/check-crate-package.sh +++ b/tools/policy/check-crate-package.sh @@ -31,11 +31,44 @@ while [ "$#" -gt 0 ]; do done rm -f target/package/*.crate + +package_oliphaunt_wasix() { + python3 tools/release/package_oliphaunt_wasix_sdk_crate.py --output-dir target/package >/dev/null +} + +default_packages() { + python3 - <<'PY' +import json +import subprocess + +metadata = json.loads( + subprocess.check_output( + ["cargo", "metadata", "--no-deps", "--format-version", "1"], + text=True, + ) +) +for package in sorted(metadata["packages"], key=lambda item: item["name"]): + if package.get("publish") == []: + continue + name = package["name"] + if name == "oliphaunt-wasix": + continue + print(name) +PY +} + if [ "${#packages[@]}" -eq 0 ]; then - cargo package --workspace --exclude xtask --locked --no-verify "${allow_dirty[@]}" + while IFS= read -r package; do + cargo package -p "$package" --locked --no-verify "${allow_dirty[@]}" + done < <(default_packages) + package_oliphaunt_wasix else for package in "${packages[@]}"; do - cargo package -p "$package" --locked --no-verify "${allow_dirty[@]}" + if [ "$package" = "oliphaunt-wasix" ]; then + package_oliphaunt_wasix + else + cargo package -p "$package" --locked --no-verify "${allow_dirty[@]}" + fi done fi tools/policy/check-crate-size.sh --enforce diff --git a/tools/policy/check-release-policy.py b/tools/policy/check-release-policy.py index 9734044c..ad7b200d 100644 --- a/tools/policy/check-release-policy.py +++ b/tools/policy/check-release-policy.py @@ -835,6 +835,20 @@ def check_release_workflow_policy() -> None: if snippet not in release_script: fail(f"release dry-runs and package publishes must cover registry-native checks: missing {snippet!r}") + crate_package_script = read_text("tools/policy/check-crate-package.sh") + for snippet in ( + '"cargo", "metadata"', + 'package.get("publish") == []', + "package_oliphaunt_wasix", + "tools/release/package_oliphaunt_wasix_sdk_crate.py", + 'if [ "$package" = "oliphaunt-wasix" ]; then', + ): + if snippet not in crate_package_script: + fail( + "crate package policy must package oliphaunt-wasix through the " + f"release-shaped local helper instead of crates.io resolution: missing {snippet!r}" + ) + release_head_script = read_text(".github/scripts/resolve-release-head.sh") for snippet in ( "INPUT_RELEASE_COMMIT", diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 4714dfb7..41710806 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -1729,9 +1729,19 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: and "oliphaunt/bin/postgres" in release_source and "oliphaunt/bin/pg_ctl" in release_source and "oliphaunt/bin/pg_dump" in release_source - and "oliphaunt/bin/psql" in release_source, + and "oliphaunt/bin/psql" in release_source + and "CORE_RUNTIME_ARCHIVE_FILES" in wasix_packager_source + and "FORBIDDEN_RUNTIME_ARCHIVE_TOOL_FILES" in wasix_packager_source + and "oliphaunt/bin/initdb" in wasix_packager_source + and "oliphaunt/bin/postgres" in wasix_packager_source + and "oliphaunt/bin/pg_ctl" in wasix_packager_source + and "oliphaunt/bin/pg_dump" in wasix_packager_source + and "oliphaunt/bin/psql" in wasix_packager_source, "Release validation must require postgres/initdb in the WASIX runtime archive and reject pg_ctl/pg_dump/psql there.", - "tools/release/release.py", + [ + "tools/release/release.py", + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.py", + ], severity="P0", ) require( diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 303edcd8..d4044734 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -995,6 +995,16 @@ def validate_typescript( "runtimeRelativePath", "TypeScript Deno native binding must resolve runtime resources from the selected liboliphaunt package", ) + require_text( + "src/sdks/js/src/native/deno.ts", + "Deno nativeDirect does not automatically materialize extension packages", + "TypeScript Deno native binding must fail clearly for package-managed extension materialization", + ) + require_text( + "src/sdks/js/src/__tests__/native-bindings.test.ts", + "testDenoNativeBindingRejectsPackageManagedExtensions", + "TypeScript SDK tests must cover Deno package-managed extension rejection", + ) require_text( "src/sdks/js/src/runtime/broker.ts", "restorePhysicalArchiveWithBroker", diff --git a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py index 27be7763..e7d7779c 100644 --- a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py +++ b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py @@ -32,7 +32,12 @@ "bin/pg_dump.wasix.wasm", "bin/psql.wasix.wasm", ) -BUNDLED_RUNTIME_TOOL_FILES = ( +CORE_RUNTIME_ARCHIVE_FILES = ( + "oliphaunt/bin/initdb", + "oliphaunt/bin/postgres", +) +FORBIDDEN_RUNTIME_ARCHIVE_TOOL_FILES = ( + "oliphaunt/bin/pg_ctl", "oliphaunt/bin/pg_dump", "oliphaunt/bin/psql", ) @@ -262,6 +267,15 @@ def validate_runtime_payload(root: Path) -> None: if not (root / required).is_file(): fail(f"WASIX runtime Cargo payload is missing {required}") runtime_members = tar_zstd_members(root / "oliphaunt.wasix.tar.zst") + missing_core_runtime_files = sorted( + member for member in CORE_RUNTIME_ARCHIVE_FILES if member not in runtime_members + ) + if missing_core_runtime_files: + fail( + "WASIX runtime Cargo payload must bundle postgres/initdb inside " + "oliphaunt.wasix.tar.zst; missing " + + ", ".join(missing_core_runtime_files) + ) bundled_icu = [ member for member in runtime_members @@ -275,7 +289,7 @@ def validate_runtime_payload(root: Path) -> None: bundled_tools = sorted( member for member in runtime_members - if member in BUNDLED_RUNTIME_TOOL_FILES + if member in FORBIDDEN_RUNTIME_ARCHIVE_TOOL_FILES ) if bundled_tools: fail( @@ -293,11 +307,11 @@ def validate_tools_payload(root: Path) -> None: def prune_runtime_archive_tools(archive: Path, scratch: Path) -> None: runtime_members = tar_zstd_members(archive) - if not any(member in BUNDLED_RUNTIME_TOOL_FILES for member in runtime_members): + if not any(member in FORBIDDEN_RUNTIME_ARCHIVE_TOOL_FILES for member in runtime_members): return extract_tar_zstd(archive, scratch) - for member in BUNDLED_RUNTIME_TOOL_FILES: + for member in FORBIDDEN_RUNTIME_ARCHIVE_TOOL_FILES: path = scratch / member if path.exists(): path.unlink() From 122be6cc6e37ebfc7267fa9605f6425aa1b7c2c2 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 05:09:12 +0000 Subject: [PATCH 036/137] fix: invalidate local cargo registry cache --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 9 + examples/electron-wasix/src-wasix/Cargo.lock | 92 +- examples/tauri-wasix/src-tauri/Cargo.lock | 92 +- examples/tauri/src-tauri/Cargo.lock | 10 +- examples/tools/check-examples.sh | 6 + .../tauri-sqlx-vanilla/src-tauri/Cargo.lock | 1024 +++++++---------- tools/release/check_release_metadata.py | 11 + tools/release/local_registry_publish.py | 24 +- 8 files changed, 663 insertions(+), 605 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 51526384..6174abfc 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -143,6 +143,15 @@ review production pipelines, then normalize implementation details. `OliphauntServer::preflight_tools()`, and each WASIX example calls the server preflight before its `pg_dump`/`psql` smoke. Release checks require the preflight API to load both split WASM payloads and their target AOT artifacts. +- Local Cargo registry publishing now treats explicit `--artifact-root` values + as the selected publish set and clears the local Cargo registry cache after + same-version republishes. This prevents stale unpacked crates from masking the + current split WASIX tools and extension-AOT package graph during example runs. +- `examples/tools/run-electron-driver-smoke.sh examples/electron-wasix` and + `examples/tools/run-tauri-webdriver-smoke.sh examples/tauri-wasix` passed + after the local Cargo registry was refreshed from current artifacts; both + compiled the selected `hstore`, `pg_trgm`, and `unaccent` WASIX AOT extension + crates from the local registry and exercised the `pg_dump`/`psql` path. - Mobile native-direct startup now passes packaged runtime `sharedPreloadLibraries` through to `shared_preload_libraries=...` startup args in Kotlin Android/React Native Android and Swift/React Native iOS. diff --git a/examples/electron-wasix/src-wasix/Cargo.lock b/examples/electron-wasix/src-wasix/Cargo.lock index 3eb38927..f5b1d040 100644 --- a/examples/electron-wasix/src-wasix/Cargo.lock +++ b/examples/electron-wasix/src-wasix/Cargo.lock @@ -84,9 +84,9 @@ checksum = "70033777eb8b5124a81a1889416543dddef2de240019b674c81285a2635a7e1e" [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arrayref" @@ -1589,11 +1589,23 @@ dependencies = [ name = "liboliphaunt-wasix-portable" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "67857a0fbca85a256e60c4ea9901958cad8fb28b7d1ee4033dbdbc0385ab9baa" +checksum = "74e4a84c8db15e4be7945d7b3a2ab1cb30a687b155367f32a25155891f604e77" dependencies = [ "oliphaunt-extension-hstore-wasix", + "oliphaunt-extension-hstore-wasix-aot-aarch64-apple-darwin", + "oliphaunt-extension-hstore-wasix-aot-aarch64-unknown-linux-gnu", + "oliphaunt-extension-hstore-wasix-aot-x86_64-pc-windows-msvc", + "oliphaunt-extension-hstore-wasix-aot-x86_64-unknown-linux-gnu", "oliphaunt-extension-pg-trgm-wasix", + "oliphaunt-extension-pg-trgm-wasix-aot-aarch64-apple-darwin", + "oliphaunt-extension-pg-trgm-wasix-aot-aarch64-unknown-linux-gnu", + "oliphaunt-extension-pg-trgm-wasix-aot-x86_64-pc-windows-msvc", + "oliphaunt-extension-pg-trgm-wasix-aot-x86_64-unknown-linux-gnu", "oliphaunt-extension-unaccent-wasix", + "oliphaunt-extension-unaccent-wasix-aot-aarch64-apple-darwin", + "oliphaunt-extension-unaccent-wasix-aot-aarch64-unknown-linux-gnu", + "oliphaunt-extension-unaccent-wasix-aot-x86_64-pc-windows-msvc", + "oliphaunt-extension-unaccent-wasix-aot-x86_64-unknown-linux-gnu", "serde", "serde_json", "sha2 0.10.9", @@ -1921,23 +1933,95 @@ version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "1d0b20fd2a03b45880974241e3443d9e324de637fefa4f43859efce70089812b" +[[package]] +name = "oliphaunt-extension-hstore-wasix-aot-aarch64-apple-darwin" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "004e128d02237a749af8e0219532f4af55b65de588709b0cf2bbef99e7fa6292" + +[[package]] +name = "oliphaunt-extension-hstore-wasix-aot-aarch64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "ae54c87147a7b4adba32fc6519a68937a8fb5155c4da28dcf36bd66b3e7e98ad" + +[[package]] +name = "oliphaunt-extension-hstore-wasix-aot-x86_64-pc-windows-msvc" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "98af804e5514ba341aa03e630320e135f7761b60104d4592743d68b324923fa9" + +[[package]] +name = "oliphaunt-extension-hstore-wasix-aot-x86_64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "b71adb2ca0f694aac91994c099572ae14906d333279e7bf91662431f86b8a06f" + [[package]] name = "oliphaunt-extension-pg-trgm-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "6ea075c13c8283d2eb26526c63061b116ffc515899fa59478a8a6c570539a312" +[[package]] +name = "oliphaunt-extension-pg-trgm-wasix-aot-aarch64-apple-darwin" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "0c5c91b06e0a5101433533753876dac7aee89936212967606175c9f141976a14" + +[[package]] +name = "oliphaunt-extension-pg-trgm-wasix-aot-aarch64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "c14ce6cbf988af1eb13f567b9a975f5bf566076688514133c093971f5a737aa6" + +[[package]] +name = "oliphaunt-extension-pg-trgm-wasix-aot-x86_64-pc-windows-msvc" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "d4e164a68f4047ac3c268ef71b9807d33242e06f61bf862bf60df9cb9a47b4ae" + +[[package]] +name = "oliphaunt-extension-pg-trgm-wasix-aot-x86_64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "96f7d7cd8ba652876f221b37e4f290a84d054e2c50625c243803224ce3e12b03" + [[package]] name = "oliphaunt-extension-unaccent-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "9ab06b4d61878a87b53afc7b047d09f5f2fd794528acb5e40d359e599b0fc956" +[[package]] +name = "oliphaunt-extension-unaccent-wasix-aot-aarch64-apple-darwin" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "37e5978c9d6e020c01336f58c8922ebaed2f4dfd6ae4568b5f91b5d416fc7cdb" + +[[package]] +name = "oliphaunt-extension-unaccent-wasix-aot-aarch64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "4ae9dd2c37edc58bf3dc34b88314e5f012221f74c96e9c538133ed162a12509e" + +[[package]] +name = "oliphaunt-extension-unaccent-wasix-aot-x86_64-pc-windows-msvc" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "f869c3c96abb7169927c921e92e44401f148e6de6138213ead88d1208462685d" + +[[package]] +name = "oliphaunt-extension-unaccent-wasix-aot-x86_64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "5c4389eaa071ac1e9bc837958ec1f5caf7f9d44a75a789b576a4938f3f0ec7cc" + [[package]] name = "oliphaunt-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "3c8629c9eecf4f01df1985c1690799f0bb40c5b843b41492340d2d6d5b560b01" +checksum = "4565b6dc142d9e70c4cdb7d63c7e3d2ae528e35dd7643119236bd1f712006221" dependencies = [ "anyhow", "async-trait", diff --git a/examples/tauri-wasix/src-tauri/Cargo.lock b/examples/tauri-wasix/src-tauri/Cargo.lock index 45152e28..ba8cd493 100644 --- a/examples/tauri-wasix/src-tauri/Cargo.lock +++ b/examples/tauri-wasix/src-tauri/Cargo.lock @@ -114,9 +114,9 @@ checksum = "70033777eb8b5124a81a1889416543dddef2de240019b674c81285a2635a7e1e" [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arrayref" @@ -2782,11 +2782,23 @@ dependencies = [ name = "liboliphaunt-wasix-portable" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "67857a0fbca85a256e60c4ea9901958cad8fb28b7d1ee4033dbdbc0385ab9baa" +checksum = "74e4a84c8db15e4be7945d7b3a2ab1cb30a687b155367f32a25155891f604e77" dependencies = [ "oliphaunt-extension-hstore-wasix", + "oliphaunt-extension-hstore-wasix-aot-aarch64-apple-darwin", + "oliphaunt-extension-hstore-wasix-aot-aarch64-unknown-linux-gnu", + "oliphaunt-extension-hstore-wasix-aot-x86_64-pc-windows-msvc", + "oliphaunt-extension-hstore-wasix-aot-x86_64-unknown-linux-gnu", "oliphaunt-extension-pg-trgm-wasix", + "oliphaunt-extension-pg-trgm-wasix-aot-aarch64-apple-darwin", + "oliphaunt-extension-pg-trgm-wasix-aot-aarch64-unknown-linux-gnu", + "oliphaunt-extension-pg-trgm-wasix-aot-x86_64-pc-windows-msvc", + "oliphaunt-extension-pg-trgm-wasix-aot-x86_64-unknown-linux-gnu", "oliphaunt-extension-unaccent-wasix", + "oliphaunt-extension-unaccent-wasix-aot-aarch64-apple-darwin", + "oliphaunt-extension-unaccent-wasix-aot-aarch64-unknown-linux-gnu", + "oliphaunt-extension-unaccent-wasix-aot-x86_64-pc-windows-msvc", + "oliphaunt-extension-unaccent-wasix-aot-x86_64-unknown-linux-gnu", "serde", "serde_json", "sha2 0.10.9", @@ -3394,23 +3406,95 @@ version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "1d0b20fd2a03b45880974241e3443d9e324de637fefa4f43859efce70089812b" +[[package]] +name = "oliphaunt-extension-hstore-wasix-aot-aarch64-apple-darwin" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "004e128d02237a749af8e0219532f4af55b65de588709b0cf2bbef99e7fa6292" + +[[package]] +name = "oliphaunt-extension-hstore-wasix-aot-aarch64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "ae54c87147a7b4adba32fc6519a68937a8fb5155c4da28dcf36bd66b3e7e98ad" + +[[package]] +name = "oliphaunt-extension-hstore-wasix-aot-x86_64-pc-windows-msvc" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "98af804e5514ba341aa03e630320e135f7761b60104d4592743d68b324923fa9" + +[[package]] +name = "oliphaunt-extension-hstore-wasix-aot-x86_64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "b71adb2ca0f694aac91994c099572ae14906d333279e7bf91662431f86b8a06f" + [[package]] name = "oliphaunt-extension-pg-trgm-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "6ea075c13c8283d2eb26526c63061b116ffc515899fa59478a8a6c570539a312" +[[package]] +name = "oliphaunt-extension-pg-trgm-wasix-aot-aarch64-apple-darwin" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "0c5c91b06e0a5101433533753876dac7aee89936212967606175c9f141976a14" + +[[package]] +name = "oliphaunt-extension-pg-trgm-wasix-aot-aarch64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "c14ce6cbf988af1eb13f567b9a975f5bf566076688514133c093971f5a737aa6" + +[[package]] +name = "oliphaunt-extension-pg-trgm-wasix-aot-x86_64-pc-windows-msvc" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "d4e164a68f4047ac3c268ef71b9807d33242e06f61bf862bf60df9cb9a47b4ae" + +[[package]] +name = "oliphaunt-extension-pg-trgm-wasix-aot-x86_64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "96f7d7cd8ba652876f221b37e4f290a84d054e2c50625c243803224ce3e12b03" + [[package]] name = "oliphaunt-extension-unaccent-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" checksum = "9ab06b4d61878a87b53afc7b047d09f5f2fd794528acb5e40d359e599b0fc956" +[[package]] +name = "oliphaunt-extension-unaccent-wasix-aot-aarch64-apple-darwin" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "37e5978c9d6e020c01336f58c8922ebaed2f4dfd6ae4568b5f91b5d416fc7cdb" + +[[package]] +name = "oliphaunt-extension-unaccent-wasix-aot-aarch64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "4ae9dd2c37edc58bf3dc34b88314e5f012221f74c96e9c538133ed162a12509e" + +[[package]] +name = "oliphaunt-extension-unaccent-wasix-aot-x86_64-pc-windows-msvc" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "f869c3c96abb7169927c921e92e44401f148e6de6138213ead88d1208462685d" + +[[package]] +name = "oliphaunt-extension-unaccent-wasix-aot-x86_64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "5c4389eaa071ac1e9bc837958ec1f5caf7f9d44a75a789b576a4938f3f0ec7cc" + [[package]] name = "oliphaunt-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "3c8629c9eecf4f01df1985c1690799f0bb40c5b843b41492340d2d6d5b560b01" +checksum = "4565b6dc142d9e70c4cdb7d63c7e3d2ae528e35dd7643119236bd1f712006221" dependencies = [ "anyhow", "async-trait", diff --git a/examples/tauri/src-tauri/Cargo.lock b/examples/tauri/src-tauri/Cargo.lock index 70c64ac6..8579c5d8 100644 --- a/examples/tauri/src-tauri/Cargo.lock +++ b/examples/tauri/src-tauri/Cargo.lock @@ -43,9 +43,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arbitrary" @@ -2203,7 +2203,7 @@ dependencies = [ name = "oliphaunt-extension-hstore-linux-x64-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "b60b0280f8b9b38ef0f02a30b4bccc4a09869e8f4b8476277fc274a376ad0632" +checksum = "4a9b6d73245fb432a8aaa74f20f5b6bd2a1adc7ab820ea289f7002d84b0d98b0" dependencies = [ "sha2", ] @@ -2212,7 +2212,7 @@ dependencies = [ name = "oliphaunt-extension-pg-trgm-linux-x64-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "1028e6777a424b90fa3cfb0139b3e0737db6059360df52976d06e086e80afae7" +checksum = "6334691d2aeb32752c4f2a586bac0836d6081d821421547e1d4a513e659d932b" dependencies = [ "sha2", ] @@ -2221,7 +2221,7 @@ dependencies = [ name = "oliphaunt-extension-unaccent-linux-x64-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "cb599a9723f73ccf66e7e33a0c395e1ef449578c5d7f5338d18c3d62fec40bda" +checksum = "58ba1f77413bf35eb5f90315fe17ec2b10a208a3090eb511d2f17d650d820b14" dependencies = [ "sha2", ] diff --git a/examples/tools/check-examples.sh b/examples/tools/check-examples.sh index 08a0e78b..0414be3f 100755 --- a/examples/tools/check-examples.sh +++ b/examples/tools/check-examples.sh @@ -112,12 +112,18 @@ require_text "examples/tauri-wasix/src-tauri/Cargo.toml" '"tools"' require_text "examples/tauri-wasix/src-tauri/Cargo.toml" 'oliphaunt-wasix-tools' require_text "examples/tauri-wasix/src-tauri/Cargo.toml" 'liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu' require_text "examples/tauri-wasix/src-tauri/Cargo.toml" 'oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu' +require_text "examples/tauri-wasix/src-tauri/Cargo.lock" 'oliphaunt-extension-hstore-wasix-aot-x86_64-unknown-linux-gnu' +require_text "examples/tauri-wasix/src-tauri/Cargo.lock" 'oliphaunt-extension-pg-trgm-wasix-aot-x86_64-unknown-linux-gnu' +require_text "examples/tauri-wasix/src-tauri/Cargo.lock" 'oliphaunt-extension-unaccent-wasix-aot-x86_64-unknown-linux-gnu' require_text "examples/tauri-wasix/src-tauri/src/lib.rs" 'preflight_tools\(\)' require_text "examples/electron-wasix/src-wasix/Cargo.toml" 'registry = "oliphaunt-local"' require_text "examples/electron-wasix/src-wasix/Cargo.toml" '"tools"' require_text "examples/electron-wasix/src-wasix/Cargo.toml" 'oliphaunt-wasix-tools' require_text "examples/electron-wasix/src-wasix/Cargo.toml" 'liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu' require_text "examples/electron-wasix/src-wasix/Cargo.toml" 'oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu' +require_text "examples/electron-wasix/src-wasix/Cargo.lock" 'oliphaunt-extension-hstore-wasix-aot-x86_64-unknown-linux-gnu' +require_text "examples/electron-wasix/src-wasix/Cargo.lock" 'oliphaunt-extension-pg-trgm-wasix-aot-x86_64-unknown-linux-gnu' +require_text "examples/electron-wasix/src-wasix/Cargo.lock" 'oliphaunt-extension-unaccent-wasix-aot-x86_64-unknown-linux-gnu' require_text "examples/electron-wasix/src-wasix/src/main.rs" 'preflight_tools\(\)' require_text "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml" 'registry = "oliphaunt-local"' require_text "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml" '"tools"' diff --git a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock index 811a5a89..1eecbbf9 100644 --- a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock +++ b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock @@ -34,9 +34,9 @@ checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" [[package]] name = "alloc-stdlib" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" dependencies = [ "alloc-no-stdlib", ] @@ -114,9 +114,9 @@ checksum = "70033777eb8b5124a81a1889416543dddef2de240019b674c81285a2635a7e1e" [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arrayref" @@ -126,9 +126,9 @@ checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" [[package]] name = "async-broadcast" @@ -223,7 +223,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -258,7 +258,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -301,9 +301,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "backtrace" @@ -358,7 +358,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -368,8 +368,8 @@ dependencies = [ "quote", "regex", "rustc-hash", - "shlex", - "syn 2.0.117", + "shlex 1.3.0", + "syn 2.0.118", ] [[package]] @@ -395,9 +395,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" dependencies = [ "serde_core", ] @@ -427,9 +427,9 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", ] @@ -458,9 +458,9 @@ dependencies = [ [[package]] name = "brotli" -version = "8.0.2" +version = "8.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -469,29 +469,38 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "5.0.0" +version = "5.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bstr" -version = "1.12.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" dependencies = [ "memchr", - "serde", + "serde_core", ] [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bus" @@ -524,7 +533,7 @@ checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -541,18 +550,18 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" dependencies = [ "serde", ] [[package]] name = "bytesize" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" +checksum = "49e78e506b9d7633710dab98996f22f95f3d0f488e8f1aa162830556ed9fc14d" dependencies = [ "serde_core", ] @@ -563,7 +572,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cairo-sys-rs", "glib", "libc", @@ -584,9 +593,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +checksum = "b4ce8d3bd5823c7504d3f579f13e7b2f3da252fcb938c594d5680ee508bf846f" dependencies = [ "serde_core", ] @@ -626,14 +635,14 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.61" +version = "1.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex", + "shlex 2.0.1", ] [[package]] @@ -680,9 +689,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures 0.3.0", @@ -691,9 +700,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "num-traits", @@ -770,7 +779,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -883,7 +892,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "core-foundation", "core-graphics-types", "foreign-types", @@ -896,16 +905,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "core-foundation", "libc", ] [[package]] name = "corosensei" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c54787b605c7df106ceccf798df23da4f2e09918defad66705d1cedf3bb914f" +checksum = "6886a0c0f263965933c438626e7179139a62b978a33aa18281cbf0cd5a975f34" dependencies = [ "autocfg", "cfg-if", @@ -1026,9 +1035,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ "hybrid-array", ] @@ -1042,7 +1051,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.13.1", + "phf", "smallvec", ] @@ -1053,7 +1062,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1113,7 +1122,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1126,7 +1135,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1139,7 +1148,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1150,7 +1159,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1161,7 +1170,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1172,14 +1181,14 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core 0.23.0", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "dashmap" -version = "6.1.0" +version = "6.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" dependencies = [ "cfg-if", "crossbeam-utils", @@ -1215,14 +1224,14 @@ version = "0.3.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" dependencies = [ - "defmt 1.0.1", + "defmt 1.1.0", ] [[package]] name = "defmt" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78" +checksum = "a6e524506490a1953d237cb87b1cfc1e46f88c18f10a22dfe0f507dc6bfc7f7f" dependencies = [ "bitflags 1.3.2", "defmt-macros", @@ -1230,15 +1239,15 @@ dependencies = [ [[package]] name = "defmt-macros" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d4fc12a85bcf441cfe44344c4b72d58493178ce635338a3f3b78943aceb258e" +checksum = "f0a27770e9c8f719a79d8b638281f4d828f77d8fd61e0bd94451b9b85e576a0b" dependencies = [ "defmt-parser", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1256,7 +1265,6 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ - "powerfmt", "serde_core", ] @@ -1278,7 +1286,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1288,7 +1296,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1310,7 +1318,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.117", + "syn 2.0.118", "unicode-xid", ] @@ -1331,9 +1339,9 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.0", + "block-buffer 0.12.1", "const-oid", - "crypto-common 0.2.1", + "crypto-common 0.2.2", ] [[package]] @@ -1372,7 +1380,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2", "libc", "objc2", @@ -1380,13 +1388,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1409,7 +1417,7 @@ checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1495,9 +1503,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" dependencies = [ "serde", ] @@ -1551,7 +1559,7 @@ checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1572,28 +1580,28 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "enumset" -version = "1.1.10" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25b07a8dfbbbfc0064c0a6bdf9edcf966de6b1c33ce344bdeca3b41615452634" +checksum = "839c4174b41e75c8f7306110b2c51996a293b8d1d850edd529011841d9fede7d" dependencies = [ "enumset_derive", ] [[package]] name = "enumset_derive" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce" +checksum = "4bd536557b58c682b217b8fb199afdff47cd3eff260623f19e77074eb073d63a" dependencies = [ "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1688,13 +1696,12 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.27" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" dependencies = [ "cfg-if", "libc", - "libredox", ] [[package]] @@ -1755,7 +1762,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1859,7 +1866,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2027,17 +2034,15 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi 6.0.0", "rand_core 0.10.1", - "wasip2", - "wasip3", "wasm-bindgen", ] @@ -2097,7 +2102,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "futures-channel", "futures-core", "futures-executor", @@ -2125,7 +2130,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2217,7 +2222,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2271,9 +2276,9 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ "foldhash 0.2.0", ] @@ -2369,9 +2374,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -2408,18 +2413,18 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hybrid-array" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" dependencies = [ "typenum", ] [[package]] name = "hyper" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -2609,9 +2614,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.25" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +checksum = "b915661dd01db3f05050265b2477bcc6527b3792388e2749b41623cc592be67d" dependencies = [ "crossbeam-deque", "globset", @@ -2641,7 +2646,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -2657,15 +2662,16 @@ dependencies = [ [[package]] name = "insta" -version = "1.47.2" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e" +checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82" dependencies = [ "console", "once_cell", "regex", "serde", "similar", + "strip-ansi-escapes", "tempfile", ] @@ -2684,16 +2690,6 @@ dependencies = [ "ipnet", ] -[[package]] -name = "iri-string" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is-docker" version = "0.2.0" @@ -2807,7 +2803,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2822,13 +2818,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.97" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -2860,16 +2855,16 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "serde", "unicode-segmentation", ] [[package]] name = "leb128" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cc46bac87ef8093eed6f272babb833b6443374399985ac8ed28471ee0918545" +checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52" [[package]] name = "leb128fmt" @@ -2945,16 +2940,67 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "liboliphaunt-wasix-aot-aarch64-apple-darwin" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "f7c773796df578853baca2f0dcfb610dc78c103f17fbd260f053c5945a5d0ba1" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "9611d8528c54f4a6981217d6acaddaba0b26cbc20841b8698cb14332fd1b8a64" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "43067bd9d8aa2499d867443a39dcba33195f83c525193a730b6e9b7d66570f88" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "8856bae97b2d60f323f5847db4223fe768a0ee34ebb785b795b11482bd1a9b86" +dependencies = [ + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "liboliphaunt-wasix-portable" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "74e4a84c8db15e4be7945d7b3a2ab1cb30a687b155367f32a25155891f604e77" +dependencies = [ + "serde", + "serde_json", + "sha2 0.10.9", +] + [[package]] name = "libredox" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "libc", "plain", - "redox_syscall 0.7.5", + "redox_syscall 0.8.1", ] [[package]] @@ -3019,15 +3065,15 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lz4_flex" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db9a0d582c2874f68138a16ce1867e0ffde6c0bb0a0df85e1f36d04146db488a" +checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e" dependencies = [ "twox-hash", ] @@ -3087,9 +3133,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "memmap2" @@ -3102,9 +3148,9 @@ dependencies = [ [[package]] name = "memmap2" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" dependencies = [ "libc", ] @@ -3142,9 +3188,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "log", @@ -3164,15 +3210,15 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbeff6bd154a309b2ada5639b2661ca6ae4599b34e8487dc276d2cd637da2d76" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "itoa", ] [[package]] name = "muda" -version = "0.19.1" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae8844f63b5b118e334e205585b8c5c17b984121dbdb179d44aeb087ffad3cb" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" dependencies = [ "crossbeam-channel", "dpi", @@ -3206,7 +3252,7 @@ checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3215,7 +3261,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "jni-sys 0.3.1", "log", "ndk-sys", @@ -3261,9 +3307,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-traits" @@ -3303,7 +3349,7 @@ dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3322,7 +3368,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2", "objc2", "objc2-core-foundation", @@ -3335,7 +3381,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "objc2", "objc2-foundation", ] @@ -3356,7 +3402,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "dispatch2", "objc2", ] @@ -3367,7 +3413,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "dispatch2", "objc2", "objc2-core-foundation", @@ -3400,7 +3446,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -3427,7 +3473,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2", "objc2", "objc2-core-foundation", @@ -3439,7 +3485,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "objc2", "objc2-core-foundation", ] @@ -3450,7 +3496,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -3462,7 +3508,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2", "objc2", "objc2-cloud-kit", @@ -3493,7 +3539,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2", "objc2", "objc2-app-kit", @@ -3518,7 +3564,7 @@ checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" dependencies = [ "crc32fast", "flate2", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "indexmap 2.14.0", "memchr", "ruzstd", @@ -3528,7 +3574,7 @@ dependencies = [ name = "oliphaunt-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "3c8629c9eecf4f01df1985c1690799f0bb40c5b843b41492340d2d6d5b560b01" +checksum = "4565b6dc142d9e70c4cdb7d63c7e3d2ae528e35dd7643119236bd1f712006221" dependencies = [ "anyhow", "async-trait", @@ -3563,57 +3609,6 @@ dependencies = [ "zstd", ] -[[package]] -name = "liboliphaunt-wasix-aot-aarch64-apple-darwin" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "f7c773796df578853baca2f0dcfb610dc78c103f17fbd260f053c5945a5d0ba1" -dependencies = [ - "serde_json", - "sha2 0.10.9", -] - -[[package]] -name = "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "9611d8528c54f4a6981217d6acaddaba0b26cbc20841b8698cb14332fd1b8a64" -dependencies = [ - "serde_json", - "sha2 0.10.9", -] - -[[package]] -name = "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "43067bd9d8aa2499d867443a39dcba33195f83c525193a730b6e9b7d66570f88" -dependencies = [ - "serde_json", - "sha2 0.10.9", -] - -[[package]] -name = "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "8856bae97b2d60f323f5847db4223fe768a0ee34ebb785b795b11482bd1a9b86" -dependencies = [ - "serde_json", - "sha2 0.10.9", -] - -[[package]] -name = "liboliphaunt-wasix-portable" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "67857a0fbca85a256e60c4ea9901958cad8fb28b7d1ee4033dbdbc0385ab9baa" -dependencies = [ - "serde", - "serde_json", - "sha2 0.10.9", -] - [[package]] name = "oliphaunt-wasix-tools" version = "0.1.0" @@ -3677,9 +3672,9 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "open" -version = "5.3.4" +version = "5.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3bab717c29a857abf75fcef718d441ec7cb2725f937343c734740a985d37fd" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" dependencies = [ "dunce", "is-wsl", @@ -3793,24 +3788,14 @@ dependencies = [ "serde", ] -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_macros 0.11.3", - "phf_shared 0.11.3", -] - [[package]] name = "phf" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ - "phf_macros 0.13.1", - "phf_shared 0.13.1", + "phf_macros", + "phf_shared", "serde", ] @@ -3820,18 +3805,8 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared 0.11.3", - "rand 0.8.6", + "phf_generator", + "phf_shared", ] [[package]] @@ -3841,20 +3816,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" dependencies = [ "fastrand", - "phf_shared 0.13.1", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", - "syn 2.0.117", + "phf_shared", ] [[package]] @@ -3863,20 +3825,11 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", + "phf_generator", + "phf_shared", "proc-macro2", "quote", - "syn 2.0.117", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", + "syn 2.0.118", ] [[package]] @@ -3890,22 +3843,22 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.12" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf0d9e68100b3a7989b4901972f265cd542e560a3a8a724e1e20322f4d06ce9" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.12" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a990e22f43e84855daf260dded30524ef4a9021cc7541c26540500a50b624389" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3975,7 +3928,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "crc32fast", "fdeflate", "flate2", @@ -4033,7 +3986,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4062,7 +4015,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.11+spec-1.1.0", + "toml_edit 0.25.12+spec-1.1.0", ] [[package]] @@ -4108,7 +4061,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4137,7 +4090,7 @@ checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4153,18 +4106,18 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.39.3" +version = "0.39.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721da970c312655cde9b4ffe0547f20a8494866a4af5ff51f18b7c633d0c870b" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -4218,7 +4171,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20", - "getrandom 0.4.2", + "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -4304,16 +4257,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] name = "redox_syscall" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" +checksum = "5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] @@ -4344,14 +4297,14 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -4372,9 +4325,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "region" @@ -4405,9 +4358,9 @@ checksum = "51743d3e274e2b18df81c4dc6caf8a5b8e15dbe799e0dca05c7617380094e884" [[package]] name = "reqwest" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64 0.22.1", "bytes", @@ -4459,7 +4412,7 @@ checksum = "73389e0c99e664f919275ab5b5b0471391fe9a8de61e1dff9b1eaf56a90f16e3" dependencies = [ "bytecheck", "bytes", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "indexmap 2.14.0", "munge", "ptr_meta", @@ -4478,7 +4431,7 @@ checksum = "5d2ed0b54125315fb36bd021e82d314d1c126548f871634b483f46b31d13cac6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4508,7 +4461,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys", @@ -4517,9 +4470,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "once_cell", "ring", @@ -4570,9 +4523,9 @@ dependencies = [ [[package]] name = "ruzstd" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ff0cc5e135c8870a775d3320910cd9b564ec036b4dc0b8741629020be63f01" +checksum = "a7c1c839d570d835527c9a5e4db7cb2198683a988cb9d7293fc8674e6bd58fc8" dependencies = [ "twox-hash", ] @@ -4653,7 +4606,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4665,7 +4618,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4680,12 +4633,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cssparser", "derive_more", "log", "new_debug_unreachable", - "phf 0.13.1", + "phf", "phf_codegen", "precomputed-hash", "rustc-hash", @@ -4759,7 +4712,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4770,14 +4723,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -4794,7 +4747,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4817,11 +4770,12 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.19.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05839ce67618e14a09b286535c0d9c94e85ef25469b0e13cb4f844e5593eb19" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" dependencies = [ "base64 0.22.1", + "bs58", "chrono", "hex", "indexmap 1.9.3", @@ -4836,14 +4790,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.19.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf2ebbe86054f9b45bc3881e865683ccfaccce97b9b4cb53f3039d67f355a334" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" dependencies = [ "darling 0.23.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4878,7 +4832,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4928,6 +4882,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -4970,9 +4930,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" dependencies = [ "serde", ] @@ -4993,9 +4953,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -5106,7 +5066,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5127,7 +5087,7 @@ dependencies = [ "sha2 0.10.9", "sqlx-core", "sqlx-postgres", - "syn 2.0.117", + "syn 2.0.118", "tokio", "url", ] @@ -5140,7 +5100,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.11.1", + "bitflags 2.13.0", "byteorder", "crc", "dotenvy", @@ -5183,7 +5143,7 @@ checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" dependencies = [ "new_debug_unreachable", "parking_lot", - "phf_shared 0.13.1", + "phf_shared", "precomputed-hash", ] @@ -5193,8 +5153,8 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", + "phf_generator", + "phf_shared", "proc-macro2", "quote", ] @@ -5210,6 +5170,15 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.11.1" @@ -5235,21 +5204,21 @@ dependencies = [ [[package]] name = "symbolic-common" -version = "13.1.1" +version = "13.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c30da69ccd7ab2780ce5309791f3cd2ef9716262c07a0a29096226d4235a979" +checksum = "b2dd5edfa38a9ff82e3f394bed19a5f953e2b40d3acf51535a45bb3653c3aabd" dependencies = [ "debugid", - "memmap2 0.9.10", + "memmap2 0.9.11", "stable_deref_trait", "uuid", ] [[package]] name = "symbolic-demangle" -version = "13.1.1" +version = "13.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1245acf80236b4a0d99e9216532102a1670950e79c70b980b607c2040966e83d" +checksum = "7bfea8acd6e7a1a51cf030a4ea77472b37af8c33b428f18ac62ceaee3645310d" dependencies = [ "cpp_demangle", "msvc-demangler", @@ -5270,9 +5239,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -5296,7 +5265,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5314,11 +5283,11 @@ dependencies = [ [[package]] name = "tao" -version = "0.35.2" +version = "0.35.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33f7f9e486ade65fcf1e45c440f9236c904f5c1002cdc7fc6ae582777345ce4" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2", "core-foundation", "core-graphics", @@ -5360,14 +5329,14 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "tar" -version = "0.4.45" +version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" dependencies = [ "filetime", "libc", @@ -5388,9 +5357,9 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "tauri" -version = "2.11.0" +version = "2.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d059f2527558d9dba6f186dec4772610e1aecfd3f94002397613e7e648752b66" +checksum = "c2616f96cb644bf2c5c456d9de4d5d5100e592d7424c74d8b55c5cb96e359e93" dependencies = [ "anyhow", "bytes", @@ -5439,9 +5408,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.6.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be9aa8c59a894f76c29a002501c589de5eb4987a5913d62a6e0a47f320901988" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" dependencies = [ "anyhow", "cargo_toml", @@ -5460,9 +5429,9 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "2.6.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e4e8230d565106aa19dfbaa01a7ed01abf78047fe0577a83377224bd1bf20e" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" dependencies = [ "base64 0.22.1", "brotli", @@ -5476,7 +5445,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.9", - "syn 2.0.117", + "syn 2.0.118", "tauri-utils", "thiserror 2.0.18", "time", @@ -5487,23 +5456,23 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.6.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc8de2cddbbc33dbdf4c84f170121886595efdbcc9cb4b3d76342b79d082cedc" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "tauri-codegen", "tauri-utils", ] [[package]] name = "tauri-plugin" -version = "2.6.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8d5f58bfd0cdcfdbc0a68dc08b354eea2afc551b421de91b07b69e0dd769d57" +checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" dependencies = [ "anyhow", "glob", @@ -5539,9 +5508,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.11.0" +version = "2.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e42bbcb76237351fbaa02f08d808c537dc12eb5a6eabbf3e517b50056334d95" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" dependencies = [ "cookie", "dpi", @@ -5564,9 +5533,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.11.0" +version = "2.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cadb13dad0c681e1e0a2c49ae488f0e2906ded3d57e7a0017f4aaf46e387117" +checksum = "fe41e015bf8fc4d6477ff4926a0ef769dc64ff34c7b0038b6f7cacae892acb5c" dependencies = [ "gtk", "http", @@ -5609,9 +5578,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.9.0" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55f61d2bf7188fbcf2b0ed095b67a6bc498f713c939314bb19eb700118a573b7" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" dependencies = [ "anyhow", "brotli", @@ -5625,7 +5594,7 @@ dependencies = [ "json-patch", "log", "memchr", - "phf 0.11.3", + "phf", "plist", "proc-macro2", "quote", @@ -5663,7 +5632,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2", @@ -5724,7 +5693,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5735,17 +5704,16 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "time" -version = "0.3.47" +version = "0.3.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde_core", @@ -5755,15 +5723,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" dependencies = [ "num-conv", "time-core", @@ -5796,9 +5764,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.2" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -5817,7 +5785,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5884,7 +5852,7 @@ dependencies = [ "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 1.0.2", + "winnow 1.0.3", ] [[package]] @@ -5940,14 +5908,14 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.11+spec-1.1.0" +version = "0.25.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" dependencies = [ "indexmap 2.14.0", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.2", + "winnow 1.0.3", ] [[package]] @@ -5956,7 +5924,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.2", + "winnow 1.0.3", ] [[package]] @@ -5982,20 +5950,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -6030,7 +5998,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -6044,9 +6012,9 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.23.1" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773" +checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" dependencies = [ "crossbeam-channel", "dirs", @@ -6084,9 +6052,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "uds_windows" @@ -6175,9 +6143,9 @@ checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-xid" @@ -6254,11 +6222,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.1" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ - "getrandom 0.4.2", + "getrandom 0.4.3", "js-sys", "serde_core", "wasm-bindgen", @@ -6289,7 +6257,7 @@ dependencies = [ "derive_more", "dunce", "futures", - "getrandom 0.4.2", + "getrandom 0.4.3", "indexmap 2.14.0", "pin-project-lite", "replace_with", @@ -6375,6 +6343,15 @@ dependencies = [ "libc", ] +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + [[package]] name = "wai-bindgen-gen-core" version = "0.2.3" @@ -6474,20 +6451,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] @@ -6498,9 +6466,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.120" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -6511,9 +6479,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.70" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ "js-sys", "wasm-bindgen", @@ -6521,9 +6489,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.120" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6531,36 +6499,26 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.120" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.120" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser 0.244.0", -] - [[package]] name = "wasm-encoder" version = "0.250.0" @@ -6568,19 +6526,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2271adb766023046af314460f1fae02cc34ea16d736d93404d3b65be44270923" dependencies = [ "leb128fmt", - "wasmparser 0.250.0", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap 2.14.0", - "wasm-encoder 0.244.0", - "wasmparser 0.244.0", + "wasmparser", ] [[package]] @@ -6648,7 +6594,7 @@ dependencies = [ "leb128", "libc", "macho-unwind-info", - "memmap2 0.9.10", + "memmap2 0.9.11", "more-asserts", "object 0.39.1", "rangemap", @@ -6663,7 +6609,7 @@ dependencies = [ "thiserror 2.0.18", "wasmer-types", "wasmer-vm", - "wasmparser 0.250.0", + "wasmparser", "which", "windows-sys 0.61.2", ] @@ -6700,7 +6646,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -6769,7 +6715,7 @@ dependencies = [ "crc32fast", "enum-iterator", "enumset", - "getrandom 0.4.2", + "getrandom 0.4.3", "hex", "indexmap 2.14.0", "itertools 0.14.0", @@ -6779,7 +6725,7 @@ dependencies = [ "sha2 0.11.0", "target-lexicon 0.13.5", "thiserror 2.0.18", - "wasmparser 0.250.0", + "wasmparser", ] [[package]] @@ -6838,7 +6784,7 @@ dependencies = [ "fs_extra", "futures", "getrandom 0.3.4", - "getrandom 0.4.2", + "getrandom 0.4.3", "heapless", "hex", "http", @@ -6877,14 +6823,14 @@ dependencies = [ "virtual-net", "waker-fn", "walkdir", - "wasm-encoder 0.250.0", + "wasm-encoder", "wasmer", "wasmer-config", "wasmer-journal", "wasmer-package", "wasmer-types", "wasmer-wasix-types", - "wasmparser 0.250.0", + "wasmparser", "webc", "weezl", "windows-sys 0.61.2", @@ -6899,7 +6845,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69e823d48c54f97a6663844c2fd52dad4894da08fc930bcb930b93799b5d9606" dependencies = [ "anyhow", - "bitflags 2.11.1", + "bitflags 2.13.0", "byteorder", "cfg-if", "num_enum", @@ -6916,33 +6862,21 @@ dependencies = [ "wasmer-types", ] -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.1", - "hashbrown 0.15.5", - "indexmap 2.14.0", - "semver", -] - [[package]] name = "wasmparser" version = "0.250.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071d99cdfb8111603ed05500506c3298a940b58d609dd0259d3981785dd33556" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "indexmap 2.14.0", ] [[package]] name = "web-sys" -version = "0.3.97" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -6950,11 +6884,11 @@ dependencies = [ [[package]] name = "web_atoms" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" dependencies = [ - "phf 0.13.1", + "phf", "phf_codegen", "string_cache", "string_cache_codegen", @@ -7038,14 +6972,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.7", + "webpki-roots 1.0.8", ] [[package]] name = "webpki-roots" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" dependencies = [ "rustls-pki-types", ] @@ -7072,7 +7006,7 @@ checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -7094,9 +7028,9 @@ checksum = "d4ca08e5ef825b65b056d9efbd95c8750683f0a6d0466d02e96dc2e4e360f3d2" [[package]] name = "which" -version = "8.0.2" +version = "8.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459" +checksum = "48d7cd18d4acb58fb3cdfe9ea54e6cd96a4e7d4cc45c56338b236e82dad47248" dependencies = [ "libc", ] @@ -7224,7 +7158,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -7235,7 +7169,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -7554,9 +7488,9 @@ checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" [[package]] name = "winnow" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] @@ -7571,100 +7505,12 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck 0.5.0", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap 2.14.0", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.1", - "indexmap 2.14.0", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder 0.244.0", - "wasm-metadata", - "wasmparser 0.244.0", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap 2.14.0", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser 0.244.0", -] - [[package]] name = "writeable" version = "0.6.3" @@ -7754,9 +7600,9 @@ checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -7771,15 +7617,15 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] [[package]] name = "zbus" -version = "5.15.0" +version = "5.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3bcbf15c8708d7fc1be0c993622e0a5cbd5e8b52bfa40afa4c3e0cd8d724ac1" +checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" dependencies = [ "async-broadcast", "async-executor", @@ -7804,7 +7650,7 @@ dependencies = [ "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow 1.0.2", + "winnow 1.0.3", "zbus_macros", "zbus_names", "zvariant", @@ -7812,14 +7658,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.15.0" +version = "5.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff" +checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "zbus_names", "zvariant", "zvariant_utils", @@ -7832,35 +7678,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" dependencies = [ "serde", - "winnow 1.0.2", + "winnow 1.0.3", "zvariant", ] [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "zerofrom" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] @@ -7873,15 +7719,15 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" @@ -7913,7 +7759,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -7952,40 +7798,40 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.11.0" +version = "5.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c1567a6ec68df868cbbfde844cfc6d81649fe5109a62b116b19fabd53e618ee" +checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" dependencies = [ "endi", "enumflags2", "serde", - "winnow 1.0.2", + "winnow 1.0.3", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "5.11.0" +version = "5.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d5b780599bbde114e39d9a0799577fad1ced5105d38515745f7b3099d8ceda" +checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "3.3.1" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d464f5733ffa07a3164d656f18533caace9d0638596721355d73256a410d691" +checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.117", - "winnow 1.0.2", + "syn 2.0.118", + "winnow 1.0.3", ] diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index d4044734..25ed29f4 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -293,6 +293,16 @@ def validate_release_setup_docs() -> None: fail("release setup guide must contain exactly one Sonatype token setup reference") +def validate_local_registry_publisher() -> None: + publisher = read_text("tools/release/local_registry_publish.py") + if "explicit_roots = list(artifact_roots)" not in publisher or "roots = explicit_roots or [" not in publisher: + fail("local registry publisher must treat explicit --artifact-root values as the selected artifact set") + if "roots.extend(extra_roots)" in publisher: + fail("local registry publisher must not append explicit artifact roots to stale default build roots") + if "def clear_local_cargo_home_cache" not in publisher or '"cache", "src", "index"' not in publisher: + fail("local registry publisher must clear Cargo's local registry cache after same-version Cargo republishes") + + def validate_rust() -> None: require_text( "src/sdks/rust/tools/check-sdk.sh", @@ -1185,6 +1195,7 @@ def main() -> int: validate_graph_files(graph) validate_exact_extension_registry_shape(graph) validate_release_setup_docs() + validate_local_registry_publisher() versions = { product: product_metadata.read_current_version(product) diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index 740735a3..1b2d7071 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -131,8 +131,9 @@ def add_skip(self, message: str) -> None: self.skipped.append(message) -def discover_roots(extra_roots: Iterable[Path]) -> list[Path]: - roots = [ +def discover_roots(artifact_roots: Iterable[Path]) -> list[Path]: + explicit_roots = list(artifact_roots) + roots = explicit_roots or [ DEFAULT_ARTIFACT_ROOT, ROOT / "target" / "sdk-artifacts", ROOT / "target" / "package" / "tmp-crate", @@ -143,7 +144,6 @@ def discover_roots(extra_roots: Iterable[Path]) -> list[Path]: ROOT / "target" / "oliphaunt-wasix" / "release-assets", ROOT / "target" / "extension-artifacts", ] - roots.extend(extra_roots) seen: set[Path] = set() result: list[Path] = [] for root in roots: @@ -2278,6 +2278,21 @@ def cargo_index_entry(crate_path: Path, package: dict[str, Any], local_package_n } +def clear_local_cargo_home_cache(registry_root: Path) -> list[Path]: + cargo_home_registry = registry_root / "cargo-home" / "registry" + removed: list[Path] = [] + for name in ["cache", "src", "index"]: + path = cargo_home_registry / name + if path.exists(): + shutil.rmtree(path) + removed.append(path) + package_cache = cargo_home_registry / ".package-cache" + if package_cache.exists(): + package_cache.unlink() + removed.append(package_cache) + return removed + + def cargo_crate_priority(path: Path, registry_root: Path) -> tuple[int, str]: resolved = path.resolve() priority = 20 @@ -2387,6 +2402,9 @@ def publish_cargo(roots: list[Path], registry_root: Path, dry_run: bool, strict: ), encoding="utf-8", ) + removed_cache_paths = clear_local_cargo_home_cache(registry_root) + if removed_cache_paths: + result.staged.extend(f"cleared {rel(path)}" for path in removed_cache_paths) result.staged.extend([rel(index_dir), rel(config_snippet)]) return result From 123397eb4a49609c23b5f7734bbc5ce1a9295d8a Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 05:20:24 +0000 Subject: [PATCH 037/137] fix: enforce native tools crate split --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 5 ++++ .../examples-ci-release-validation.md | 5 ++++ tools/release/check_consumer_shape.py | 30 +++++++++++++++++++ .../create-liboliphaunt-release-fixture.py | 26 ++++++++++++++++ 4 files changed, 66 insertions(+) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 6174abfc..95f6d558 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -63,6 +63,11 @@ review production pipelines, then normalize implementation details. - The active branch contains the split native/WASIX tools package work and the example GUI smoke coverage. - Local-registry WASIX smoke coverage proves `pg_dump` through the SDK `dump_sql` path and `psql` through `PsqlOptions::command("SELECT 1")`. - Local-registry Cargo payload inspection confirmed `liboliphaunt-native-linux-x64-gnu-part-*` contains `initdb`, `pg_ctl`, and `postgres` only under `runtime/bin`, while `oliphaunt-tools-linux-x64-gnu-part-*` contains only `pg_dump` and `psql` there. +- The small liboliphaunt release fixture now includes all five native desktop + PostgreSQL binaries so fixture Cargo packaging exercises the split: + `liboliphaunt-native-*` keeps `initdb`, `pg_ctl`, and `postgres`, while + `oliphaunt-tools-*` keeps `pg_dump` and `psql`. Consumer-shape checks enforce + the same generator contract. - Release dry-run validation now inspects the nested WASIX runtime archive for `postgres` and `initdb`, and rejects `pg_ctl`, `pg_dump`, or `psql` there. - Local registry publication was refreshed with explicit native runtime/tools, diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index 81b54dc1..28079fb2 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -87,6 +87,11 @@ the release/tooling surface after the runtime tool crate split. `pg_dump` and `psql`. The generated `.crate` files are all below 10 MiB. - Generated root native payload content has `postgres`, `initdb`, and `pg_ctl` only; `pg_dump` and `psql` are present only in `oliphaunt-tools-*`. +- The small liboliphaunt release fixture now models all five native desktop + PostgreSQL binaries, so fixture packaging verifies that + `liboliphaunt-native-*` part crates keep only `initdb`, `pg_ctl`, and + `postgres`, while `oliphaunt-tools-*` part crates keep `pg_dump` and `psql`. + Consumer-shape checks now enforce that generator contract. - The local Cargo registry was refreshed from the split artifacts. The native Tauri example regenerated its lockfile through `examples/tools/with-local-registries.sh`, `cargo check` passed, and `startup_smoke_runs_sql_dump` passed through packaged diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 41710806..abab5e3c 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -20,6 +20,7 @@ import artifact_targets import product_metadata import extension_artifact_targets +import optimize_native_runtime_payload import package_liboliphaunt_wasix_cargo_artifacts @@ -409,6 +410,35 @@ def check_liboliphaunt(findings: list[Finding]) -> None: f"src/runtimes/liboliphaunt/native/release.toml registry_packages={product_registry_packages(product)!r}", severity="P0", ) + native_packager = read_text("tools/release/package_liboliphaunt_cargo_artifacts.py") + native_optimizer = read_text("tools/release/optimize_native_runtime_payload.py") + release_cli = read_text("tools/release/release.py") + require( + findings, + product, + "liboliphaunt-native-tool-split", + set(optimize_native_runtime_payload.NATIVE_RUNTIME_TOOL_STEMS) == {"initdb", "pg_ctl", "postgres"} + and set(optimize_native_runtime_payload.NATIVE_TOOLS_TOOL_STEMS) == {"pg_dump", "psql"} + and "copy_tools_payload" in native_packager + and "required_tools_member_paths" in native_packager + and "package_base=TOOLS_PRODUCT" in native_packager + and 'artifact_product=TOOLS_PRODUCT' in native_packager + and 'tool_set="runtime"' in native_packager + and 'tool_set="tools"' in native_packager + and "required_runtime_member_paths" in release_cli + and "required_tools_member_paths" in release_cli + and "stage_liboliphaunt_tools_npm_payloads" in release_cli + and "remove_native_tools_from_runtime" in release_cli + and "NATIVE_RUNTIME_TOOL_STEMS" in native_optimizer + and "NATIVE_TOOLS_TOOL_STEMS" in native_optimizer, + "Native root packages and crates must keep postgres/initdb/pg_ctl only, with pg_dump/psql published through oliphaunt-tools packages/crates.", + [ + "tools/release/optimize_native_runtime_payload.py", + "tools/release/package_liboliphaunt_cargo_artifacts.py", + "tools/release/release.py", + ], + severity="P0", + ) icu_package = read_json("src/runtimes/liboliphaunt/native/icu-npm/package.json") icu_metadata = icu_package.get("oliphaunt", {}) require( diff --git a/tools/test/create-liboliphaunt-release-fixture.py b/tools/test/create-liboliphaunt-release-fixture.py index 7117c4d7..db7e7152 100644 --- a/tools/test/create-liboliphaunt-release-fixture.py +++ b/tools/test/create-liboliphaunt-release-fixture.py @@ -15,6 +15,24 @@ from release_fixture_utils import write_checksum_manifest, write_tar_gz, write_zip +NATIVE_TOOL_STEMS = ("initdb", "pg_ctl", "pg_dump", "postgres", "psql") + + +def native_runtime_entries(*, windows: bool = False) -> dict[str, bytes]: + suffix = ".exe" if windows else "" + entries = { + f"runtime/bin/{tool}{suffix}": f"not-a-real-{tool}{suffix}\n".encode("utf-8") + for tool in NATIVE_TOOL_STEMS + } + entries["runtime/share/postgresql/README.release-fixture"] = b"release-shaped native runtime fixture\n" + return entries + + +def native_runtime_modes(*, windows: bool = False) -> dict[str, int]: + suffix = ".exe" if windows else "" + return {f"runtime/bin/{tool}{suffix}": 0o755 for tool in NATIVE_TOOL_STEMS} + + def runtime_resource_entries() -> dict[str, bytes]: return { "oliphaunt/package-size.tsv": ( @@ -141,21 +159,27 @@ def write_fixture_assets(asset_dir: Path, version: str) -> None: { "lib/liboliphaunt.dylib": b"not-a-real-dylib\n", "lib/modules/plpgsql.dylib": b"not-a-real-module\n", + **native_runtime_entries(), }, + modes=native_runtime_modes(), ) write_tar_gz( asset_dir / f"liboliphaunt-{version}-linux-x64-gnu.tar.gz", { "lib/liboliphaunt.so": b"not-a-real-elf\n", "lib/modules/plpgsql.so": b"not-a-real-module\n", + **native_runtime_entries(), }, + modes=native_runtime_modes(), ) write_tar_gz( asset_dir / f"liboliphaunt-{version}-linux-arm64-gnu.tar.gz", { "lib/liboliphaunt.so": b"not-a-real-elf\n", "lib/modules/plpgsql.so": b"not-a-real-module\n", + **native_runtime_entries(), }, + modes=native_runtime_modes(), ) write_tar_gz( asset_dir / f"liboliphaunt-{version}-ios-xcframework.tar.gz", @@ -174,7 +198,9 @@ def write_fixture_assets(asset_dir: Path, version: str) -> None: { "bin/oliphaunt.dll": b"not-a-real-dll\n", "lib/modules/plpgsql.dll": b"not-a-real-module\n", + **native_runtime_entries(windows=True), }, + modes=native_runtime_modes(windows=True), ) write_zip( asset_dir / f"liboliphaunt-{version}-apple-spm-xcframework.zip", From c5c0b87a2cbb9d6e121e30626eff973956d9442c Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 05:27:46 +0000 Subject: [PATCH 038/137] fix: derive local publish artifact preset --- docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 4 ++++ .../maintainers/examples-ci-release-validation.md | 4 ++++ tools/release/artifact_targets.py | 14 ++++++++++++++ tools/release/check_release_metadata.py | 8 ++++++++ tools/release/local_registry_publish.py | 15 ++++++--------- 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 95f6d558..d5566d01 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -96,6 +96,10 @@ review production pipelines, then normalize implementation details. artifact-target checks, and release policy checks now derive native/helper target artifact names from `artifact_targets` instead of restating the platform list. +- The local-registry `local-publish` preset now also derives WASIX AOT runtime + artifact names from release target metadata and rejects duplicate artifact + names. The preset currently resolves 35 unique CI artifacts for local publish + staging. - Dead existing-tag release workflow probes were removed. Idempotent rerun behavior stays in the publish handlers that actually own registry/GitHub publication, such as matching GitHub asset checksum skips and already-published diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index 28079fb2..29574967 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -160,6 +160,10 @@ the release/tooling surface after the runtime tool crate split. downloads, the local-registry download preset, and Node direct package-dir validation now derive artifact/package names from `artifact_targets` instead of copying the platform target list. +- The local-registry `local-publish` preset now derives WASIX AOT runtime + artifact names from release target metadata as well, and rejects duplicate + artifact names. The preset currently resolves 35 unique CI artifacts for local + publish staging. - Dead existing-tag workflow probes were removed; rerun idempotency remains in the publish handlers that own the actual registry or GitHub publication step. - TypeScript optional runtime package validation and release PR sync now share diff --git a/tools/release/artifact_targets.py b/tools/release/artifact_targets.py index 33f39f64..6796e070 100644 --- a/tools/release/artifact_targets.py +++ b/tools/release/artifact_targets.py @@ -671,6 +671,20 @@ def ci_npm_package_artifact_names(product: str, kind: str) -> list[str]: return sorted(names) +def ci_wasix_aot_runtime_artifact_names() -> list[str]: + names = [ + f"liboliphaunt-wasix-runtime-aot-{target.target}" + for target in artifact_targets( + product="liboliphaunt-wasix", + kind="wasix-aot-runtime", + published_only=True, + ) + ] + if not names: + product_metadata.fail("liboliphaunt-wasix has no published WASIX AOT runtime targets") + return sorted(names) + + def ci_sdk_package_artifact_name(product: str) -> str: config = product_metadata.product_config(product) if config.get("kind") != "sdk": diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 25ed29f4..c63bcbc9 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -294,6 +294,8 @@ def validate_release_setup_docs() -> None: def validate_local_registry_publisher() -> None: + import local_registry_publish + publisher = read_text("tools/release/local_registry_publish.py") if "explicit_roots = list(artifact_roots)" not in publisher or "roots = explicit_roots or [" not in publisher: fail("local registry publisher must treat explicit --artifact-root values as the selected artifact set") @@ -301,6 +303,12 @@ def validate_local_registry_publisher() -> None: fail("local registry publisher must not append explicit artifact roots to stale default build roots") if "def clear_local_cargo_home_cache" not in publisher or '"cache", "src", "index"' not in publisher: fail("local registry publisher must clear Cargo's local registry cache after same-version Cargo republishes") + artifacts = local_registry_publish.local_publish_artifacts() + duplicates = sorted({artifact for artifact in artifacts if artifacts.count(artifact) > 1}) + if duplicates: + fail("local registry publish artifact preset must not contain duplicate names: " + ", ".join(duplicates)) + if "ci_wasix_aot_runtime_artifact_names()" not in publisher: + fail("local registry publish preset must derive WASIX AOT artifact names from artifact target metadata") def validate_rust() -> None: diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index 1b2d7071..a945c51a 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -59,29 +59,26 @@ "liboliphaunt-native-release-assets", "liboliphaunt-wasix-extension-artifacts-wasix-portable", "liboliphaunt-wasix-release-assets", - "liboliphaunt-wasix-runtime-aot-linux-arm64-gnu", - "liboliphaunt-wasix-runtime-aot-linux-x64-gnu", - "liboliphaunt-wasix-runtime-aot-macos-arm64", - "liboliphaunt-wasix-runtime-aot-windows-x64-msvc", "liboliphaunt-wasix-runtime-portable", - "oliphaunt-broker-release-assets-linux-arm64-gnu", - "oliphaunt-broker-release-assets-linux-x64-gnu", - "oliphaunt-broker-release-assets-macos-arm64", - "oliphaunt-broker-release-assets-windows-x64-msvc", "oliphaunt-extension-package-artifacts", "oliphaunt-mobile-extension-package-artifacts", ] def local_publish_artifacts() -> list[str]: - return [ + artifacts = [ *STATIC_LOCAL_PUBLISH_ARTIFACTS, *artifact_targets.ci_release_asset_artifact_names("liboliphaunt-native", "native-runtime"), + *artifact_targets.ci_wasix_aot_runtime_artifact_names(), *artifact_targets.ci_release_asset_artifact_names("oliphaunt-broker", "broker-helper"), *artifact_targets.ci_release_asset_artifact_names("oliphaunt-node-direct", "node-direct-addon"), *artifact_targets.ci_npm_package_artifact_names("oliphaunt-node-direct", "node-direct-addon"), *artifact_targets.ci_sdk_package_artifact_names(), ] + duplicates = sorted({artifact for artifact in artifacts if artifacts.count(artifact) > 1}) + if duplicates: + raise RuntimeError("duplicate local publish artifact names: " + ", ".join(duplicates)) + return artifacts def rel(path: Path) -> str: From b118e70d96cb5861214d9c7b657a7d9b903be45d Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 05:42:12 +0000 Subject: [PATCH 039/137] fix: enforce runtime tools crate split --- tools/release/check_consumer_shape.py | 4 ++- tools/release/check_release_metadata.py | 46 +++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index abab5e3c..dc59474f 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -1761,13 +1761,15 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: and "oliphaunt/bin/pg_dump" in release_source and "oliphaunt/bin/psql" in release_source and "CORE_RUNTIME_ARCHIVE_FILES" in wasix_packager_source + and "TOOLS_PAYLOAD_FILES" in wasix_packager_source + and "TOOLS_AOT_ARTIFACTS" in wasix_packager_source and "FORBIDDEN_RUNTIME_ARCHIVE_TOOL_FILES" in wasix_packager_source and "oliphaunt/bin/initdb" in wasix_packager_source and "oliphaunt/bin/postgres" in wasix_packager_source and "oliphaunt/bin/pg_ctl" in wasix_packager_source and "oliphaunt/bin/pg_dump" in wasix_packager_source and "oliphaunt/bin/psql" in wasix_packager_source, - "Release validation must require postgres/initdb in the WASIX runtime archive and reject pg_ctl/pg_dump/psql there.", + "Release validation must require postgres/initdb in the WASIX runtime archive, reject pg_ctl/pg_dump/psql there, and publish pg_dump/psql through WASIX tools payload/AOT crates.", [ "tools/release/release.py", "tools/release/package_liboliphaunt_wasix_cargo_artifacts.py", diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index c63bcbc9..3bb5e573 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1131,6 +1131,52 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None tools_feature = set(manifest.get("features", {}).get("tools", [])) if tools_feature != expected_tools_feature: fail("oliphaunt-wasix tools feature must select exactly the WASIX pg_dump/psql tool artifact crates") + asset_manifest = tomllib.loads(read_text("src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml")) + if asset_manifest.get("package", {}).get("name") != "liboliphaunt-wasix-portable": + fail("WASIX root runtime asset crate must be liboliphaunt-wasix-portable") + tools_manifest = tomllib.loads(read_text("src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml")) + if tools_manifest.get("package", {}).get("name") != "oliphaunt-wasix-tools": + fail("WASIX split tools asset crate must be oliphaunt-wasix-tools") + asset_build_source = read_text("src/runtimes/liboliphaunt/wasix/crates/assets/build.rs") + if ( + '"bin/initdb.wasix.wasm"' not in asset_build_source + or '"bin/pg_dump.wasix.wasm"' in asset_build_source + or '"bin/psql.wasix.wasm"' in asset_build_source + or 'manifest["pg-dump"] = serde_json::Value::Null;' not in asset_build_source + or 'manifest["psql"] = serde_json::Value::Null;' not in asset_build_source + ): + fail("WASIX root runtime asset crate must embed initdb only and null split pg_dump/psql manifest entries") + tools_build_source = read_text("src/runtimes/liboliphaunt/wasix/crates/tools/build.rs") + if ( + '"bin/pg_dump.wasix.wasm"' not in tools_build_source + or '"bin/psql.wasix.wasm"' not in tools_build_source + or "pg_ctl" in tools_build_source + ): + fail("WASIX tools asset crate must package pg_dump and psql only; pg_ctl is intentionally absent") + wasix_packager_source = read_text("tools/release/package_liboliphaunt_wasix_cargo_artifacts.py") + if ( + package_liboliphaunt_wasix_cargo_artifacts.CORE_RUNTIME_ARCHIVE_FILES + != ("oliphaunt/bin/initdb", "oliphaunt/bin/postgres") + or package_liboliphaunt_wasix_cargo_artifacts.TOOLS_PAYLOAD_FILES + != ("bin/pg_dump.wasix.wasm", "bin/psql.wasix.wasm") + or package_liboliphaunt_wasix_cargo_artifacts.FORBIDDEN_RUNTIME_ARCHIVE_TOOL_FILES + != ("oliphaunt/bin/pg_ctl", "oliphaunt/bin/pg_dump", "oliphaunt/bin/psql") + or package_liboliphaunt_wasix_cargo_artifacts.TOOLS_AOT_ARTIFACTS + != {"tool:pg_dump", "tool:psql"} + or "split_runtime_tools_payload" not in wasix_packager_source + or "split_aot_tools_payload" not in wasix_packager_source + ): + fail("WASIX Cargo artifact packager must split pg_dump/psql into tools crates while keeping only postgres/initdb in root runtime crates") + native_packager_source = read_text("tools/release/package_liboliphaunt_cargo_artifacts.py") + if ( + optimize_native_runtime_payload.NATIVE_RUNTIME_TOOL_STEMS != ("initdb", "pg_ctl", "postgres") + or optimize_native_runtime_payload.NATIVE_TOOLS_TOOL_STEMS != ("pg_dump", "psql") + or "copy_tools_payload" not in native_packager_source + or "required_tools_member_paths" not in native_packager_source + or "package_base=TOOLS_PRODUCT" not in native_packager_source + or 'artifact_product=TOOLS_PRODUCT' not in native_packager_source + ): + fail("Native Cargo artifact packager must split pg_dump/psql into oliphaunt-tools crates while keeping postgres/initdb/pg_ctl in root runtime crates") sdk_lib_source = read_text("src/bindings/wasix-rust/crates/oliphaunt-wasix/src/lib.rs") sdk_server_source = read_text("src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/server.rs") sdk_pg_dump_source = read_text("src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/pg_dump.rs") From 3762f33215264d50dedc9eb1a01aabc8fec54b59 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 06:05:47 +0000 Subject: [PATCH 040/137] fix: harden mobile extension validation --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 8 ++ .../examples-ci-release-validation.md | 8 ++ src/sdks/react-native/android/build.gradle | 26 +++++ src/sdks/react-native/tools/check-sdk.sh | 96 ++++++++++++++++--- .../Tests/OliphauntTests/OliphauntTests.swift | 1 + tools/release/check_consumer_shape.py | 38 ++++++++ tools/release/check_release_metadata.py | 28 ++++++ 7 files changed, 192 insertions(+), 13 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index d5566d01..3b31008e 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -132,6 +132,14 @@ review production pipelines, then normalize implementation details. applies the same check after Maven exact-extension runtime artifacts are merged, and release metadata plus consumer-shape checks now enforce that resolver behavior. +- On 2026-06-26, + `examples/tools/with-local-registries.sh bash src/sdks/react-native/tools/check-sdk.sh build-android-bridge` + passed using the checked-in Gradle wrapper. The lane exercised the positive + split/prebuilt runtime resource paths and the negative selected-extension + missing-SQL diagnostics. +- Swift runtime-resource package-kind rejection now has an executable `@Test` + annotation, and release metadata plus consumer-shape checks guard against + regressing it to an unannotated helper. - Subagent SDK audit found these remaining next fixes: continue the broader SDK artifact-resolution comparison, identify any remaining feature gaps across SDKs, and add parity checks for invariants that are still documented only in diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index 29574967..8f191257 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -198,6 +198,14 @@ the release/tooling surface after the runtime tool crate split. and consumer-shape checks now enforce that the resolver extracts the selected Maven artifact, merges its `files/` payload, and validates both the selected `.control` file and versioned SQL files before updating generated manifests. +- On 2026-06-26, + `examples/tools/with-local-registries.sh bash src/sdks/react-native/tools/check-sdk.sh build-android-bridge` + passed with the checked-in Gradle wrapper. The lane covers split runtime, + prebuilt runtime resources, selected-extension missing-SQL failures, Android + static extension link evidence, unit tests, and lint. +- Swift runtime-resource package-kind rejection is covered by an executable + `@Test`, and release metadata plus consumer-shape checks require that + annotation to remain present. - Mobile native-direct startup now passes packaged runtime `sharedPreloadLibraries` through to `shared_preload_libraries=...` startup args in Kotlin Android/React Native Android and Swift/React Native iOS. diff --git a/src/sdks/react-native/android/build.gradle b/src/sdks/react-native/android/build.gradle index c144c326..5ace55c6 100644 --- a/src/sdks/react-native/android/build.gradle +++ b/src/sdks/react-native/android/build.gradle @@ -10,6 +10,7 @@ import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.TaskAction +import java.io.FileFilter import java.nio.file.Files import java.nio.file.Path import java.nio.file.StandardCopyOption @@ -313,6 +314,7 @@ abstract class PrepareOliphauntAndroidAssetsTask extends DefaultTask { } validateRuntimeResourcesSchema(sourceRuntimeResourcesRoot) copyTree(sourceRuntimeResourcesRoot.toPath(), new File(output, "oliphaunt").toPath(), ["static-registry/archives"] as Set) + validateSelectedExtensionFiles(new File(output, "oliphaunt/runtime/files"), selectedExtensions.get()) return } @@ -394,6 +396,7 @@ abstract class PrepareOliphauntAndroidAssetsTask extends DefaultTask { copyTree(source.toPath(), filesDir.toPath()) Map> metadataBySqlName = generatedExtensionMetadataBySqlName() List extensions = resolveExtensionSelection(requestedExtensions, metadataBySqlName) + validateSelectedExtensionFiles(filesDir, extensions) List nativeModuleStems = nativeModuleStems(extensions, metadataBySqlName) Set registeredModuleStems = new TreeSet<>(mobileStaticModuleStems) Set selectedModuleStems = new TreeSet<>(nativeModuleStems) @@ -436,6 +439,29 @@ abstract class PrepareOliphauntAndroidAssetsTask extends DefaultTask { ].join("\n") } + private static void validateSelectedExtensionFiles(File filesDir, List extensions) { + if (extensions.isEmpty()) { + return + } + File extensionDir = new File(filesDir, "share/postgresql/extension") + extensions.each { extension -> + File control = new File(extensionDir, "${extension}.control") + if (!control.isFile()) { + throw new GradleException( + "Oliphaunt React Native Android selected extension '${extension}' is missing control file ${control}" + ) + } + File[] sqlFiles = extensionDir.listFiles({ File file -> + file.isFile() && file.name.startsWith("${extension}--") && file.name.endsWith(".sql") + } as FileFilter) ?: [] as File[] + if (sqlFiles.length == 0) { + throw new GradleException( + "Oliphaunt React Native Android selected extension '${extension}' has no packaged SQL files in ${extensionDir}" + ) + } + } + } + private static Map> loadGeneratedExtensionMetadata(File metadataFile) { def parsed = new JsonSlurper().parse(metadataFile) if (!(parsed instanceof Map) || !(parsed.extensions instanceof List)) { diff --git a/src/sdks/react-native/tools/check-sdk.sh b/src/sdks/react-native/tools/check-sdk.sh index 71a3ea91..de30d753 100755 --- a/src/sdks/react-native/tools/check-sdk.sh +++ b/src/sdks/react-native/tools/check-sdk.sh @@ -163,7 +163,11 @@ YAML --exclude ios/vendor \ "$source_package_dir/" "$package_dir/" rm -rf "$scratch_root/node_modules" "$package_dir/node_modules" - run pnpm --dir "$scratch_root" install --frozen-lockfile + if [ "${PNPM_CONFIG_LOCKFILE:-}" = "false" ]; then + run pnpm --dir "$scratch_root" install --no-frozen-lockfile + else + run pnpm --dir "$scratch_root" install --frozen-lockfile + fi if [ ! -e "$package_dir/node_modules" ]; then ln -s "$scratch_root/node_modules" "$package_dir/node_modules" fi @@ -184,6 +188,12 @@ NODE require node require pnpm export CI="${CI:-1}" +gradle_cmd="gradle" +if [ -x "$root/src/sdks/kotlin/gradlew" ]; then + gradle_cmd="$root/src/sdks/kotlin/gradlew" +else + require gradle +fi if [ "$mode" = "coverage" ]; then exec tools/coverage/run-product oliphaunt-react-native @@ -652,19 +662,25 @@ if [ "$run_android_platform_checks" = "1" ]; then echo "React Native Android adapter checks require ANDROID_HOME" >&2 exit 1 } - run gradle -p "$android_dir" $android_abi_gradle_args $gradle_scratch_args $gradle_cache_args --quiet help - run gradle -p "$android_dir" assembleDebug $android_abi_gradle_args $gradle_scratch_args $gradle_cache_args + run "$gradle_cmd" -p "$android_dir" $android_abi_gradle_args $gradle_scratch_args $gradle_cache_args --quiet help + run "$gradle_cmd" -p "$android_dir" assembleDebug $android_abi_gradle_args $gradle_scratch_args $gradle_cache_args tmp_split_runtime="$(prepare_scratch_dir react-native-split-runtime)" tmp_split_template="$(prepare_scratch_dir react-native-split-template)" mkdir -p \ - "$tmp_split_runtime/share/postgresql" \ + "$tmp_split_runtime/share/postgresql/extension" \ "$tmp_split_runtime/lib/postgresql" \ "$tmp_split_template/base" printf 'runtime split smoke\n' >"$tmp_split_runtime/share/postgresql/README.liboliphaunt-split-smoke" + printf "comment = 'vector split smoke control'\n" >"$tmp_split_runtime/share/postgresql/extension/vector.control" + printf "select 'vector split smoke sql';\n" >"$tmp_split_runtime/share/postgresql/extension/vector--1.0.sql" + printf "comment = 'cube split smoke control'\n" >"$tmp_split_runtime/share/postgresql/extension/cube.control" + printf "select 'cube split smoke sql';\n" >"$tmp_split_runtime/share/postgresql/extension/cube--1.0.sql" + printf "comment = 'earthdistance split smoke control'\n" >"$tmp_split_runtime/share/postgresql/extension/earthdistance.control" + printf "select 'earthdistance split smoke sql';\n" >"$tmp_split_runtime/share/postgresql/extension/earthdistance--1.0.sql" printf '18\n' >"$tmp_split_template/PG_VERSION" printf 'template split smoke\n' >"$tmp_split_template/base/README.liboliphaunt-split-smoke" - run gradle -p "$android_dir" prepareOliphauntAndroidAssets \ + run "$gradle_cmd" -p "$android_dir" prepareOliphauntAndroidAssets \ "-PoliphauntRuntimeDir=$tmp_split_runtime" \ "-PoliphauntTemplatePgdataDir=$tmp_split_template" \ "-PoliphauntExtensions=vector" \ @@ -702,10 +718,37 @@ if [ "$run_android_platform_checks" = "1" ]; then require_manifest_line "$split_template_manifest" "mobileStaticRegistrySource=" \ "React Native Android split template manifest should not claim generated mobile static-registry source" + tmp_split_incomplete_runtime="$(prepare_scratch_dir react-native-split-incomplete-extension)" + mkdir -p "$tmp_split_incomplete_runtime/share/postgresql/extension" + printf 'runtime split incomplete smoke\n' >"$tmp_split_incomplete_runtime/share/postgresql/README.liboliphaunt-split-incomplete-smoke" + printf "comment = 'vector split incomplete control'\n" >"$tmp_split_incomplete_runtime/share/postgresql/extension/vector.control" + split_incomplete_extension_log="$scratch_root/react-native-split-incomplete-extension.log" + rm -f "$split_incomplete_extension_log" + printf '\n==> %s\n' "$gradle_cmd -p $android_dir prepareOliphauntAndroidAssets -PoliphauntExtensions=vector" + if "$gradle_cmd" -p "$android_dir" prepareOliphauntAndroidAssets \ + "-PoliphauntRuntimeDir=$tmp_split_incomplete_runtime" \ + "-PoliphauntTemplatePgdataDir=$tmp_split_template" \ + "-PoliphauntExtensions=vector" \ + $gradle_scratch_args \ + $gradle_smoke_cache_args >"$split_incomplete_extension_log" 2>&1; then + echo "React Native Android split runtime packaging accepted a selected extension without packaged SQL files" >&2 + cat "$split_incomplete_extension_log" >&2 + rm -f "$split_incomplete_extension_log" + exit 1 + fi + if ! grep -Fq "selected extension 'vector' has no packaged SQL files" "$split_incomplete_extension_log"; then + echo "React Native Android split runtime packaging failed without the expected selected-extension file diagnostic" >&2 + cat "$split_incomplete_extension_log" >&2 + rm -f "$split_incomplete_extension_log" + exit 1 + fi + rm -f "$split_incomplete_extension_log" + rm -rf "$tmp_split_incomplete_runtime" + split_static_log="$scratch_root/react-native-split-static.log" rm -f "$split_static_log" - printf '\n==> %s\n' "gradle -p $android_dir prepareOliphauntAndroidAssets -PoliphauntMobileStaticModules=vector" - if gradle -p "$android_dir" prepareOliphauntAndroidAssets \ + printf '\n==> %s\n' "$gradle_cmd -p $android_dir prepareOliphauntAndroidAssets -PoliphauntMobileStaticModules=vector" + if "$gradle_cmd" -p "$android_dir" prepareOliphauntAndroidAssets \ "-PoliphauntRuntimeDir=$tmp_split_runtime" \ "-PoliphauntTemplatePgdataDir=$tmp_split_template" \ "-PoliphauntExtensions=vector" \ @@ -725,7 +768,7 @@ if [ "$run_android_platform_checks" = "1" ]; then fi rm -f "$split_static_log" - run gradle -p "$android_dir" prepareOliphauntAndroidAssets \ + run "$gradle_cmd" -p "$android_dir" prepareOliphauntAndroidAssets \ "-PoliphauntRuntimeDir=$tmp_split_runtime" \ "-PoliphauntTemplatePgdataDir=$tmp_split_template" \ "-PoliphauntExtensions=earthdistance" \ @@ -742,8 +785,8 @@ if [ "$run_android_platform_checks" = "1" ]; then split_unknown_extension_log="$scratch_root/react-native-split-unknown-extension.log" rm -f "$split_unknown_extension_log" - printf '\n==> %s\n' "gradle -p $android_dir prepareOliphauntAndroidAssets -PoliphauntExtensions=acme_unknown" - if gradle -p "$android_dir" prepareOliphauntAndroidAssets \ + printf '\n==> %s\n' "$gradle_cmd -p $android_dir prepareOliphauntAndroidAssets -PoliphauntExtensions=acme_unknown" + if "$gradle_cmd" -p "$android_dir" prepareOliphauntAndroidAssets \ "-PoliphauntRuntimeDir=$tmp_split_runtime" \ "-PoliphauntTemplatePgdataDir=$tmp_split_template" \ "-PoliphauntExtensions=acme_unknown" \ @@ -831,9 +874,36 @@ package static-registry - - 45 extensions selected - - 30 extension vector - 3 30 REPORT + tmp_assets_incomplete="$(prepare_scratch_dir react-native-runtime-resources-incomplete-extension)" + cp -R "$tmp_assets/." "$tmp_assets_incomplete/" + rm -f "$tmp_assets_incomplete/oliphaunt/runtime/files/share/postgresql/extension/vector--1.0.sql" + runtime_resources_incomplete_log="$scratch_root/react-native-runtime-resources-incomplete-extension.log" + rm -f "$runtime_resources_incomplete_log" + printf '\n==> %s\n' "$gradle_cmd -p $android_dir prepareOliphauntAndroidAssets -PoliphauntRuntimeResourcesDir= -PoliphauntExtensions=vector" + if "$gradle_cmd" -p "$android_dir" prepareOliphauntAndroidAssets \ + "-PoliphauntRuntimeResourcesDir=$tmp_assets_incomplete" \ + "-PoliphauntExtensions=vector" \ + $gradle_scratch_args \ + $gradle_smoke_cache_args >"$runtime_resources_incomplete_log" 2>&1; then + echo "React Native Android prebuilt runtime resources accepted a selected extension without packaged SQL files" >&2 + cat "$runtime_resources_incomplete_log" >&2 + rm -f "$runtime_resources_incomplete_log" + rm -rf "$tmp_assets_incomplete" + exit 1 + fi + if ! grep -Fq "selected extension 'vector' has no packaged SQL files" "$runtime_resources_incomplete_log"; then + echo "React Native Android prebuilt runtime resources failed without the expected selected-extension file diagnostic" >&2 + cat "$runtime_resources_incomplete_log" >&2 + rm -f "$runtime_resources_incomplete_log" + rm -rf "$tmp_assets_incomplete" + exit 1 + fi + rm -f "$runtime_resources_incomplete_log" + rm -rf "$tmp_assets_incomplete" + android_link_evidence="$scratch_root/android-static-extension-link-$android_smoke_abi.tsv" rm -f "$android_link_evidence" - run gradle -p "$android_dir" assembleDebug \ + run "$gradle_cmd" -p "$android_dir" assembleDebug \ "-PoliphauntRuntimeResourcesDir=$tmp_assets" \ "-PoliphauntAndroidJniLibsDir=$tmp_static_jni" \ "-PoliphauntAndroidAbiFilters=$android_smoke_abi" \ @@ -931,7 +1001,7 @@ REPORT tmp_jni="$(prepare_scratch_dir react-native-jni)" mkdir -p "$tmp_jni/jniLibs/arm64-v8a" printf 'not-a-real-android-elf-for-packaging-smoke\n' >"$tmp_jni/jniLibs/arm64-v8a/liboliphaunt.so" - run gradle -p "$android_dir" prepareOliphauntAndroidJniLibs \ + run "$gradle_cmd" -p "$android_dir" prepareOliphauntAndroidJniLibs \ "-PoliphauntAndroidJniLibsDir=$tmp_jni" \ $gradle_scratch_args \ $gradle_smoke_cache_args @@ -943,7 +1013,7 @@ REPORT fi rm -rf "$tmp_jni" - run gradle -p "$android_dir" testDebugUnitTest lintDebug $android_abi_gradle_args $gradle_scratch_args $gradle_cache_args + run "$gradle_cmd" -p "$android_dir" testDebugUnitTest lintDebug $android_abi_gradle_args $gradle_scratch_args $gradle_cache_args fi if [ "$mode" = "build-android-bridge" ]; then diff --git a/src/sdks/swift/Tests/OliphauntTests/OliphauntTests.swift b/src/sdks/swift/Tests/OliphauntTests/OliphauntTests.swift index 963b29b7..9ae8ae84 100644 --- a/src/sdks/swift/Tests/OliphauntTests/OliphauntTests.swift +++ b/src/sdks/swift/Tests/OliphauntTests/OliphauntTests.swift @@ -1657,6 +1657,7 @@ func runtimeResourcesRejectUnsupportedSchema() throws { } } +@Test func runtimeResourcesRejectUnsupportedPackageKindLayout() throws { let fixture = try makeRuntimeResourceFixture() defer { diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index dc59474f..f50bc699 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -1075,6 +1075,16 @@ def check_swift(findings: list[Finding]) -> None: f"tools/release/render_swiftpm_release_package.py still contains {forbidden}", severity="P0", ) + swift_tests = read_text("src/sdks/swift/Tests/OliphauntTests/OliphauntTests.swift") + require( + findings, + product, + "swift-runtime-resource-layout-test", + "@Test\nfunc runtimeResourcesRejectUnsupportedPackageKindLayout() throws" in swift_tests, + "Swift runtime-resource layout rejection must stay covered by an executable test.", + "src/sdks/swift/Tests/OliphauntTests/OliphauntTests.swift", + severity="P0", + ) def check_kotlin(findings: list[Finding]) -> None: @@ -1326,6 +1336,34 @@ def check_react_native(findings: list[Finding]) -> None: "src/sdks/react-native/OliphauntReactNative.podspec", severity="P0", ) + android_gradle = read_text("src/sdks/react-native/android/build.gradle") + rn_check = read_text("src/sdks/react-native/tools/check-sdk.sh") + rn_extension_validation_fragments = [ + 'validateSelectedExtensionFiles(new File(output, "oliphaunt/runtime/files"), selectedExtensions.get())', + "validateSelectedExtensionFiles(filesDir, extensions)", + "private static void validateSelectedExtensionFiles", + "is missing control file", + "has no packaged SQL files in", + "PNPM_CONFIG_LOCKFILE", + "src/sdks/kotlin/gradlew", + "react-native-split-incomplete-extension", + "prebuilt runtime resources accepted a selected extension without packaged SQL files", + ] + require( + findings, + product, + "rn-android-extension-file-validation", + all( + fragment in android_gradle or fragment in rn_check + for fragment in rn_extension_validation_fragments + ), + "React Native Android must reject selected extensions when split or prebuilt runtime resources lack packaged control/SQL files.", + [ + "src/sdks/react-native/android/build.gradle", + "src/sdks/react-native/tools/check-sdk.sh", + ], + severity="P0", + ) def check_typescript(findings: list[Finding]) -> None: diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 3bb5e573..eb18f2d4 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -526,6 +526,11 @@ def validate_swift(swift_version: str, liboliphaunt_version: str) -> None: "oliphaunt-extension-vector", "Swift SDK README must describe exact-extension artifacts by release product, not hidden SwiftPM products", ) + require_text( + "src/sdks/swift/Tests/OliphauntTests/OliphauntTests.swift", + "@Test\nfunc runtimeResourcesRejectUnsupportedPackageKindLayout() throws", + "Swift runtime-resource layout rejection must be an executable test, not an unannotated helper", + ) swift_readme = read_text("src/sdks/swift/README.md") allowed_extension_api_symbols = { "OliphauntExtensionArtifactResolution", @@ -679,6 +684,29 @@ def validate_react_native(rn_version: str, swift_version: str, kotlin_version: s '?: "dev.oliphaunt:oliphaunt:${kotlinSdkVersion}"', "React Native Android package must default to the published Kotlin SDK Maven coordinate", ) + for needle in [ + 'validateSelectedExtensionFiles(new File(output, "oliphaunt/runtime/files"), selectedExtensions.get())', + "validateSelectedExtensionFiles(filesDir, extensions)", + "private static void validateSelectedExtensionFiles", + "is missing control file", + "has no packaged SQL files in", + ]: + require_text( + "src/sdks/react-native/android/build.gradle", + needle, + "React Native Android asset preparation must validate selected extension control and SQL files for split and prebuilt runtime resources", + ) + for needle in [ + "PNPM_CONFIG_LOCKFILE", + "src/sdks/kotlin/gradlew", + "react-native-split-incomplete-extension", + "prebuilt runtime resources accepted a selected extension without packaged SQL files", + ]: + require_text( + "src/sdks/react-native/tools/check-sdk.sh", + needle, + "React Native Android package checks must cover selected-extension file validation for split and prebuilt runtime resources", + ) require_text( "src/sdks/react-native/tools/check-sdk.sh", "local Kotlin SDK composite builds must be explicit development overrides", From f72c9879279b651bba2ba3b2624c842f3e816fc9 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 06:27:43 +0000 Subject: [PATCH 041/137] fix: generate local cargo artifacts from release assets --- tools/release/check_release_metadata.py | 9 ++ tools/release/local_registry_publish.py | 126 +++++++++++++++++++++++- 2 files changed, 134 insertions(+), 1 deletion(-) diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index eb18f2d4..9f9f72f8 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -303,6 +303,15 @@ def validate_local_registry_publisher() -> None: fail("local registry publisher must not append explicit artifact roots to stale default build roots") if "def clear_local_cargo_home_cache" not in publisher or '"cache", "src", "index"' not in publisher: fail("local registry publisher must clear Cargo's local registry cache after same-version Cargo republishes") + if ( + "def stage_release_asset_cargo_packages" not in publisher + or "package_liboliphaunt_cargo_artifacts.py" not in publisher + or "package_broker_cargo_artifacts.py" not in publisher + or "package_liboliphaunt_wasix_cargo_artifacts.py" not in publisher + or "host_cargo_release_target()" not in publisher + or "stage_release_asset_cargo_packages(roots, registry_root, dry_run, result)" not in publisher + ): + fail("local registry Cargo publishing must generate runtime/tool artifact crates from staged release assets") artifacts = local_registry_publish.local_publish_artifacts() duplicates = sorted({artifact for artifact in artifacts if artifacts.count(artifact) > 1}) if duplicates: diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index a945c51a..259fb965 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -242,11 +242,20 @@ def copy_release_assets( patterns: tuple[str, ...], ) -> list[Path]: candidates: list[Path] = [] + destination_resolved = destination.resolve() for root in roots: if not root.is_dir(): continue for pattern in patterns: - candidates.extend(path for path in root.rglob(pattern) if path.is_file()) + for path in root.rglob(pattern): + if not path.is_file(): + continue + try: + path.resolve().relative_to(destination_resolved) + continue + except ValueError: + pass + candidates.append(path) if not candidates: return [] @@ -266,6 +275,12 @@ def copy_release_assets( return copied +def release_asset_dir_has_files(asset_dir: Path, patterns: tuple[str, ...]) -> bool: + if not asset_dir.is_dir(): + return False + return any(path.is_file() for pattern in patterns for path in asset_dir.glob(pattern)) + + def host_npm_target() -> str | None: machine = host_platform.machine().lower() if sys.platform == "linux" and machine in {"x86_64", "amd64"}: @@ -2310,8 +2325,117 @@ def cargo_crate_priority(path: Path, registry_root: Path) -> tuple[int, str]: return priority, str(path) +def stage_release_asset_cargo_packages( + roots: list[Path], + registry_root: Path, + dry_run: bool, + result: SurfaceResult, +) -> list[Path]: + if dry_run: + result.staged.append("dry-run generated release-asset Cargo artifact crates") + return [] + + sys.path.insert(0, str(ROOT / "tools" / "release")) + import release # type: ignore + + output_root = registry_root / "cargo-generated" / "release-asset-crates" + shutil.rmtree(output_root, ignore_errors=True) + output_root.mkdir(parents=True, exist_ok=True) + generated_roots: list[Path] = [] + host_target = host_cargo_release_target() + + lib_version = release.current_product_version("liboliphaunt-native") + lib_patterns = (f"liboliphaunt-{lib_version}-*",) + lib_asset_dir = ROOT / "target" / "liboliphaunt" / "release-assets" + copied_lib_assets = copy_release_assets(roots, lib_asset_dir, lib_patterns) + lib_output_dir = output_root / "liboliphaunt-native" + if host_target is None: + result.add_skip("current host does not map to a supported native runtime Cargo target") + elif copied_lib_assets or release_asset_dir_has_files(lib_asset_dir, lib_patterns): + if copied_lib_assets: + result.staged.append( + f"staged {len(copied_lib_assets)} liboliphaunt release asset(s) for Cargo" + ) + run( + [ + "python3", + "tools/release/package_liboliphaunt_cargo_artifacts.py", + "--version", + lib_version, + "--output-dir", + str(lib_output_dir), + "--target", + host_target, + ] + ) + generated_roots.append(lib_output_dir) + else: + result.add_skip("no liboliphaunt release assets found for native Cargo artifact packages") + + broker_version = release.current_product_version("oliphaunt-broker") + broker_patterns = ("oliphaunt-broker-*.tar.gz", "oliphaunt-broker-*.zip") + broker_asset_dir = ROOT / "target" / "oliphaunt-broker" / "release-assets" + copied_broker_assets = copy_release_assets(roots, broker_asset_dir, broker_patterns) + broker_output_dir = output_root / "oliphaunt-broker" + if host_target is None: + result.add_skip("current host does not map to a supported broker Cargo target") + elif copied_broker_assets or release_asset_dir_has_files(broker_asset_dir, broker_patterns): + if copied_broker_assets: + result.staged.append( + f"staged {len(copied_broker_assets)} broker release asset(s) for Cargo" + ) + run( + [ + "python3", + "tools/release/package_broker_cargo_artifacts.py", + "--version", + broker_version, + "--output-dir", + str(broker_output_dir), + "--target", + host_target, + ] + ) + generated_roots.append(broker_output_dir) + else: + result.add_skip("no broker release assets found for broker Cargo artifact packages") + + wasix_version = release.current_product_version("liboliphaunt-wasix") + wasix_patterns = (f"liboliphaunt-wasix-{wasix_version}-*",) + wasix_asset_dir = ROOT / "target" / "oliphaunt-wasix" / "release-assets" + copied_wasix_assets = copy_release_assets(roots, wasix_asset_dir, wasix_patterns) + wasix_output_dir = output_root / "liboliphaunt-wasix" + if copied_wasix_assets or release_asset_dir_has_files(wasix_asset_dir, wasix_patterns): + if copied_wasix_assets: + result.staged.append( + f"staged {len(copied_wasix_assets)} WASIX release asset(s) for Cargo" + ) + run( + [ + "python3", + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.py", + "--version", + wasix_version, + "--output-dir", + str(wasix_output_dir), + ] + ) + generated_roots.append(wasix_output_dir) + else: + result.add_skip("no WASIX release assets found for WASIX Cargo artifact packages") + + generated_crates = discover_files(generated_roots, (".crate",)) + if generated_crates: + result.staged.append(f"generated {len(generated_crates)} release-asset Cargo crate(s)") + return generated_roots + return generated_roots + + def publish_cargo(roots: list[Path], registry_root: Path, dry_run: bool, strict: bool) -> SurfaceResult: result = SurfaceResult("cargo") + release_asset_roots = stage_release_asset_cargo_packages(roots, registry_root, dry_run, result) + if release_asset_roots: + roots = [*roots, *release_asset_roots] generated_roots = stage_cargo_source_crates(roots, registry_root, dry_run, result) generated_roots.extend( package_native_extension_cargo_crates( From ac37f5b3a9c667ee8e41bc5322ef7f998598a0ca Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 06:58:37 +0000 Subject: [PATCH 042/137] fix: publish native icu in local registry --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 30 +++++++++++++++++++ examples/electron-wasix/src-wasix/Cargo.lock | 4 +-- examples/tauri-wasix/src-tauri/Cargo.lock | 4 +-- examples/tauri/src-tauri/Cargo.lock | 6 ++-- .../tauri-sqlx-vanilla/src-tauri/Cargo.lock | 4 +-- .../check-sdk-mobile-extension-surface.sh | 2 ++ tools/release/check_release_metadata.py | 2 ++ tools/release/local_registry_publish.py | 1 - 8 files changed, 43 insertions(+), 10 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 3b31008e..44af628a 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -29,6 +29,9 @@ review production pipelines, then normalize implementation details. - [x] Verify native runtime payloads contain `postgres`, `initdb`, `pg_ctl`; native tools payloads contain `pg_dump`, `psql`. - [x] Verify WASIX runtime payloads contain `postgres`, `initdb`; WASIX tools payloads contain `pg_dump`, `psql`, not `pg_ctl`. - [ ] Verify extension packages and runtime tools are published and installed from registries idiomatically. +- [ ] Derive or validate native Maven runtime package manifests and Kotlin Maven existing-version probes from release metadata. +- [ ] Add a publish-target coverage check that every declared registry/release target has both CI production and release publication handling. +- [ ] Derive or policy-check the WASIX runtime/tools AOT Cargo package maps from the public WASIX package graph. - [x] Make extension Maven registry surfaces explicit in extension metadata instead of silently appending them in release tooling. - [x] Remove or generate duplicated release target lists in workflow downloads, node-direct package dirs, artifact target checks, and release policy checks. - [x] Decide whether existing-tag release probes should become a uniform idempotency gate or be removed. @@ -74,6 +77,18 @@ review production pipelines, then normalize implementation details. broker, WASIX runtime/tools/AOT, extension, JS SDK, and node-direct artifact roots. The npm install surface now includes `@oliphaunt/tools-linux-x64-gnu` from Verdaccio, and its payload contains only `pg_dump` and `psql`. +- The local npm registry publisher now includes the declared `@oliphaunt/icu` + sidecar package when staging native liboliphaunt packages from release assets. + `tools/release/check_release_metadata.py` rejects future `include_icu=False` + drift in that path. A focused local npm publish verified + `@oliphaunt/icu`, `@oliphaunt/liboliphaunt-linux-x64-gnu`, + `@oliphaunt/tools-linux-x64-gnu`, and `@oliphaunt/ts` at version `0.1.0` + from Verdaccio. +- The public WASIX release assets were regenerated from current generated + assets; the portable runtime archive now provides both split tool payloads + (`bin/pg_dump.wasix.wasm` and `bin/psql.wasix.wasm`) for the + `oliphaunt-wasix-tools` package builder, while the root runtime manifest keeps + tools out of the normal runtime payload. - Frontend builds passed through `examples/tools/with-local-registries.sh` for `examples/electron`, `examples/electron-wasix`, `examples/tauri`, `examples/tauri-wasix`, and @@ -85,6 +100,12 @@ review production pipelines, then normalize implementation details. reads, while normal CI keeps `--frozen-lockfile`. - `examples/tools/run-tauri-webdriver-smoke.sh examples/tauri` and `examples/tools/run-tauri-webdriver-smoke.sh examples/tauri-wasix` now provide repeatable Linux GUI smoke coverage using `tauri-driver`, `WebKitWebDriver`, and `xvfb-run`. - `examples/tools/run-electron-driver-smoke.sh examples/electron` and `examples/tools/run-electron-driver-smoke.sh examples/electron-wasix` now provide repeatable Linux GUI smoke coverage using the packaged Electron binary, an IPC test-driver hook, and `xvfb-run` when present. +- On 2026-06-26, all four GUI smoke commands passed against the refreshed local + registries: native Electron, WASIX Electron, native Tauri, and WASIX Tauri. + Native Tauri compiled `oliphaunt-tools-linux-x64-gnu` plus split runtime and + extension crates from `oliphaunt-local`; WASIX Tauri exercised the split + WASIX runtime/tools/AOT and selected extension package graph through + WebDriver. - `tools/release/sync_release_pr.py --check`, `check_release_metadata.py`, `check_consumer_shape.py`, `check_artifact_targets.py`, and the full `tools/release/release.py check` pass after refreshing the WASIX asset input fingerprint and extension evidence digests. - Extension Maven publication is now explicit in each exact-extension `release.toml`: the metadata lists `maven-central` and the two Android Maven @@ -132,6 +153,10 @@ review production pipelines, then normalize implementation details. applies the same check after Maven exact-extension runtime artifacts are merged, and release metadata plus consumer-shape checks now enforce that resolver behavior. +- React Native Android split/local runtime packaging now has the same selected + extension control/SQL validation as Kotlin Android, with the mobile extension + surface policy checking that the guard remains in place before manifests are + published. - On 2026-06-26, `examples/tools/with-local-registries.sh bash src/sdks/react-native/tools/check-sdk.sh build-android-bridge` passed using the checked-in Gradle wrapper. The lane exercised the positive @@ -144,6 +169,11 @@ review production pipelines, then normalize implementation details. artifact-resolution comparison, identify any remaining feature gaps across SDKs, and add parity checks for invariants that are still documented only in prose. +- Subagent CI/release audit found these remaining release-surface fixes: remove + or validate the duplicated native Maven artifact manifest rows, derive Kotlin + Maven existing-version probes from the declared package set, add coverage + checks from `publish_targets` to workflow/release handlers, and keep WASIX + tools-AOT package maps tied to the public WASIX Cargo package graph. - Local workflow tooling is available: `act` is installed at v0.2.89, which matches the latest upstream release published on 2026-06-01, Docker is available, `act -l` parses the CI, Release, and mobile E2E workflow graph, diff --git a/examples/electron-wasix/src-wasix/Cargo.lock b/examples/electron-wasix/src-wasix/Cargo.lock index f5b1d040..fdb65219 100644 --- a/examples/electron-wasix/src-wasix/Cargo.lock +++ b/examples/electron-wasix/src-wasix/Cargo.lock @@ -1589,7 +1589,7 @@ dependencies = [ name = "liboliphaunt-wasix-portable" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "74e4a84c8db15e4be7945d7b3a2ab1cb30a687b155367f32a25155891f604e77" +checksum = "c37d60ec719b989025b70a04e72c062afc69da9b55e26c15e2726a566da01fc2" dependencies = [ "oliphaunt-extension-hstore-wasix", "oliphaunt-extension-hstore-wasix-aot-aarch64-apple-darwin", @@ -2060,7 +2060,7 @@ dependencies = [ name = "oliphaunt-wasix-tools" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "d0e68ff6be7ea53e3d8685859a8f2cf67597ff4d0badb24623df3bb56824530c" +checksum = "3a767b3afef41b9d6692c74870df7739aeb208bf3078a92a116afb4558872b4d" dependencies = [ "sha2 0.10.9", ] diff --git a/examples/tauri-wasix/src-tauri/Cargo.lock b/examples/tauri-wasix/src-tauri/Cargo.lock index ba8cd493..972cdb01 100644 --- a/examples/tauri-wasix/src-tauri/Cargo.lock +++ b/examples/tauri-wasix/src-tauri/Cargo.lock @@ -2782,7 +2782,7 @@ dependencies = [ name = "liboliphaunt-wasix-portable" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "74e4a84c8db15e4be7945d7b3a2ab1cb30a687b155367f32a25155891f604e77" +checksum = "c37d60ec719b989025b70a04e72c062afc69da9b55e26c15e2726a566da01fc2" dependencies = [ "oliphaunt-extension-hstore-wasix", "oliphaunt-extension-hstore-wasix-aot-aarch64-apple-darwin", @@ -3533,7 +3533,7 @@ dependencies = [ name = "oliphaunt-wasix-tools" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "d0e68ff6be7ea53e3d8685859a8f2cf67597ff4d0badb24623df3bb56824530c" +checksum = "3a767b3afef41b9d6692c74870df7739aeb208bf3078a92a116afb4558872b4d" dependencies = [ "sha2 0.10.9", ] diff --git a/examples/tauri/src-tauri/Cargo.lock b/examples/tauri/src-tauri/Cargo.lock index 8579c5d8..82d353e0 100644 --- a/examples/tauri/src-tauri/Cargo.lock +++ b/examples/tauri/src-tauri/Cargo.lock @@ -2203,7 +2203,7 @@ dependencies = [ name = "oliphaunt-extension-hstore-linux-x64-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "4a9b6d73245fb432a8aaa74f20f5b6bd2a1adc7ab820ea289f7002d84b0d98b0" +checksum = "6a4ff122d6b692bcc1a0b7e3c20e88c4255f76deb9507c0c6300f67870839efd" dependencies = [ "sha2", ] @@ -2212,7 +2212,7 @@ dependencies = [ name = "oliphaunt-extension-pg-trgm-linux-x64-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "6334691d2aeb32752c4f2a586bac0836d6081d821421547e1d4a513e659d932b" +checksum = "1877c71f7a75afadc5cd5a34bc3b246a1b1603c24f06aa9a1c762145a6672596" dependencies = [ "sha2", ] @@ -2221,7 +2221,7 @@ dependencies = [ name = "oliphaunt-extension-unaccent-linux-x64-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "58ba1f77413bf35eb5f90315fe17ec2b10a208a3090eb511d2f17d650d820b14" +checksum = "9eabb41963dd6935ae1418179f0667b89a604eb30a636b781583157527f21901" dependencies = [ "sha2", ] diff --git a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock index 1eecbbf9..44f7e134 100644 --- a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock +++ b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock @@ -2984,7 +2984,7 @@ dependencies = [ name = "liboliphaunt-wasix-portable" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "74e4a84c8db15e4be7945d7b3a2ab1cb30a687b155367f32a25155891f604e77" +checksum = "c37d60ec719b989025b70a04e72c062afc69da9b55e26c15e2726a566da01fc2" dependencies = [ "serde", "serde_json", @@ -3613,7 +3613,7 @@ dependencies = [ name = "oliphaunt-wasix-tools" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "d0e68ff6be7ea53e3d8685859a8f2cf67597ff4d0badb24623df3bb56824530c" +checksum = "3a767b3afef41b9d6692c74870df7739aeb208bf3078a92a116afb4558872b4d" dependencies = [ "sha2 0.10.9", ] diff --git a/tools/policy/check-sdk-mobile-extension-surface.sh b/tools/policy/check-sdk-mobile-extension-surface.sh index 8a5897b7..c74a59c2 100755 --- a/tools/policy/check-sdk-mobile-extension-surface.sh +++ b/tools/policy/check-sdk-mobile-extension-surface.sh @@ -86,6 +86,8 @@ require_text src/sdks/react-native/android/build.gradle "generatedNativeModuleSt "React Native Android Gradle packaging must derive native module stems from generated extension metadata" require_text src/sdks/react-native/android/build.gradle "cannot select unknown extension" \ "React Native Android split runtime packaging must reject extensions absent from generated metadata" +require_text src/sdks/react-native/android/build.gradle "validateSelectedExtensionFiles" \ + "React Native Android split runtime packaging must validate selected extension control and SQL files before publishing manifests" reject_text src/sdks/react-native/android/build.gradle " return extension" \ "React Native Android Gradle packaging must not infer native module stems for unknown extensions" reject_text src/sdks/react-native/android/build.gradle "return \"postgis-3\"" \ diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 9f9f72f8..e54f72fd 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -301,6 +301,8 @@ def validate_local_registry_publisher() -> None: fail("local registry publisher must treat explicit --artifact-root values as the selected artifact set") if "roots.extend(extra_roots)" in publisher: fail("local registry publisher must not append explicit artifact roots to stale default build roots") + if "include_icu=False" in publisher: + fail("local registry npm publishing must include the declared @oliphaunt/icu sidecar package") if "def clear_local_cargo_home_cache" not in publisher or '"cache", "src", "index"' not in publisher: fail("local registry publisher must clear Cargo's local registry cache after same-version Cargo republishes") if ( diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index 259fb965..0156f6b9 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -1144,7 +1144,6 @@ def stage_release_asset_npm_packages( lib_version, validate_assets=False, targets=targets, - include_icu=False, ) ) else: From 037349aafdff4ef08c37b1aafbdc175e7b609828 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 07:06:00 +0000 Subject: [PATCH 043/137] fix: derive release publish metadata --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 18 +++- .../release/build_maven_artifact_manifest.py | 72 +++++++++------- tools/release/check_release_metadata.py | 46 ++++++++++ tools/release/product_metadata.py | 17 ++++ tools/release/release.py | 86 +++++++++++++++---- 5 files changed, 189 insertions(+), 50 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 44af628a..fcbe6cf6 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -29,9 +29,9 @@ review production pipelines, then normalize implementation details. - [x] Verify native runtime payloads contain `postgres`, `initdb`, `pg_ctl`; native tools payloads contain `pg_dump`, `psql`. - [x] Verify WASIX runtime payloads contain `postgres`, `initdb`; WASIX tools payloads contain `pg_dump`, `psql`, not `pg_ctl`. - [ ] Verify extension packages and runtime tools are published and installed from registries idiomatically. -- [ ] Derive or validate native Maven runtime package manifests and Kotlin Maven existing-version probes from release metadata. -- [ ] Add a publish-target coverage check that every declared registry/release target has both CI production and release publication handling. -- [ ] Derive or policy-check the WASIX runtime/tools AOT Cargo package maps from the public WASIX package graph. +- [x] Derive or validate native Maven runtime package manifests and Kotlin Maven existing-version probes from release metadata. +- [x] Add a publish-target coverage check that every declared registry/release target has release publication handling and a Release workflow invocation. +- [x] Derive or policy-check the WASIX runtime/tools AOT Cargo package maps from the public WASIX package graph. - [x] Make extension Maven registry surfaces explicit in extension metadata instead of silently appending them in release tooling. - [x] Remove or generate duplicated release target lists in workflow downloads, node-direct package dirs, artifact target checks, and release policy checks. - [x] Decide whether existing-tag release probes should become a uniform idempotency gate or be removed. @@ -174,6 +174,18 @@ review production pipelines, then normalize implementation details. Maven existing-version probes from the declared package set, add coverage checks from `publish_targets` to workflow/release handlers, and keep WASIX tools-AOT package maps tied to the public WASIX Cargo package graph. +- Native runtime Maven artifact manifest generation now derives its four + `dev.oliphaunt.runtime:*` coordinates from + `liboliphaunt-native.registry_packages`; unknown runtime Maven coordinates + fail manifest generation instead of being silently omitted. +- Kotlin Maven existing-version probes now derive their three Maven Central POM + URLs from `oliphaunt-kotlin.registry_packages`. The release metadata check + rejects reintroduced hard-coded Kotlin Maven URLs. +- Release metadata checks now compare every product's declared + `publish_targets` with `release.py` publish-step target coverage and require + the Release workflow to invoke each non-extension product step. TypeScript's + combined npm/JSR step and Swift's combined GitHub/SwiftPM-source-tag step are + represented explicitly in the coverage map. - Local workflow tooling is available: `act` is installed at v0.2.89, which matches the latest upstream release published on 2026-06-01, Docker is available, `act -l` parses the CI, Release, and mobile E2E workflow graph, diff --git a/tools/release/build_maven_artifact_manifest.py b/tools/release/build_maven_artifact_manifest.py index cacc5dd4..a39c1ff6 100644 --- a/tools/release/build_maven_artifact_manifest.py +++ b/tools/release/build_maven_artifact_manifest.py @@ -48,44 +48,56 @@ def tsv_row( return "\t".join(values) +RUNTIME_MAVEN_ARTIFACTS = { + "liboliphaunt-runtime-resources": { + "filename": "liboliphaunt-{version}-runtime-resources.tar.gz", + "name": "Oliphaunt runtime resources", + "description": "Package-managed Oliphaunt PostgreSQL runtime resources for Android app builds.", + }, + "oliphaunt-icu": { + "filename": "liboliphaunt-{version}-icu-data.tar.gz", + "name": "Oliphaunt ICU data", + "description": "Package-managed optional ICU data files for Oliphaunt app builds.", + }, + "liboliphaunt-android-arm64-v8a": { + "filename": "liboliphaunt-{version}-android-arm64-v8a.tar.gz", + "name": "Oliphaunt Android runtime arm64-v8a", + "description": "Package-managed liboliphaunt Android runtime for arm64-v8a app builds.", + }, + "liboliphaunt-android-x86_64": { + "filename": "liboliphaunt-{version}-android-x86_64.tar.gz", + "name": "Oliphaunt Android runtime x86_64", + "description": "Package-managed liboliphaunt Android runtime for x86_64 app builds.", + }, +} + + +def split_maven_coordinate(coordinate: str) -> tuple[str, str]: + group_id, separator, artifact_id = coordinate.partition(":") + if not separator or not group_id or not artifact_id: + fail(f"invalid Maven coordinate {coordinate!r}; expected group:artifact") + return group_id, artifact_id + + def runtime_rows(asset_root: Path) -> list[str]: version = product_metadata.read_current_version("liboliphaunt-native") - assets = [ - ( - "liboliphaunt-runtime-resources", - f"liboliphaunt-{version}-runtime-resources.tar.gz", - "Oliphaunt runtime resources", - "Package-managed Oliphaunt PostgreSQL runtime resources for Android app builds.", - ), - ( - "oliphaunt-icu", - f"liboliphaunt-{version}-icu-data.tar.gz", - "Oliphaunt ICU data", - "Package-managed optional ICU data files for Oliphaunt app builds.", - ), - ( - "liboliphaunt-android-arm64-v8a", - f"liboliphaunt-{version}-android-arm64-v8a.tar.gz", - "Oliphaunt Android runtime arm64-v8a", - "Package-managed liboliphaunt Android runtime for arm64-v8a app builds.", - ), - ( - "liboliphaunt-android-x86_64", - f"liboliphaunt-{version}-android-x86_64.tar.gz", - "Oliphaunt Android runtime x86_64", - "Package-managed liboliphaunt Android runtime for x86_64 app builds.", - ), - ] rows = [] - for artifact_id, filename, name, description in assets: + for coordinate in product_metadata.registry_package_names("liboliphaunt-native", "maven"): + group_id, artifact_id = split_maven_coordinate(coordinate) + if group_id != "dev.oliphaunt.runtime": + fail(f"liboliphaunt-native Maven artifact {coordinate} must use dev.oliphaunt.runtime") + artifact = RUNTIME_MAVEN_ARTIFACTS.get(artifact_id) + if artifact is None: + fail(f"liboliphaunt-native Maven artifact {coordinate} has no release asset mapping") + filename = artifact["filename"].format(version=version) rows.append( tsv_row( - group_id="dev.oliphaunt.runtime", + group_id=group_id, artifact_id=artifact_id, version=version, file=require_file(asset_root / filename, artifact_id), - name=name, - description=description, + name=artifact["name"], + description=artifact["description"], ) ) return rows diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index e54f72fd..1c6216e6 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -15,6 +15,7 @@ import optimize_native_runtime_payload import package_liboliphaunt_wasix_cargo_artifacts import product_metadata +import release ROOT = Path(__file__).resolve().parents[2] @@ -251,6 +252,35 @@ def validate_exact_extension_registry_shape(graph: dict) -> None: fail(f"{product} derived Android Maven targets are wrong: {sorted(android_targets)}") +def validate_publish_target_coverage(graph: dict) -> None: + workflow = read_text(".github/workflows/release.yml") + release_source = read_text("tools/release/release.py") + saw_extension = False + for product, config in product_metadata.graph_products(graph).items(): + declared = set(product_metadata.string_list(config, "publish_targets", product)) + supported = release.supported_publish_targets(product) + if declared != supported: + fail( + f"{product}.publish_targets must match release.py publish handler coverage: " + f"declared={sorted(declared)}, supported={sorted(supported)}" + ) + step_coverage = release.publish_step_target_coverage(product) + if release.is_extension_product(product): + saw_extension = True + continue + for step in step_coverage: + if f'product == "{product}" and step == "{step}"' not in release_source: + fail(f"release.py must dispatch publish step {product}:{step}") + if f"--product {product} --step {step}" not in workflow: + fail(f"Release workflow must invoke publish step {product}:{step}") + if saw_extension: + for step in ["github-release-assets", "maven-central"]: + if f'is_extension_product(product) and step == "{step}"' not in release_source: + fail(f"release.py must dispatch extension publish step {step}") + if f"--step {step} --products-json" not in workflow: + fail(f"Release workflow must invoke aggregate extension publish step {step}") + + def validate_release_setup_docs() -> None: setup = read_text("docs/maintainers/release-setup.md") normalized_setup = re.sub(r"\s+", " ", setup) @@ -605,6 +635,21 @@ def validate_kotlin(kotlin_version: str, liboliphaunt_version: str) -> None: "dev.oliphaunt.runtime:oliphaunt-icu", "Kotlin README must document the optional ICU Maven artifact", ) + require_text( + "tools/release/release.py", + 'product_metadata.registry_package_names("oliphaunt-kotlin", "maven")', + "Kotlin Maven release idempotency probes must derive package coordinates from release metadata", + ) + reject_text( + "tools/release/release.py", + "https://repo1.maven.org/maven2/dev/oliphaunt/oliphaunt/", + "Kotlin Maven release idempotency probes must not hard-code package coordinates", + ) + require_text( + "tools/release/build_maven_artifact_manifest.py", + 'product_metadata.registry_package_names("liboliphaunt-native", "maven")', + "Native runtime Maven artifact manifests must derive package coordinates from release metadata", + ) android_resolver = ( "src/sdks/kotlin/oliphaunt-android-gradle-plugin/src/main/java/dev/oliphaunt/android/ResolveOliphauntAndroidAssetsTask.java" ) @@ -1287,6 +1332,7 @@ def main() -> int: graph = load_graph() validate_graph_files(graph) validate_exact_extension_registry_shape(graph) + validate_publish_target_coverage(graph) validate_release_setup_docs() validate_local_registry_publisher() diff --git a/tools/release/product_metadata.py b/tools/release/product_metadata.py index 1c0e2247..8b534a4c 100644 --- a/tools/release/product_metadata.py +++ b/tools/release/product_metadata.py @@ -313,6 +313,23 @@ def string_list(config: dict, key: str, product: str) -> list[str]: return value +def registry_package_names(product: str, package_kind: str) -> list[str]: + names: list[str] = [] + for raw in string_list(product_config(product), "registry_packages", product): + kind, separator, name = raw.partition(":") + if not separator or not kind or not name: + fail(f"{product}.registry_packages entry {raw!r} must use kind:name") + if kind == package_kind: + names.append(name) + duplicates = sorted({name for name in names if names.count(name) > 1}) + if duplicates: + fail( + f"{product} declares duplicate {package_kind} registry packages: " + + ", ".join(duplicates) + ) + return names + + def _string_field(config: dict[str, Any], key: str, context: str) -> str: value = config.get(key) if not isinstance(value, str) or not value: diff --git a/tools/release/release.py b/tools/release/release.py index f1fdea9f..2c7c6fa4 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -380,6 +380,60 @@ def selected_extension_products(products: list[str]) -> list[str]: return sorted(product for product in products if is_extension_product(product)) +def publish_step_target_coverage(product: str) -> dict[str, set[str]]: + if is_extension_product(product): + return { + "github-release-assets": {"github-release-assets"}, + "maven-central": {"maven-central"}, + } + return { + "liboliphaunt-native": { + "github-release-assets": {"github-release-assets"}, + "npm": {"npm"}, + "maven-central": {"maven-central"}, + "crates-io": {"crates-io"}, + }, + "liboliphaunt-wasix": { + "github-release-assets": {"github-release-assets"}, + "crates-io": {"crates-io"}, + }, + "oliphaunt-broker": { + "github-release-assets": {"github-release-assets"}, + "crates-io": {"crates-io"}, + "npm": {"npm"}, + }, + "oliphaunt-js": { + "npm-jsr": {"npm", "jsr"}, + }, + "oliphaunt-kotlin": { + "maven-central": {"maven-central"}, + }, + "oliphaunt-node-direct": { + "github-release-assets": {"github-release-assets"}, + "npm": {"npm"}, + }, + "oliphaunt-react-native": { + "npm": {"npm"}, + }, + "oliphaunt-rust": { + "crates-io": {"crates-io"}, + }, + "oliphaunt-swift": { + "github-release": {"github-release", "swift-package-source-tag"}, + }, + "oliphaunt-wasix-rust": { + "crates-io": {"crates-io"}, + }, + }.get(product, {}) + + +def supported_publish_targets(product: str) -> set[str]: + covered: set[str] = set() + for targets in publish_step_target_coverage(product).values(): + covered.update(targets) + return covered + + def extension_sql_name(product: str) -> str: config = product_metadata.product_config(product) value = config.get("extension_sql_name") @@ -538,18 +592,18 @@ def cargo_publish_manifest(package: str, version: str, manifest_path: Path, *, a def cargo_registry_packages(product: str) -> list[str]: - config = product_metadata.product_config(product) - packages = config.get("registry_packages", []) - if not isinstance(packages, list): - fail(f"{product}.registry_packages must be a list") - crates = sorted( - package.split(":", 1)[1] - for package in packages - if isinstance(package, str) and package.startswith("crates:") + return sorted(product_metadata.registry_package_names(product, "crates")) + + +def maven_pom_url(coordinate: str, version: str) -> str: + group_id, separator, artifact_id = coordinate.partition(":") + if not separator or not group_id or not artifact_id: + fail(f"invalid Maven coordinate {coordinate!r}; expected group:artifact") + group_path = group_id.replace(".", "/") + return ( + f"https://repo1.maven.org/maven2/{group_path}/{artifact_id}/" + f"{version}/{artifact_id}-{version}.pom" ) - if len(crates) != len(set(crates)): - fail(f"{product} declares duplicate Cargo registry packages: {crates}") - return crates def rust_artifact_cargo_target_cfg(target: artifact_targets.ArtifactTarget) -> str: @@ -1738,12 +1792,10 @@ def publish_swift_release(head_ref: str) -> None: def kotlin_artifacts_published(version: str) -> bool: - urls = [ - f"https://repo1.maven.org/maven2/dev/oliphaunt/oliphaunt/{version}/oliphaunt-{version}.pom", - f"https://repo1.maven.org/maven2/dev/oliphaunt/oliphaunt-android-gradle-plugin/{version}/oliphaunt-android-gradle-plugin-{version}.pom", - f"https://repo1.maven.org/maven2/dev/oliphaunt/android/dev.oliphaunt.android.gradle.plugin/{version}/dev.oliphaunt.android.gradle.plugin-{version}.pom", - ] - return all(url_exists(url) for url in urls) + return all( + url_exists(maven_pom_url(coordinate, version)) + for coordinate in product_metadata.registry_package_names("oliphaunt-kotlin", "maven") + ) def publish_kotlin_maven(head_ref: str) -> None: From 39b051a9b4d3e6e266792f2bb6e64401721f2c72 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 07:32:09 +0000 Subject: [PATCH 044/137] fix: align split runtime registry validation --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 15 ++++- .../oliphaunt-wasix/src/oliphaunt/aot.rs | 10 ++- src/sdks/kotlin/oliphaunt/build.gradle.kts | 32 ++++++---- .../oliphaunt/AndroidNativeDirectEngine.kt | 2 + .../kotlin/dev/oliphaunt/OliphauntAndroid.kt | 2 + .../OliphauntAndroidRuntimeAssets.kt | 64 +++++++++++++++++-- src/sdks/react-native/android/build.gradle | 40 +++++++----- .../oliphaunt/reactnative/OliphauntModule.kt | 5 ++ src/sdks/react-native/tools/check-sdk.sh | 1 + .../check-sdk-mobile-extension-surface.sh | 18 ++++++ tools/release/check_release_metadata.py | 47 +++++++++++++- 11 files changed, 198 insertions(+), 38 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index fcbe6cf6..5f0f0e9b 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -28,7 +28,7 @@ review production pipelines, then normalize implementation details. - [ ] Verify package naming is symmetric across native and WASIX, with `wasix` special-cased rather than `native`. - [x] Verify native runtime payloads contain `postgres`, `initdb`, `pg_ctl`; native tools payloads contain `pg_dump`, `psql`. - [x] Verify WASIX runtime payloads contain `postgres`, `initdb`; WASIX tools payloads contain `pg_dump`, `psql`, not `pg_ctl`. -- [ ] Verify extension packages and runtime tools are published and installed from registries idiomatically. +- [x] Verify extension packages and runtime tools are published and installed from registries idiomatically. - [x] Derive or validate native Maven runtime package manifests and Kotlin Maven existing-version probes from release metadata. - [x] Add a publish-target coverage check that every declared registry/release target has release publication handling and a Release workflow invocation. - [x] Derive or policy-check the WASIX runtime/tools AOT Cargo package maps from the public WASIX package graph. @@ -139,6 +139,11 @@ review production pipelines, then normalize implementation details. The same packager helper also drives the WASIX AOT target-cfg dependency maps and `tools` feature dependency expectations used by release metadata, consumer-shape, and release publication checks. +- WASIX runtime and tools source crates keep `publish = false` as a + source-tree guard, but the release Cargo artifact packager removes it from + staged manifests before publishing. Release metadata now checks that behavior, + so `oliphaunt-wasix-tools` and tools-AOT crates remain registry-publishable + while `oliphaunt-wasix` installs them through optional dependencies. - SDK CI package artifact names now derive from release products marked `kind = "sdk"`. The release workflow and local registry publisher use `release.py ci-artifacts --family sdk-package` instead of repeating @@ -162,6 +167,14 @@ review production pipelines, then normalize implementation details. passed using the checked-in Gradle wrapper. The lane exercised the positive split/prebuilt runtime resource paths and the negative selected-extension missing-SQL diagnostics. +- On 2026-06-26, local Android validation used `target/android-sdk` with + Android platform 36, build tools 35/36, CMake 3.22.1, NDK 27.0.12077973, + command-line tools, and Java 17. Kotlin `test-unit` passed against that SDK. + The React Native Android bridge local-registry lane also passed after + aligning Gradle property lookup so both canonical lower-case + `-Poliphaunt...` properties and the existing capitalized spellings resolve, + and after enabling packaged runtime mode for the static-extension link + evidence assertion. - Swift runtime-resource package-kind rejection now has an executable `@Test` annotation, and release metadata plus consumer-shape checks guard against regressing it to an unannotated helper. diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs index 2d27e0d0..10daf8f6 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs @@ -515,9 +515,13 @@ fn merge_extension_aot_manifests(_manifest: &mut AotManifest) -> Result<()> { { let manifest = _manifest; for sql_name in liboliphaunt_wasix_portable::SELECTED_EXTENSION_SQL_NAMES { - let Some(json) = assets::extension_aot_manifest_json(target_triple(), sql_name) else { - continue; - }; + let json = assets::extension_aot_manifest_json(target_triple(), sql_name) + .with_context(|| { + format!( + "missing package-manager-resolved AOT manifest for selected extension '{sql_name}' on target {}", + target_triple(), + ) + })?; let extension_manifest: AotManifest = serde_json::from_str(json).with_context(|| { format!( diff --git a/src/sdks/kotlin/oliphaunt/build.gradle.kts b/src/sdks/kotlin/oliphaunt/build.gradle.kts index b9f474cc..08de02ae 100644 --- a/src/sdks/kotlin/oliphaunt/build.gradle.kts +++ b/src/sdks/kotlin/oliphaunt/build.gradle.kts @@ -114,6 +114,12 @@ val explicitPublicationSigning = .map { it.equals("true", ignoreCase = true) || it.equals("yes", ignoreCase = true) || it == "1" } .orElse(false) +fun oliphauntProperty(name: String): Any? = + project.findProperty(name) + ?: name + .takeIf { it.startsWith("oliphaunt") } + ?.let { project.findProperty("O${it.drop(1)}") } + mavenPublishing { publishToMavenCentral(automaticRelease = true) if (mavenCentralPublishRequested || explicitPublicationSigning.get()) { @@ -154,7 +160,7 @@ val generatedAndroidAssetsDir = layout.buildDirectory.dir("generated/oliphaunt-a val generatedAndroidJniLibsDir = layout.buildDirectory.dir("generated/oliphaunt-android-jniLibs") val configuredCxxBuildRoot = ( - project.findProperty("oliphauntCxxBuildRoot") + oliphauntProperty("oliphauntCxxBuildRoot") ?: System.getenv("OLIPHAUNT_CXX_BUILD_ROOT") )?.toString() ?.takeIf(String::isNotBlank) @@ -168,54 +174,54 @@ val cxxBuildRoot = .asFile val packagedRuntimeResourcesDir = ( - project.findProperty("oliphauntRuntimeResourcesDir") + oliphauntProperty("oliphauntRuntimeResourcesDir") ?: System.getenv("OLIPHAUNT_KOTLIN_ANDROID_RUNTIME_RESOURCES_DIR") ?: System.getenv("OLIPHAUNT_ANDROID_RUNTIME_RESOURCES_DIR") )?.toString() val packagedAndroidJniLibsDir = ( - project.findProperty("oliphauntAndroidJniLibsDir") + oliphauntProperty("oliphauntAndroidJniLibsDir") ?: System.getenv("OLIPHAUNT_KOTLIN_ANDROID_JNI_LIBS_DIR") )?.toString() val packagedAndroidExtensionArchivesDir = ( - project.findProperty("oliphauntAndroidExtensionArchivesDir") - ?: project.findProperty("oliphauntExtensionArchivesDir") + oliphauntProperty("oliphauntAndroidExtensionArchivesDir") + ?: oliphauntProperty("oliphauntExtensionArchivesDir") ?: System.getenv("OLIPHAUNT_KOTLIN_ANDROID_EXTENSION_ARCHIVES_DIR") ?: System.getenv("OLIPHAUNT_ANDROID_EXTENSION_ARCHIVES_DIR") )?.toString() val packagedAndroidLinkEvidenceFile = ( - project.findProperty("oliphauntAndroidLinkEvidenceFile") + oliphauntProperty("oliphauntAndroidLinkEvidenceFile") ?: System.getenv("OLIPHAUNT_KOTLIN_ANDROID_LINK_EVIDENCE_FILE") ?: System.getenv("OLIPHAUNT_ANDROID_LINK_EVIDENCE_FILE") )?.toString() val explicitPackagedRuntimeDir = ( - project.findProperty("oliphauntRuntimeDir") + oliphauntProperty("oliphauntRuntimeDir") ?: System.getenv("OLIPHAUNT_KOTLIN_ANDROID_RUNTIME_DIR") )?.toString() val explicitPackagedTemplatePgdataDir = ( - project.findProperty("oliphauntTemplatePgdataDir") + oliphauntProperty("oliphauntTemplatePgdataDir") ?: System.getenv("OLIPHAUNT_KOTLIN_ANDROID_TEMPLATE_PGDATA_DIR") )?.toString() val explicitPackagedExtensionsRaw = ( - project.findProperty("oliphauntExtensions") + oliphauntProperty("oliphauntExtensions") ?: System.getenv("OLIPHAUNT_KOTLIN_ANDROID_EXTENSIONS") )?.toString() val explicitMobileStaticModulesRaw = ( - project.findProperty("oliphauntMobileStaticModules") - ?: project.findProperty("oliphauntMobileStaticModuleStems") + oliphauntProperty("oliphauntMobileStaticModules") + ?: oliphauntProperty("oliphauntMobileStaticModuleStems") ?: System.getenv("OLIPHAUNT_KOTLIN_ANDROID_MOBILE_STATIC_MODULES") ?: System.getenv("OLIPHAUNT_KOTLIN_ANDROID_MOBILE_STATIC_MODULE_STEMS") )?.toString() val explicitAndroidAbiFiltersRaw = ( - project.findProperty("oliphauntAndroidAbiFilters") - ?: project.findProperty("oliphauntAndroidAbis") + oliphauntProperty("oliphauntAndroidAbiFilters") + ?: oliphauntProperty("oliphauntAndroidAbis") ?: System.getenv("OLIPHAUNT_KOTLIN_ANDROID_ABI_FILTERS") ?: System.getenv("OLIPHAUNT_ANDROID_ABI_FILTERS") )?.toString() diff --git a/src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/AndroidNativeDirectEngine.kt b/src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/AndroidNativeDirectEngine.kt index 9c401709..3dbdf721 100644 --- a/src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/AndroidNativeDirectEngine.kt +++ b/src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/AndroidNativeDirectEngine.kt @@ -16,6 +16,7 @@ public class AndroidNativeDirectEngine( context: Context, private val libraryPath: String? = null, private val runtimeDirectory: String? = null, + private val resourceRoot: File? = null, private val username: String = "postgres", private val database: String = "postgres", ) : OliphauntEngine { @@ -42,6 +43,7 @@ public class AndroidNativeDirectEngine( ?: env("OLIPHAUNT_INSTALL_DIR") ?: env("OLIPHAUNT_RUNTIME_DIR"), requestedExtensions = config.extensions, + resourceRoot = resourceRoot, ) val root = config.root?.let(::File) diff --git a/src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/OliphauntAndroid.kt b/src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/OliphauntAndroid.kt index 6c9b0366..5211abf6 100644 --- a/src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/OliphauntAndroid.kt +++ b/src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/OliphauntAndroid.kt @@ -18,6 +18,7 @@ public object OliphauntAndroid { config: OliphauntConfig = OliphauntConfig(), libraryPath: String? = null, runtimeDirectory: String? = null, + resourceRoot: File? = null, username: String = "postgres", database: String = "postgres", ): OliphauntDatabase = OliphauntDatabase.open( @@ -27,6 +28,7 @@ public object OliphauntAndroid { context = context, libraryPath = libraryPath, runtimeDirectory = runtimeDirectory, + resourceRoot = resourceRoot, username = username, database = database, ), diff --git a/src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssets.kt b/src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssets.kt index 4e8cfe9c..c3408fa9 100644 --- a/src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssets.kt +++ b/src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssets.kt @@ -10,6 +10,7 @@ import java.util.Properties internal data class OliphauntAndroidAssetPackage( val assetRoot: String, val cacheKey: String, + val resourceRoot: File? = null, val extensions: Set = emptySet(), val runtimeFeatures: Set = emptySet(), val sharedPreloadLibraries: Set = emptySet(), @@ -77,10 +78,21 @@ internal object OliphauntAndroidRuntimeAssets { context: Context, explicitRuntimeDirectory: String?, requestedExtensions: Collection = emptyList(), + resourceRoot: File? = null, ): OliphauntAndroidResolvedRuntime { val requestedExtensionSet = validateExtensionIds(requestedExtensions) - val templatePgdata = packageManifestOrNull(context.assets, TEMPLATE_PGDATA_ASSET_ROOT) - val packagedRuntime = packageManifestOrNull(context.assets, RUNTIME_ASSET_ROOT) + val templatePgdata = + if (resourceRoot == null) { + packageManifestOrNull(context.assets, TEMPLATE_PGDATA_ASSET_ROOT) + } else { + filePackageManifestOrNull(resourceRoot, TEMPLATE_PGDATA_ASSET_ROOT) + } + val packagedRuntime = + if (resourceRoot == null) { + packageManifestOrNull(context.assets, RUNTIME_ASSET_ROOT) + } else { + filePackageManifestOrNull(resourceRoot, RUNTIME_ASSET_ROOT) + } val usePackagedRuntime = explicitRuntimeDirectory?.takeIf(String::isNotEmpty) == null val runtimeDirectory = explicitRuntimeDirectory?.takeIf(String::isNotEmpty) @@ -152,7 +164,7 @@ internal object OliphauntAndroidRuntimeAssets { val temp = File(parent, ".pgdata-template-${templatePgdata.cacheKey}-${System.nanoTime()}") temp.deleteRecursively() try { - copyAssetTree(assetManager, "${templatePgdata.assetRoot}/$FILES_DIR_NAME", temp) + copyPackageTree(assetManager, templatePgdata, temp) ensureTemplatePgdataDirectoriesForAndroid(temp) normalizeTemplatePgdataForAndroid(temp) if (!File(temp, "PG_VERSION").isFile) { @@ -211,6 +223,7 @@ internal object OliphauntAndroidRuntimeAssets { internal fun parseManifestProperties( assetRoot: String, properties: Properties, + resourceRoot: File? = null, ): OliphauntAndroidAssetPackage { val schema = properties.getProperty("schema")?.trim().orEmpty() if (schema != RUNTIME_RESOURCES_SCHEMA) { @@ -272,6 +285,7 @@ internal object OliphauntAndroidRuntimeAssets { return OliphauntAndroidAssetPackage( assetRoot = assetRoot, cacheKey = cacheKey, + resourceRoot = resourceRoot, extensions = extensions, runtimeFeatures = runtimeFeatures, sharedPreloadLibraries = sharedPreloadLibraries, @@ -292,7 +306,7 @@ internal object OliphauntAndroidRuntimeAssets { } val properties = Properties() manifest.inputStream().use(properties::load) - return parseManifestProperties(assetRoot, properties) + return parseManifestProperties(assetRoot, properties, resourceRoot = resourceRoot) } private fun OliphauntPackageSizeReport.withRuntimeManifest(runtime: OliphauntAndroidAssetPackage?): OliphauntPackageSizeReport = if (runtime == null) { @@ -680,7 +694,7 @@ internal object OliphauntAndroidRuntimeAssets { val temp = File(parent, ".${target.name}.tmp-${System.nanoTime()}") temp.deleteRecursively() try { - copyAssetTree(assetManager, "${assetPackage.assetRoot}/$FILES_DIR_NAME", temp) + copyPackageTree(assetManager, assetPackage, temp) markRuntimeExecutablePlaceholders(temp) File(temp, STAMP_NAME).writeText(assetPackage.cacheKey) if (target.exists()) { @@ -695,6 +709,19 @@ internal object OliphauntAndroidRuntimeAssets { } } + private fun copyPackageTree( + assetManager: AssetManager, + assetPackage: OliphauntAndroidAssetPackage, + destination: File, + ) { + val resourceRoot = assetPackage.resourceRoot + if (resourceRoot == null) { + copyAssetTree(assetManager, "${assetPackage.assetRoot}/$FILES_DIR_NAME", destination) + } else { + copyFileTree(File(resourceRoot, "${assetPackage.assetRoot}/$FILES_DIR_NAME"), destination) + } + } + private fun markRuntimeExecutablePlaceholders(root: File) { val postgres = File(root, "bin/postgres") if (postgres.isFile) { @@ -732,6 +759,33 @@ internal object OliphauntAndroidRuntimeAssets { } } + private fun copyFileTree( + source: File, + destination: File, + ) { + if (!source.exists()) { + throw OliphauntException("missing Oliphaunt resource path ${source.absolutePath}") + } + if (source.isFile) { + destination.parentFile?.mkdirs() + source.inputStream().use { input -> + destination.outputStream().use { output -> + input.copyTo(output) + } + } + return + } + if (!source.isDirectory) { + throw OliphauntException("Oliphaunt resource path is not a file or directory: ${source.absolutePath}") + } + if (!destination.mkdirs() && !destination.isDirectory) { + throw OliphauntException("failed to create directory ${destination.absolutePath}") + } + source.listFiles().orEmpty().sortedBy(File::getName).forEach { child -> + copyFileTree(child, File(destination, child.name)) + } + } + private fun File.readTextOrNull(): String? = try { if (isFile) readText() else null } catch (_: IOException) { diff --git a/src/sdks/react-native/android/build.gradle b/src/sdks/react-native/android/build.gradle index 5ace55c6..29710890 100644 --- a/src/sdks/react-native/android/build.gradle +++ b/src/sdks/react-native/android/build.gradle @@ -68,6 +68,16 @@ if (reactNativeDir == null || reactNativeCodegenDir == null) { ) } def nodeExecutable = (project.findProperty("nodeExecutable") ?: System.getenv("NODE_BINARY") ?: "node").toString() +def oliphauntProperty = { String name -> + def value = project.findProperty(name) + if (value != null) { + return value + } + if (name.startsWith("oliphaunt")) { + return project.findProperty("O${name.substring(1)}") + } + return null +} def generatedCodegenDir = file("${buildDir}/generated/source/codegen") def generatedCodegenSchema = file("${generatedCodegenDir}/schema.json") @@ -82,17 +92,17 @@ def kotlinSdkVersion = ( ).toString() def generatedOliphauntAssetsDir = file("${buildDir}/generated/liboliphaunt-assets") def generatedOliphauntJniLibsDir = file("${buildDir}/generated/liboliphaunt-jniLibs") -def configuredCxxBuildRoot = project.findProperty("oliphauntCxxBuildRoot") ?: System.getenv("OLIPHAUNT_CXX_BUILD_ROOT") +def configuredCxxBuildRoot = oliphauntProperty("oliphauntCxxBuildRoot") ?: System.getenv("OLIPHAUNT_CXX_BUILD_ROOT") def cxxBuildRoot = configuredCxxBuildRoot == null || configuredCxxBuildRoot.toString().isBlank() ? file("${layout.buildDirectory.get().asFile}/cxx") : new File(file(configuredCxxBuildRoot), project.path == ":" ? "root" : project.path.substring(1).replace(":", "/")) def localKotlinSdkProject = findProject(":oliphaunt") def kotlinSdkDependency = (project.findProperty("liboliphauntKotlinSdkDependency") ?: System.getenv("OLIPHAUNT_REACT_NATIVE_KOTLIN_SDK_DEPENDENCY"))?.toString() ?: "dev.oliphaunt:oliphaunt:${kotlinSdkVersion}" -def kotlinSdkMavenRepository = (project.findProperty("oliphauntKotlinSdkMavenRepository") ?: System.getenv("OLIPHAUNT_REACT_NATIVE_KOTLIN_SDK_MAVEN_REPOSITORY"))?.toString() +def kotlinSdkMavenRepository = (oliphauntProperty("oliphauntKotlinSdkMavenRepository") ?: System.getenv("OLIPHAUNT_REACT_NATIVE_KOTLIN_SDK_MAVEN_REPOSITORY"))?.toString() ?.trim() def boolOption = { String propertyName, String environmentName, boolean defaultValue -> - def raw = project.findProperty(propertyName) ?: System.getenv(environmentName) + def raw = oliphauntProperty(propertyName) ?: System.getenv(environmentName) if (raw == null || raw.toString().isBlank()) { return defaultValue } @@ -117,27 +127,27 @@ def packagesAndroidRuntimeInReactNative = boolOption( false ) def packagedRuntimeResourcesDir = ( - project.findProperty("oliphauntRuntimeResourcesDir") + oliphauntProperty("oliphauntRuntimeResourcesDir") ?: System.getenv("OLIPHAUNT_REACT_NATIVE_ANDROID_RUNTIME_RESOURCES_DIR") ?: System.getenv("OLIPHAUNT_ANDROID_RUNTIME_RESOURCES_DIR") )?.toString() -def packagedAndroidJniLibsDir = (project.findProperty("oliphauntAndroidJniLibsDir") ?: System.getenv("OLIPHAUNT_REACT_NATIVE_ANDROID_JNI_LIBS_DIR"))?.toString() +def packagedAndroidJniLibsDir = (oliphauntProperty("oliphauntAndroidJniLibsDir") ?: System.getenv("OLIPHAUNT_REACT_NATIVE_ANDROID_JNI_LIBS_DIR"))?.toString() def packagedAndroidExtensionArchivesDir = ( - project.findProperty("oliphauntAndroidExtensionArchivesDir") - ?: project.findProperty("oliphauntExtensionArchivesDir") + oliphauntProperty("oliphauntAndroidExtensionArchivesDir") + ?: oliphauntProperty("oliphauntExtensionArchivesDir") ?: System.getenv("OLIPHAUNT_REACT_NATIVE_ANDROID_EXTENSION_ARCHIVES_DIR") ?: System.getenv("OLIPHAUNT_ANDROID_EXTENSION_ARCHIVES_DIR") )?.toString() def packagedAndroidLinkEvidenceFile = ( - project.findProperty("oliphauntAndroidLinkEvidenceFile") + oliphauntProperty("oliphauntAndroidLinkEvidenceFile") ?: System.getenv("OLIPHAUNT_REACT_NATIVE_ANDROID_LINK_EVIDENCE_FILE") )?.toString() -def explicitPackagedRuntimeDir = (project.findProperty("oliphauntRuntimeDir") ?: System.getenv("OLIPHAUNT_REACT_NATIVE_ANDROID_RUNTIME_DIR"))?.toString() -def explicitPackagedTemplatePgdataDir = (project.findProperty("oliphauntTemplatePgdataDir") ?: System.getenv("OLIPHAUNT_REACT_NATIVE_ANDROID_TEMPLATE_PGDATA_DIR"))?.toString() -def explicitPackagedExtensionsRaw = (project.findProperty("oliphauntExtensions") ?: System.getenv("OLIPHAUNT_REACT_NATIVE_ANDROID_EXTENSIONS"))?.toString() +def explicitPackagedRuntimeDir = (oliphauntProperty("oliphauntRuntimeDir") ?: System.getenv("OLIPHAUNT_REACT_NATIVE_ANDROID_RUNTIME_DIR"))?.toString() +def explicitPackagedTemplatePgdataDir = (oliphauntProperty("oliphauntTemplatePgdataDir") ?: System.getenv("OLIPHAUNT_REACT_NATIVE_ANDROID_TEMPLATE_PGDATA_DIR"))?.toString() +def explicitPackagedExtensionsRaw = (oliphauntProperty("oliphauntExtensions") ?: System.getenv("OLIPHAUNT_REACT_NATIVE_ANDROID_EXTENSIONS"))?.toString() def explicitMobileStaticModulesRaw = ( - project.findProperty("oliphauntMobileStaticModules") - ?: project.findProperty("oliphauntMobileStaticModuleStems") + oliphauntProperty("oliphauntMobileStaticModules") + ?: oliphauntProperty("oliphauntMobileStaticModuleStems") ?: System.getenv("OLIPHAUNT_REACT_NATIVE_ANDROID_MOBILE_STATIC_MODULES") ?: System.getenv("OLIPHAUNT_REACT_NATIVE_ANDROID_MOBILE_STATIC_MODULE_STEMS") )?.toString() @@ -237,8 +247,8 @@ def parseExtensions = { String raw -> def packagedExtensions = parseExtensions(packagedExtensionsRaw) def packagedMobileStaticModules = parsePortableList(packagedMobileStaticModulesRaw, "mobile static module stem") def explicitAndroidAbiFiltersRaw = ( - project.findProperty("oliphauntAndroidAbiFilters") - ?: project.findProperty("oliphauntAndroidAbis") + oliphauntProperty("oliphauntAndroidAbiFilters") + ?: oliphauntProperty("oliphauntAndroidAbis") ?: System.getenv("OLIPHAUNT_REACT_NATIVE_ANDROID_ABI_FILTERS") ?: System.getenv("OLIPHAUNT_ANDROID_ABI_FILTERS") )?.toString() diff --git a/src/sdks/react-native/android/src/main/java/dev/oliphaunt/reactnative/OliphauntModule.kt b/src/sdks/react-native/android/src/main/java/dev/oliphaunt/reactnative/OliphauntModule.kt index 72b7ed8c..2d7deac0 100644 --- a/src/sdks/react-native/android/src/main/java/dev/oliphaunt/reactnative/OliphauntModule.kt +++ b/src/sdks/react-native/android/src/main/java/dev/oliphaunt/reactnative/OliphauntModule.kt @@ -98,6 +98,7 @@ class OliphauntModule( config = openConfig.config, libraryPath = openConfig.libraryPath, runtimeDirectory = openConfig.runtimeDirectory, + resourceRoot = openConfig.resourceRoot?.let(::File), username = openConfig.username, database = openConfig.database, ) @@ -285,6 +286,7 @@ class OliphauntModule( } val runtimeDirectory = reactNativeRuntimeDirectory(config.pathOverride("runtimeDirectory")) val libraryPath = reactNativeLibraryPath(config.pathOverride("libraryPath")) + val resourceRoot = config.pathOverride("resourceRoot") val username = config.startupIdentity("username") val database = config.startupIdentity("database") @@ -301,6 +303,7 @@ class OliphauntModule( ), libraryPath = libraryPath, runtimeDirectory = runtimeDirectory, + resourceRoot = resourceRoot, username = username ?: "postgres", database = database ?: "postgres", ) @@ -310,6 +313,7 @@ class OliphauntModule( val config: OliphauntConfig, val libraryPath: String?, val runtimeDirectory: String?, + val resourceRoot: String?, val username: String, val database: String, ) { @@ -325,6 +329,7 @@ class OliphauntModule( config.extensions.joinToString(","), libraryPath.orEmpty(), runtimeDirectory.orEmpty(), + resourceRoot.orEmpty(), ).joinToString(separator = "\u001f") } diff --git a/src/sdks/react-native/tools/check-sdk.sh b/src/sdks/react-native/tools/check-sdk.sh index de30d753..4f9e62e9 100755 --- a/src/sdks/react-native/tools/check-sdk.sh +++ b/src/sdks/react-native/tools/check-sdk.sh @@ -907,6 +907,7 @@ REPORT "-PoliphauntRuntimeResourcesDir=$tmp_assets" \ "-PoliphauntAndroidJniLibsDir=$tmp_static_jni" \ "-PoliphauntAndroidAbiFilters=$android_smoke_abi" \ + "-PoliphauntReactNativePackageRuntime=true" \ "-PoliphauntAndroidLinkEvidenceFile=$android_link_evidence" \ $gradle_scratch_args \ $gradle_smoke_cache_args diff --git a/tools/policy/check-sdk-mobile-extension-surface.sh b/tools/policy/check-sdk-mobile-extension-surface.sh index c74a59c2..a6db4d2f 100755 --- a/tools/policy/check-sdk-mobile-extension-surface.sh +++ b/tools/policy/check-sdk-mobile-extension-surface.sh @@ -12,8 +12,16 @@ require_text src/sdks/kotlin/oliphaunt/build.gradle.kts "mobileStaticRegistryPen "Kotlin Android Gradle packaging must emit mobile static-registry metadata" require_text src/sdks/kotlin/oliphaunt/build.gradle.kts "sharedPreloadLibraries=" \ "Kotlin Android Gradle packaging must emit shared-preload metadata" +require_text src/sdks/kotlin/oliphaunt/build.gradle.kts "fun oliphauntProperty(name: String)" \ + "Kotlin Android Gradle packaging must accept canonical and existing capitalized Oliphaunt property spellings" +require_text src/sdks/kotlin/oliphaunt/build.gradle.kts 'project.findProperty("O${it.drop(1)}")' \ + "Kotlin Android Gradle packaging must keep backward-compatible capitalized Oliphaunt property lookup" require_text src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/AndroidNativeDirectEngine.kt "config.postgresStartupArgs(runtime.sharedPreloadLibraries)" \ "Kotlin Android native-direct startup must pass packaged shared-preload libraries to liboliphaunt" +require_text src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/OliphauntAndroid.kt "resourceRoot: File? = null" \ + "Kotlin Android open must expose an optional resourceRoot for local release-shaped runtime resources" +require_text src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/AndroidNativeDirectEngine.kt "resourceRoot = resourceRoot" \ + "Kotlin Android native-direct startup must pass explicit resourceRoot to runtime resource resolution" require_text src/sdks/kotlin/oliphaunt/build.gradle.kts "nativeModuleStems=" \ "Kotlin Android Gradle packaging must emit expected native module stems" require_text src/sdks/kotlin/oliphaunt/build.gradle.kts "generatedExtensionMetadata.from(layout.projectDirectory.file(\"src/generated/extensions.json\"))" \ @@ -74,6 +82,14 @@ require_text src/sdks/react-native/android/build.gradle "mobileStaticRegistryPen "React Native Android Gradle packaging must emit mobile static-registry metadata" require_text src/sdks/react-native/android/build.gradle "sharedPreloadLibraries=" \ "React Native Android Gradle packaging must emit shared-preload metadata" +require_text src/sdks/react-native/android/build.gradle "def oliphauntProperty = { String name ->" \ + "React Native Android Gradle packaging must accept canonical and existing capitalized Oliphaunt property spellings" +require_text src/sdks/react-native/android/build.gradle 'project.findProperty("O${name.substring(1)}")' \ + "React Native Android Gradle packaging must keep backward-compatible capitalized Oliphaunt property lookup" +require_text src/sdks/react-native/android/src/main/java/dev/oliphaunt/reactnative/OliphauntModule.kt "resourceRoot = openConfig.resourceRoot?.let(::File)" \ + "React Native Android open must forward resourceRoot to the Kotlin Android runtime resolver" +require_text src/sdks/react-native/android/src/main/java/dev/oliphaunt/reactnative/OliphauntModule.kt "resourceRoot.orEmpty()" \ + "React Native Android reopen keys must include resourceRoot so different resource sets are not aliased" require_text src/sdks/react-native/android/build.gradle "nativeModuleStems=" \ "React Native Android Gradle packaging must emit expected native module stems" require_text src/sdks/react-native/android/build.gradle "generatedExtensionMetadata.from(file(\"../src/generated/extensions.json\"))" \ @@ -102,6 +118,8 @@ require_text src/sdks/react-native/android/src/main/cpp/CMakeLists.txt "add_libr "React Native Android CMake must link a support library from prebuilt static extension archives" require_text src/sdks/react-native/android/src/main/cpp/CMakeLists.txt "oliphaunt_dependency_archives" \ "React Native Android CMake must link selected mobile static dependency archives" +require_text src/sdks/react-native/tools/check-sdk.sh "-PoliphauntReactNativePackageRuntime=true" \ + "React Native Android bridge check must enable packaged runtime mode when asserting static-extension link evidence" require_text src/sdks/react-native/android/build.gradle "resolveExtensionSelection" \ "React Native Android Gradle packaging must resolve exact extension selections" require_text src/sdks/react-native/README.md "published React Native artifact does not carry base \`liboliphaunt\`" \ diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 1c6216e6..b405adba 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -635,6 +635,26 @@ def validate_kotlin(kotlin_version: str, liboliphaunt_version: str) -> None: "dev.oliphaunt.runtime:oliphaunt-icu", "Kotlin README must document the optional ICU Maven artifact", ) + require_text( + "src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/OliphauntAndroid.kt", + "resourceRoot: File? = null", + "Kotlin Android open must expose optional resourceRoot for release-shaped local runtime resources", + ) + require_text( + "src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/AndroidNativeDirectEngine.kt", + "resourceRoot = resourceRoot", + "Kotlin Android native-direct engine must pass explicit resourceRoot into runtime resolution", + ) + require_text( + "src/sdks/kotlin/oliphaunt/build.gradle.kts", + "fun oliphauntProperty(name: String)", + "Kotlin Android Gradle packaging must accept canonical and existing capitalized Oliphaunt property spellings", + ) + require_text( + "src/sdks/kotlin/oliphaunt/build.gradle.kts", + 'project.findProperty("O${it.drop(1)}")', + "Kotlin Android Gradle packaging must keep backward-compatible capitalized Oliphaunt property lookup", + ) require_text( "tools/release/release.py", 'product_metadata.registry_package_names("oliphaunt-kotlin", "maven")', @@ -740,6 +760,26 @@ def validate_react_native(rn_version: str, swift_version: str, kotlin_version: s '?: "dev.oliphaunt:oliphaunt:${kotlinSdkVersion}"', "React Native Android package must default to the published Kotlin SDK Maven coordinate", ) + require_text( + "src/sdks/react-native/android/src/main/java/dev/oliphaunt/reactnative/OliphauntModule.kt", + "resourceRoot = openConfig.resourceRoot?.let(::File)", + "React Native Android open must forward resourceRoot to the Kotlin Android runtime resolver", + ) + require_text( + "src/sdks/react-native/android/src/main/java/dev/oliphaunt/reactnative/OliphauntModule.kt", + "resourceRoot.orEmpty()", + "React Native Android reopen keys must include resourceRoot", + ) + require_text( + "src/sdks/react-native/android/build.gradle", + "def oliphauntProperty = { String name ->", + "React Native Android Gradle packaging must accept canonical and existing capitalized Oliphaunt property spellings", + ) + require_text( + "src/sdks/react-native/android/build.gradle", + 'project.findProperty("O${name.substring(1)}")', + "React Native Android Gradle packaging must keep backward-compatible capitalized Oliphaunt property lookup", + ) for needle in [ 'validateSelectedExtensionFiles(new File(output, "oliphaunt/runtime/files"), selectedExtensions.get())', "validateSelectedExtensionFiles(filesDir, extensions)", @@ -757,6 +797,7 @@ def validate_react_native(rn_version: str, swift_version: str, kotlin_version: s "src/sdks/kotlin/gradlew", "react-native-split-incomplete-extension", "prebuilt runtime resources accepted a selected extension without packaged SQL files", + "-PoliphauntReactNativePackageRuntime=true", ]: require_text( "src/sdks/react-native/tools/check-sdk.sh", @@ -1249,8 +1290,9 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None != {"tool:pg_dump", "tool:psql"} or "split_runtime_tools_payload" not in wasix_packager_source or "split_aot_tools_payload" not in wasix_packager_source + or "text = re.sub(r'(?m)^publish = false\\n?', \"\", text)" not in wasix_packager_source ): - fail("WASIX Cargo artifact packager must split pg_dump/psql into tools crates while keeping only postgres/initdb in root runtime crates") + fail("WASIX Cargo artifact packager must split pg_dump/psql into publishable tools crates while keeping only postgres/initdb in root runtime crates") native_packager_source = read_text("tools/release/package_liboliphaunt_cargo_artifacts.py") if ( optimize_native_runtime_payload.NATIVE_RUNTIME_TOOL_STEMS != ("initdb", "pg_ctl", "postgres") @@ -1272,6 +1314,9 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None or "load_psql_module(&engine)" not in sdk_pg_dump_source ): fail("oliphaunt-wasix must expose an explicit split pg_dump/psql tools preflight that validates payload and AOT artifacts") + sdk_aot_source = read_text("src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs") + if "missing package-manager-resolved AOT manifest for selected extension" not in sdk_aot_source: + fail("oliphaunt-wasix must fail when a selected extension AOT manifest is missing for the target") aot_source = read_text("src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs") for cfg in expected_aot_dependencies: rust_cfg = cfg.removeprefix("cfg(").removesuffix(")") From 55740b35292c651565f4839d6711dec40bffff72 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 07:43:58 +0000 Subject: [PATCH 045/137] fix: derive maven runtime artifacts --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 21 +++-- .../release/build_maven_artifact_manifest.py | 84 +++++++++++++------ tools/release/check_consumer_shape.py | 29 +++++++ tools/release/check_release_metadata.py | 30 +++++++ 4 files changed, 132 insertions(+), 32 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 5f0f0e9b..b0711257 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -24,8 +24,8 @@ review production pipelines, then normalize implementation details. ## Priority 2: CI and Release Shape -- [ ] Map CI producer jobs to release package consumers for Cargo, npm, Maven, SwiftPM, and GitHub release assets. -- [ ] Verify package naming is symmetric across native and WASIX, with `wasix` special-cased rather than `native`. +- [x] Map CI producer jobs to release package consumers for Cargo, npm, Maven, SwiftPM, and GitHub release assets. +- [x] Verify package naming is symmetric across native and WASIX, with `wasix` special-cased rather than `native`. - [x] Verify native runtime payloads contain `postgres`, `initdb`, `pg_ctl`; native tools payloads contain `pg_dump`, `psql`. - [x] Verify WASIX runtime payloads contain `postgres`, `initdb`; WASIX tools payloads contain `pg_dump`, `psql`, not `pg_ctl`. - [x] Verify extension packages and runtime tools are published and installed from registries idiomatically. @@ -149,9 +149,20 @@ review production pipelines, then normalize implementation details. `release.py ci-artifacts --family sdk-package` instead of repeating per-product artifact names, and the WASIX Rust binding is normalized to the same SDK release kind. -- CI/release DRY audit still needs a pass over broader workflow topology string - checks to distinguish legitimate job-shape assertions from remaining copied - package-surface contracts. +- CI/release producer-to-consumer audit found no P0/P1 mapping gaps across + Cargo, npm, Maven, SwiftPM, or GitHub release assets. Existing + `release.py check`, artifact-target, release-metadata, consumer-shape, and + registry-publication checks cover the package surfaces. One P2 cleanup + remains: local-registry publish still has a small aggregate artifact-name + preset to compare more directly with CI upload producers. +- Native runtime Maven publication now derives runtime asset filenames from + `artifact_targets` instead of a static `RUNTIME_MAVEN_ARTIFACTS` table, and + release metadata rejects reintroducing that duplicate Maven package-surface + mapping. +- Exact-extension package naming is now policy-checked: native/mobile extension + registry packages stay target-suffixed without a `native` qualifier, while + generated WASIX extension crates use `oliphaunt-extension-*-wasix` and + `oliphaunt-extension-*-wasix-aot-*`. - Android split/local runtime packaging now validates selected extension control and versioned SQL files in the copied runtime tree before generated manifests can declare those extensions. The public Android Gradle resolver diff --git a/tools/release/build_maven_artifact_manifest.py b/tools/release/build_maven_artifact_manifest.py index a39c1ff6..b3c1ac1c 100644 --- a/tools/release/build_maven_artifact_manifest.py +++ b/tools/release/build_maven_artifact_manifest.py @@ -8,6 +8,7 @@ from pathlib import Path from typing import NoReturn +import artifact_targets import extension_artifact_targets import product_metadata @@ -48,30 +49,6 @@ def tsv_row( return "\t".join(values) -RUNTIME_MAVEN_ARTIFACTS = { - "liboliphaunt-runtime-resources": { - "filename": "liboliphaunt-{version}-runtime-resources.tar.gz", - "name": "Oliphaunt runtime resources", - "description": "Package-managed Oliphaunt PostgreSQL runtime resources for Android app builds.", - }, - "oliphaunt-icu": { - "filename": "liboliphaunt-{version}-icu-data.tar.gz", - "name": "Oliphaunt ICU data", - "description": "Package-managed optional ICU data files for Oliphaunt app builds.", - }, - "liboliphaunt-android-arm64-v8a": { - "filename": "liboliphaunt-{version}-android-arm64-v8a.tar.gz", - "name": "Oliphaunt Android runtime arm64-v8a", - "description": "Package-managed liboliphaunt Android runtime for arm64-v8a app builds.", - }, - "liboliphaunt-android-x86_64": { - "filename": "liboliphaunt-{version}-android-x86_64.tar.gz", - "name": "Oliphaunt Android runtime x86_64", - "description": "Package-managed liboliphaunt Android runtime for x86_64 app builds.", - }, -} - - def split_maven_coordinate(coordinate: str) -> tuple[str, str]: group_id, separator, artifact_id = coordinate.partition(":") if not separator or not group_id or not artifact_id: @@ -79,23 +56,76 @@ def split_maven_coordinate(coordinate: str) -> tuple[str, str]: return group_id, artifact_id +def runtime_maven_artifact_id(target: artifact_targets.ArtifactTarget) -> str | None: + if target.kind == "runtime-resources": + return "liboliphaunt-runtime-resources" + if target.kind == "icu-data": + return "oliphaunt-icu" + if target.kind == "native-runtime" and target.target.startswith("android-"): + return f"liboliphaunt-{target.target}" + return None + + +def runtime_maven_artifact_metadata(target: artifact_targets.ArtifactTarget) -> tuple[str, str]: + if target.kind == "runtime-resources": + return ( + "Oliphaunt runtime resources", + "Package-managed Oliphaunt PostgreSQL runtime resources for Android app builds.", + ) + if target.kind == "icu-data": + return ( + "Oliphaunt ICU data", + "Package-managed optional ICU data files for Oliphaunt app builds.", + ) + if target.kind == "native-runtime" and target.target.startswith("android-"): + abi = target.target.removeprefix("android-") + return ( + f"Oliphaunt Android runtime {abi}", + f"Package-managed liboliphaunt Android runtime for {abi} app builds.", + ) + fail(f"unsupported liboliphaunt-native Maven artifact target {target.id}") + + +def runtime_maven_artifacts(version: str) -> dict[str, dict[str, str]]: + artifacts: dict[str, dict[str, str]] = {} + for target in artifact_targets.artifact_targets( + product="liboliphaunt-native", + surface="maven", + published_only=True, + ): + artifact_id = runtime_maven_artifact_id(target) + if artifact_id is None: + continue + if artifact_id in artifacts: + fail(f"duplicate liboliphaunt-native Maven artifact mapping for {artifact_id}") + name, description = runtime_maven_artifact_metadata(target) + artifacts[artifact_id] = { + "filename": target.asset_name(version), + "name": name, + "description": description, + } + if not artifacts: + fail("liboliphaunt-native artifact targets did not produce any Maven runtime artifacts") + return artifacts + + def runtime_rows(asset_root: Path) -> list[str]: version = product_metadata.read_current_version("liboliphaunt-native") + artifacts = runtime_maven_artifacts(version) rows = [] for coordinate in product_metadata.registry_package_names("liboliphaunt-native", "maven"): group_id, artifact_id = split_maven_coordinate(coordinate) if group_id != "dev.oliphaunt.runtime": fail(f"liboliphaunt-native Maven artifact {coordinate} must use dev.oliphaunt.runtime") - artifact = RUNTIME_MAVEN_ARTIFACTS.get(artifact_id) + artifact = artifacts.get(artifact_id) if artifact is None: fail(f"liboliphaunt-native Maven artifact {coordinate} has no release asset mapping") - filename = artifact["filename"].format(version=version) rows.append( tsv_row( group_id=group_id, artifact_id=artifact_id, version=version, - file=require_file(asset_root / filename, artifact_id), + file=require_file(asset_root / artifact["filename"], artifact_id), name=artifact["name"], description=artifact["description"], ) diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index f50bc699..f1dd3648 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -1902,6 +1902,35 @@ def check_exact_extension(findings: list[Finding], product: str) -> None: f"{package_path}/release.toml: native={sorted(native_targets)!r} wasix={sorted(wasix_targets)!r}", severity="P0", ) + wasix_package = package_liboliphaunt_wasix_cargo_artifacts.wasix_extension_package_name(product) + wasix_aot_packages = { + package_liboliphaunt_wasix_cargo_artifacts.wasix_extension_aot_package_name(product, target) + for target in package_liboliphaunt_wasix_cargo_artifacts.EXPECTED_EXTENSION_AOT_TARGETS + } + native_qualified_registry_packages = [ + package for package in product_registry_packages(product) if "-native-" in package + ] + require( + findings, + product, + "extension-package-naming", + "-native-" not in product + and not product.endswith("-native") + and not native_qualified_registry_packages + and all(not target.startswith("native-") for target in native_targets) + and all(target.startswith("wasix-") for target in wasix_targets) + and wasix_package == f"{product}-wasix" + and "-native-" not in wasix_package + and wasix_aot_packages + == { + f"{product}-wasix-aot-{target}" + for target in package_liboliphaunt_wasix_cargo_artifacts.EXPECTED_EXTENSION_AOT_TARGETS + } + and all("-native-" not in package for package in wasix_aot_packages), + "Exact-extension registry/package names must keep native targets platform-suffixed without a native qualifier and reserve the wasix qualifier for WASIX Cargo packages.", + f"{package_path}/release.toml registry={sorted(product_registry_packages(product))!r} wasix={wasix_package!r} wasix_aot={sorted(wasix_aot_packages)!r}", + severity="P0", + ) require( findings, product, diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index b405adba..3bbf733e 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -231,10 +231,18 @@ def validate_graph_files(graph: dict) -> None: def validate_exact_extension_registry_shape(graph: dict) -> None: for product in product_metadata.extension_product_ids(graph): config = product_metadata.product_config(product, graph) + if "-native-" in product or product.endswith("-native"): + fail(f"{product} exact-extension product names must stay platform-neutral; special-case wasix packages only") publish_targets = set(product_metadata.string_list(config, "publish_targets", product)) if not {"github-release-assets", "maven-central"}.issubset(publish_targets): fail(f"{product} must publish exact-extension GitHub assets and Android Maven artifacts") registry_packages = product_metadata.string_list(config, "registry_packages", product) + native_named_packages = sorted(package for package in registry_packages if "-native-" in package) + if native_named_packages: + fail( + f"{product} exact-extension registry package names must not include a native qualifier: " + + ", ".join(native_named_packages) + ) expected_registry_packages = { f"maven:dev.oliphaunt.extensions:{product}-{target.target}" for target in extension_artifact_targets.published_android_maven_targets(product) @@ -250,6 +258,18 @@ def validate_exact_extension_registry_shape(graph: dict) -> None: } if android_targets != {"android-arm64-v8a", "android-x86_64"}: fail(f"{product} derived Android Maven targets are wrong: {sorted(android_targets)}") + for target in extension_artifact_targets.artifact_targets(product=product, published_only=True): + if target.family == "native" and target.target.startswith("native-"): + fail(f"{product} native exact-extension target {target.target} must not repeat a native qualifier") + if target.family == "wasix" and not target.target.startswith("wasix-"): + fail(f"{product} WASIX exact-extension target {target.target} must carry the wasix qualifier") + wasix_package = package_liboliphaunt_wasix_cargo_artifacts.wasix_extension_package_name(product) + if wasix_package != f"{product}-wasix" or "-native-" in wasix_package: + fail(f"{product} WASIX extension Cargo package name must be {product}-wasix, got {wasix_package}") + for target in package_liboliphaunt_wasix_cargo_artifacts.EXPECTED_EXTENSION_AOT_TARGETS: + package = package_liboliphaunt_wasix_cargo_artifacts.wasix_extension_aot_package_name(product, target) + if package != f"{product}-wasix-aot-{target}" or "-native-" in package: + fail(f"{product} WASIX extension AOT Cargo package name is wrong: {package}") def validate_publish_target_coverage(graph: dict) -> None: @@ -670,6 +690,16 @@ def validate_kotlin(kotlin_version: str, liboliphaunt_version: str) -> None: 'product_metadata.registry_package_names("liboliphaunt-native", "maven")', "Native runtime Maven artifact manifests must derive package coordinates from release metadata", ) + require_text( + "tools/release/build_maven_artifact_manifest.py", + 'artifact_targets.artifact_targets(', + "Native runtime Maven artifact manifests must derive release asset filenames from artifact target metadata", + ) + reject_text( + "tools/release/build_maven_artifact_manifest.py", + "RUNTIME_MAVEN_ARTIFACTS", + "Native runtime Maven artifact manifests must not duplicate release asset filenames in a static Maven table", + ) android_resolver = ( "src/sdks/kotlin/oliphaunt-android-gradle-plugin/src/main/java/dev/oliphaunt/android/ResolveOliphauntAndroidAssetsTask.java" ) From fe12ea961447d367ef558fdc3f04cc13abaea87a Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 07:48:21 +0000 Subject: [PATCH 046/137] fix: derive local publish artifacts --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 16 ++++++++------ tools/release/artifact_targets.py | 22 +++++++++++++++++++ tools/release/check_release_metadata.py | 11 ++++++++++ tools/release/extension_artifact_targets.py | 22 +++++++++++++++++++ tools/release/local_registry_publish.py | 19 ++++++++-------- 5 files changed, 74 insertions(+), 16 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index b0711257..9885dc02 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -117,10 +117,12 @@ review production pipelines, then normalize implementation details. artifact-target checks, and release policy checks now derive native/helper target artifact names from `artifact_targets` instead of restating the platform list. -- The local-registry `local-publish` preset now also derives WASIX AOT runtime - artifact names from release target metadata and rejects duplicate artifact - names. The preset currently resolves 35 unique CI artifacts for local publish - staging. +- The local-registry `local-publish` preset now derives aggregate native/WASIX + runtime artifact names, WASIX portable runtime artifacts, WASIX exact-extension + target artifacts, exact-extension package artifacts, WASIX AOT runtime + artifacts, helper artifacts, node-direct npm artifacts, and SDK package + artifacts from release metadata helpers. The preset currently resolves 35 + unique CI artifacts for local publish staging and rejects duplicates. - Dead existing-tag release workflow probes were removed. Idempotent rerun behavior stays in the publish handlers that actually own registry/GitHub publication, such as matching GitHub asset checksum skips and already-published @@ -152,9 +154,9 @@ review production pipelines, then normalize implementation details. - CI/release producer-to-consumer audit found no P0/P1 mapping gaps across Cargo, npm, Maven, SwiftPM, or GitHub release assets. Existing `release.py check`, artifact-target, release-metadata, consumer-shape, and - registry-publication checks cover the package surfaces. One P2 cleanup - remains: local-registry publish still has a small aggregate artifact-name - preset to compare more directly with CI upload producers. + registry-publication checks cover the package surfaces. The local-registry + aggregate artifact-name preset was replaced with derived release metadata + helpers after the audit. - Native runtime Maven publication now derives runtime asset filenames from `artifact_targets` instead of a static `RUNTIME_MAVEN_ARTIFACTS` table, and release metadata rejects reintroducing that duplicate Maven package-surface diff --git a/tools/release/artifact_targets.py b/tools/release/artifact_targets.py index 6796e070..9c6cf8d1 100644 --- a/tools/release/artifact_targets.py +++ b/tools/release/artifact_targets.py @@ -685,6 +685,28 @@ def ci_wasix_aot_runtime_artifact_names() -> list[str]: return sorted(names) +def ci_aggregate_release_asset_artifact_name(product: str) -> str: + config = product_metadata.product_config(product) + release_artifacts = config.get("release_artifacts") + if not isinstance(release_artifacts, list) or not release_artifacts: + product_metadata.fail(f"{product} does not publish aggregate release assets") + return f"{product}-release-assets" + + +def ci_wasix_runtime_artifact_names() -> list[str]: + names = [ + f"liboliphaunt-wasix-runtime-{target.target}" + for target in artifact_targets( + product="liboliphaunt-wasix", + kind="wasix-runtime", + published_only=True, + ) + ] + if not names: + product_metadata.fail("liboliphaunt-wasix has no published WASIX runtime targets") + return sorted(names) + + def ci_sdk_package_artifact_name(product: str) -> str: config = product_metadata.product_config(product) if config.get("kind") != "sdk": diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 3bbf733e..c256d33f 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -368,6 +368,17 @@ def validate_local_registry_publisher() -> None: duplicates = sorted({artifact for artifact in artifacts if artifacts.count(artifact) > 1}) if duplicates: fail("local registry publish artifact preset must not contain duplicate names: " + ", ".join(duplicates)) + if "STATIC_LOCAL_PUBLISH_ARTIFACTS" in publisher: + fail("local registry publish preset must derive aggregate artifact names instead of keeping a static list") + if ( + "local_publish_aggregate_artifacts()" not in publisher + or "ci_aggregate_release_asset_artifact_name(\"liboliphaunt-native\")" not in publisher + or "ci_aggregate_release_asset_artifact_name(\"liboliphaunt-wasix\")" not in publisher + or "ci_wasix_runtime_artifact_names()" not in publisher + or "ci_wasix_extension_artifact_names()" not in publisher + or "ci_extension_package_artifact_names()" not in publisher + ): + fail("local registry publish preset must derive aggregate runtime and extension artifact names from release metadata") if "ci_wasix_aot_runtime_artifact_names()" not in publisher: fail("local registry publish preset must derive WASIX AOT artifact names from artifact target metadata") diff --git a/tools/release/extension_artifact_targets.py b/tools/release/extension_artifact_targets.py index 5949321a..23ee8ffe 100644 --- a/tools/release/extension_artifact_targets.py +++ b/tools/release/extension_artifact_targets.py @@ -228,3 +228,25 @@ def published_android_maven_targets(product: str) -> list[ExtensionArtifactTarge ), key=lambda target: target.target, ) + + +def ci_wasix_extension_artifact_names() -> list[str]: + names = [ + f"liboliphaunt-wasix-extension-artifacts-{target_id}" + for target_id in published_target_ids(family="wasix") + ] + if not names: + product_metadata.fail("exact-extension metadata has no published WASIX artifact targets") + return names + + +def ci_extension_package_artifact_names() -> list[str]: + names = ["oliphaunt-extension-package-artifacts"] + mobile_targets = [ + target + for target in artifact_targets(family="native", published_only=True) + if target.kind == "native-static-registry" + ] + if mobile_targets: + names.append("oliphaunt-mobile-extension-package-artifacts") + return names diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index 0156f6b9..42634af6 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -35,6 +35,7 @@ from typing import Any, Iterable import artifact_targets +import extension_artifact_targets ROOT = Path(__file__).resolve().parents[2] @@ -55,19 +56,19 @@ "oliphaunt-wasix-aot-x86_64-unknown-linux-gnu", } -STATIC_LOCAL_PUBLISH_ARTIFACTS = [ - "liboliphaunt-native-release-assets", - "liboliphaunt-wasix-extension-artifacts-wasix-portable", - "liboliphaunt-wasix-release-assets", - "liboliphaunt-wasix-runtime-portable", - "oliphaunt-extension-package-artifacts", - "oliphaunt-mobile-extension-package-artifacts", -] +def local_publish_aggregate_artifacts() -> list[str]: + return [ + artifact_targets.ci_aggregate_release_asset_artifact_name("liboliphaunt-native"), + artifact_targets.ci_aggregate_release_asset_artifact_name("liboliphaunt-wasix"), + *artifact_targets.ci_wasix_runtime_artifact_names(), + *extension_artifact_targets.ci_wasix_extension_artifact_names(), + *extension_artifact_targets.ci_extension_package_artifact_names(), + ] def local_publish_artifacts() -> list[str]: artifacts = [ - *STATIC_LOCAL_PUBLISH_ARTIFACTS, + *local_publish_aggregate_artifacts(), *artifact_targets.ci_release_asset_artifact_names("liboliphaunt-native", "native-runtime"), *artifact_targets.ci_wasix_aot_runtime_artifact_names(), *artifact_targets.ci_release_asset_artifact_names("oliphaunt-broker", "broker-helper"), From 87ce61824924dc430ca5fc53202a3d637386fbd1 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 07:56:19 +0000 Subject: [PATCH 047/137] fix: track rust sdk macro surface --- docs/maintainers/sdk-api-surface.md | 2 ++ tools/policy/generate-sdk-api-surface.mjs | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/docs/maintainers/sdk-api-surface.md b/docs/maintainers/sdk-api-surface.md index a91eb028..2ebde324 100644 --- a/docs/maintainers/sdk-api-surface.md +++ b/docs/maintainers/sdk-api-surface.md @@ -95,6 +95,8 @@ node tools/policy/generate-sdk-api-surface.mjs --write - `oliphaunt::QueryParam` - `oliphaunt::QueryResult` - `oliphaunt::QueryRow` +- `oliphaunt::register_build_resources_dir` +- `oliphaunt::register_build_resources!` - `oliphaunt::required_shared_preload_libraries` - `oliphaunt::resolve_extension_selection` - `oliphaunt::resolve_prebuilt_extension_artifacts_from_indexes` diff --git a/tools/policy/generate-sdk-api-surface.mjs b/tools/policy/generate-sdk-api-surface.mjs index 08908e43..aefe295d 100755 --- a/tools/policy/generate-sdk-api-surface.mjs +++ b/tools/policy/generate-sdk-api-surface.mjs @@ -94,6 +94,15 @@ function extractRustSurface() { skipDocHidden = false; } + for (const file of listFiles('src/sdks/rust/src', '.rs')) { + const source = readRelative(file); + const macroPattern = + /#\[\s*macro_export\s*\]\s*(?:#\[[^\]]+\]\s*)*macro_rules!\s+([A-Za-z_][A-Za-z0-9_]*)/gu; + for (const match of source.matchAll(macroPattern)) { + symbols.push(`oliphaunt::${match[1]}!`); + } + } + return sorted(symbols); } From 0bf896569c6c0b3855c03ceb18ccb39653d5d716 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 08:02:26 +0000 Subject: [PATCH 048/137] fix: track sdk artifact resolution parity --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 6 +++ docs/maintainers/sdk-parity-policy.md | 20 +++++++-- tools/policy/check-sdk-parity.sh | 42 +++++++++++++++++++ tools/policy/sdk-manifest.toml | 20 +++++++++ 4 files changed, 85 insertions(+), 3 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 9885dc02..a8b43a04 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -243,3 +243,9 @@ review production pipelines, then normalize implementation details. Kotlin static/unit checks, mobile extension policy checks, and release checks passed locally; Swift-specific test execution was not run because this Linux host does not have a Swift toolchain. +- SDK parity metadata now records each SDK's normal runtime artifact, standalone + tool, exact-extension, and explicit local override path. The parity policy + documents the cross-SDK artifact-resolution matrix, and + `tools/policy/check-sdk-parity.sh` fails if Rust/TypeScript split tools, + mobile direct-mode no-tools behavior, React Native delegation, or the Deno + explicit-`runtimeDirectory` extension deviation drift from that matrix. diff --git a/docs/maintainers/sdk-parity-policy.md b/docs/maintainers/sdk-parity-policy.md index 8578fed5..75706a08 100644 --- a/docs/maintainers/sdk-parity-policy.md +++ b/docs/maintainers/sdk-parity-policy.md @@ -11,9 +11,9 @@ The machine-checked SDK registry is `tools/policy/sdk-manifest.toml`. It is the compact source -of truth for SDK classification, target platforms, runtime ownership, and -React Native delegation. The prose below explains the contract; the parity check -guards the registry and the docs together. +of truth for SDK classification, target platforms, runtime ownership, artifact +resolution, and React Native delegation. The prose below explains the contract; +the parity check guards the registry and the docs together. The generated public surface inventory is [`sdk-api-surface.md`](sdk-api-surface.md). It is intentionally no-build so @@ -64,6 +64,20 @@ per-extension `layout`; Swift and Kotlin validate those fields before using generated resources, and React Native inherits the same checks through those platform SDKs. +## Artifact Resolution + +Normal installs must use the host ecosystem's package manager. SDKs can still +offer explicit local overrides for contributor and custom-runtime workflows, but +those overrides are not the consumer install path. + +| SDK | Runtime/library artifacts | Standalone tools | Extension artifacts | Explicit local override | +| --- | --- | --- | --- | --- | +| Rust | Cargo-resolved `liboliphaunt-native-*` artifact crates staged by `oliphaunt-build` | split `oliphaunt-tools-*` Cargo artifact crates copied into the runtime cache | exact `oliphaunt-extension-*` Cargo artifact crates | `OLIPHAUNT_RESOURCES_DIR` | +| TypeScript | npm optional platform packages such as `@oliphaunt/liboliphaunt-*` and `@oliphaunt/node-direct-*` | split `@oliphaunt/tools-*` npm packages | Node/Bun exact extension npm packages; Deno requires an explicit prepared `runtimeDirectory` for extension materialization | `libraryPath` and `runtimeDirectory` | +| Swift | SwiftPM release assets and packaged runtime resources | not exposed in mobile native-direct mode | exact extension XCFramework artifacts selected by SQL extension name | `runtimeDirectory` or `resourceRoot` | +| Kotlin | Maven runtime artifacts applied through the Android Gradle plugin | not exposed in Android native-direct mode | exact extension Maven artifacts selected by SQL extension name | `runtimeDirectory` or `resourceRoot` | +| React Native | delegated SwiftPM and Maven platform SDK resolution | delegated to the platform SDK; no separate RN tool runtime | delegated exact extension artifacts through Swift/Kotlin integrations | `runtimeDirectory` or `resourceRoot` | + ## Parity Bar Rust is classified as an SDK, not an internal implementation detail. Its release diff --git a/tools/policy/check-sdk-parity.sh b/tools/policy/check-sdk-parity.sh index d84244c6..345f0bfb 100755 --- a/tools/policy/check-sdk-parity.sh +++ b/tools/policy/check-sdk-parity.sh @@ -108,6 +108,12 @@ require_manifest_text rust 'primary_targets = ["tauri", "rust-desktop"]' \ "SDK manifest must classify Rust as the Tauri/Rust desktop SDK" require_manifest_text rust 'available_modes = ["native-direct", "native-broker", "native-server"]' \ "SDK manifest must declare Rust mode availability" +require_manifest_text rust 'artifact_resolution = "cargo-artifact-crates"' \ + "SDK manifest must declare Rust Cargo artifact runtime resolution" +require_manifest_text rust 'tool_resolution = "split-oliphaunt-tools-cargo-crates"' \ + "SDK manifest must declare Rust split oliphaunt-tools Cargo resolution" +require_manifest_text rust 'extension_resolution = "exact-extension-cargo-crates"' \ + "SDK manifest must declare Rust exact-extension Cargo resolution" require_manifest_text swift 'classification = "sdk"' \ "SDK manifest must classify Swift as a product SDK" require_manifest_text swift 'primary_targets = ["ios", "macos"]' \ @@ -118,6 +124,12 @@ require_manifest_text swift 'available_modes = ["native-direct"]' \ "SDK manifest must declare current Swift mode availability" require_manifest_text swift 'unsupported_modes = ["native-broker", "native-server"]' \ "SDK manifest must declare current Swift unsupported modes" +require_manifest_text swift 'artifact_resolution = "swiftpm-release-assets"' \ + "SDK manifest must declare SwiftPM release asset resolution" +require_manifest_text swift 'tool_resolution = "not-applicable-mobile-native-direct"' \ + "SDK manifest must declare that Swift mobile native-direct does not expose standalone PostgreSQL tools" +require_manifest_text swift 'extension_resolution = "exact-extension-xcframework-artifacts"' \ + "SDK manifest must declare Swift exact-extension XCFramework resolution" require_manifest_text kotlin 'classification = "sdk"' \ "SDK manifest must classify Kotlin as a product SDK" require_manifest_text kotlin 'primary_targets = ["android"]' \ @@ -128,6 +140,12 @@ require_manifest_text kotlin 'available_modes = ["native-direct"]' \ "SDK manifest must declare current Kotlin mode availability" require_manifest_text kotlin 'unsupported_modes = ["native-broker", "native-server"]' \ "SDK manifest must declare current Kotlin unsupported modes" +require_manifest_text kotlin 'artifact_resolution = "maven-runtime-artifacts"' \ + "SDK manifest must declare Kotlin Maven runtime artifact resolution" +require_manifest_text kotlin 'tool_resolution = "not-applicable-mobile-native-direct"' \ + "SDK manifest must declare that Kotlin Android native-direct does not expose standalone PostgreSQL tools" +require_manifest_text kotlin 'extension_resolution = "exact-extension-maven-artifacts"' \ + "SDK manifest must declare Kotlin exact-extension Maven resolution" require_manifest_text react-native 'classification = "sdk"' \ "SDK manifest must classify React Native as an SDK" require_manifest_text react-native 'runtime_owner = false' \ @@ -140,6 +158,12 @@ require_manifest_text react-native 'available_modes = ["native-direct"]' \ "SDK manifest must declare current React Native delegated mode availability" require_manifest_text react-native 'unsupported_modes = ["native-broker", "native-server"]' \ "SDK manifest must declare current React Native unsupported modes" +require_manifest_text react-native 'artifact_resolution = "delegated-swiftpm-maven"' \ + "SDK manifest must declare React Native delegated platform artifact resolution" +require_manifest_text react-native 'tool_resolution = "delegated-platform-sdk"' \ + "SDK manifest must declare React Native delegated tool behavior" +require_manifest_text react-native 'extension_resolution = "delegated-exact-extension-artifacts"' \ + "SDK manifest must declare React Native delegated exact-extension resolution" require_manifest_text typescript 'classification = "sdk"' \ "SDK manifest must classify TypeScript as an SDK" require_manifest_text typescript 'package_name = "@oliphaunt/ts"' \ @@ -150,6 +174,12 @@ require_manifest_text typescript 'available_modes = ["native-direct", "native-br "SDK manifest must declare TypeScript mode availability" require_manifest_text typescript 'depends_on_rust_broker_helper = true' \ "SDK manifest must make the TypeScript broker helper dependency explicit" +require_manifest_text typescript 'artifact_resolution = "npm-optional-platform-packages"' \ + "SDK manifest must declare TypeScript npm optional platform package resolution" +require_manifest_text typescript 'tool_resolution = "split-oliphaunt-tools-npm-packages"' \ + "SDK manifest must declare TypeScript split oliphaunt-tools npm resolution" +require_manifest_text typescript 'extension_resolution = "node-bun-exact-extension-npm-packages-deno-explicit-runtimeDirectory"' \ + "SDK manifest must declare TypeScript Node/Bun registry extension resolution and Deno's explicit-runtimeDirectory gap" require_text docs/maintainers/sdk-products-policy.md "These are product SDKs, not auxiliary bindings." \ "SDK maintainer policy must frame Rust/Swift/Kotlin/RN as product SDKs" require_text docs/maintainers/sdk-products-policy.md '`tools/policy/sdk-manifest.toml` is the repo-level SDK registry kept for' \ @@ -236,6 +266,18 @@ require_text docs/maintainers/sdk-parity-policy.md 'src/shared/fixtures/protocol "SDK parity docs must document the shared protocol fixture corpus" require_text docs/maintainers/sdk-parity-policy.md "React Native is not a fifth runtime." \ "SDK parity docs must forbid an independent React Native runtime" +require_text docs/maintainers/sdk-parity-policy.md "## Artifact Resolution" \ + "SDK parity docs must include the artifact-resolution contract" +require_text docs/maintainers/sdk-parity-policy.md "split \`oliphaunt-tools-*\` Cargo artifact crates copied into the runtime cache" \ + "SDK parity docs must describe Rust split tools Cargo artifact resolution" +require_text docs/maintainers/sdk-parity-policy.md "split \`@oliphaunt/tools-*\` npm packages" \ + "SDK parity docs must describe TypeScript split tools npm resolution" +require_text docs/maintainers/sdk-parity-policy.md "Deno requires an explicit prepared \`runtimeDirectory\` for extension materialization" \ + "SDK parity docs must document the Deno extension-resolution deviation" +require_text docs/maintainers/sdk-parity-policy.md "not exposed in Android native-direct mode" \ + "SDK parity docs must state Android native-direct does not expose standalone PostgreSQL tools" +require_text docs/maintainers/sdk-parity-policy.md "delegated SwiftPM and Maven platform SDK resolution" \ + "SDK parity docs must state React Native artifact resolution is delegated" require_text docs/maintainers/sdk-parity-policy.md "Cloned Rust \`Oliphaunt\` handles share one SDK executor" \ "SDK parity docs must make cloned Rust handle/executor semantics explicit" require_text docs/maintainers/sdk-parity-policy.md "FIFO async serial gate" \ diff --git a/tools/policy/sdk-manifest.toml b/tools/policy/sdk-manifest.toml index 82877bb5..cbb018d5 100644 --- a/tools/policy/sdk-manifest.toml +++ b/tools/policy/sdk-manifest.toml @@ -18,6 +18,10 @@ runtime_boundary = "oliphaunt" parity_role = "canonical" available_modes = ["native-direct", "native-broker", "native-server"] unsupported_modes = [] +artifact_resolution = "cargo-artifact-crates" +tool_resolution = "split-oliphaunt-tools-cargo-crates" +extension_resolution = "exact-extension-cargo-crates" +resource_override = "OLIPHAUNT_RESOURCES_DIR" [sdks.swift] classification = "sdk" @@ -31,6 +35,10 @@ parity_role = "platform-peer" available_modes = ["native-direct"] unsupported_modes = ["native-broker", "native-server"] unsupported_mode_reason = "platform broker/server adapters are not implemented yet; direct mode remains a single-session runtime" +artifact_resolution = "swiftpm-release-assets" +tool_resolution = "not-applicable-mobile-native-direct" +extension_resolution = "exact-extension-xcframework-artifacts" +resource_override = "runtimeDirectory-resourceRoot" [sdks.kotlin] classification = "sdk" @@ -44,6 +52,10 @@ parity_role = "platform-peer" available_modes = ["native-direct"] unsupported_modes = ["native-broker", "native-server"] unsupported_mode_reason = "Android broker/server adapters are not implemented yet; direct mode remains a single-session runtime" +artifact_resolution = "maven-runtime-artifacts" +tool_resolution = "not-applicable-mobile-native-direct" +extension_resolution = "exact-extension-maven-artifacts" +resource_override = "runtimeDirectory-resourceRoot" [sdks.react-native] classification = "sdk" @@ -59,6 +71,10 @@ parity_role = "delegating-platform-peer" available_modes = ["native-direct"] unsupported_modes = ["native-broker", "native-server"] unsupported_mode_reason = "runtime availability is delegated to Swift and Kotlin supportedModes" +artifact_resolution = "delegated-swiftpm-maven" +tool_resolution = "delegated-platform-sdk" +extension_resolution = "delegated-exact-extension-artifacts" +resource_override = "runtimeDirectory-resourceRoot" [sdks.typescript] classification = "sdk" @@ -73,3 +89,7 @@ available_modes = ["native-direct", "native-broker", "native-server"] unsupported_modes = [] depends_on_rust_broker_helper = true broker_helper_product = "oliphaunt-rust" +artifact_resolution = "npm-optional-platform-packages" +tool_resolution = "split-oliphaunt-tools-npm-packages" +extension_resolution = "node-bun-exact-extension-npm-packages-deno-explicit-runtimeDirectory" +resource_override = "libraryPath-runtimeDirectory" From e1cc9e53ace5bd2d5a2b9b697c747edfb502e52b Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 08:14:55 +0000 Subject: [PATCH 049/137] fix: package wasix sdk crate with bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 8 + src/bindings/wasix-rust/moon.yml | 1 + tools/policy/check-crate-package.sh | 2 +- tools/policy/check-release-policy.py | 2 +- tools/release/build-sdk-ci-artifacts.sh | 3 +- .../package_oliphaunt_wasix_sdk_crate.mjs | 339 ++++++++++++++++++ .../package_oliphaunt_wasix_sdk_crate.py | 32 -- 7 files changed, 352 insertions(+), 35 deletions(-) create mode 100755 tools/release/package_oliphaunt_wasix_sdk_crate.mjs delete mode 100755 tools/release/package_oliphaunt_wasix_sdk_crate.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index a8b43a04..82f3baf9 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -151,6 +151,14 @@ review production pipelines, then normalize implementation details. `release.py ci-artifacts --family sdk-package` instead of repeating per-product artifact names, and the WASIX Rust binding is normalized to the same SDK release kind. +- WASIX Rust SDK crate packaging now uses a Bun helper that derives the release + artifact dependency pins from `liboliphaunt-wasix` `registry_packages`, + removes local Cargo paths, writes a deterministic `.crate`, and enforces the + crates.io 10 MiB package limit. Focused validation passed with + `tools/policy/check-crate-package.sh --package oliphaunt-wasix` reporting the + SDK crate at 0.16 MiB, and + `tools/release/build-sdk-ci-artifacts.sh oliphaunt-wasix-rust` staged the same + crate through the SDK artifact path. - CI/release producer-to-consumer audit found no P0/P1 mapping gaps across Cargo, npm, Maven, SwiftPM, or GitHub release assets. Existing `release.py check`, artifact-target, release-metadata, consumer-shape, and diff --git a/src/bindings/wasix-rust/moon.yml b/src/bindings/wasix-rust/moon.yml index 0b48bbe5..8c68a588 100644 --- a/src/bindings/wasix-rust/moon.yml +++ b/src/bindings/wasix-rust/moon.yml @@ -100,6 +100,7 @@ tasks: - "/src/runtimes/liboliphaunt/wasix/crates/**/*" - "/src/bindings/wasix-rust/tools/check-package.sh" - "/tools/release/build-sdk-ci-artifacts.sh" + - "/tools/release/package_oliphaunt_wasix_sdk_crate.mjs" outputs: - "/target/sdk-artifacts/oliphaunt-wasix-rust/**/*" options: diff --git a/tools/policy/check-crate-package.sh b/tools/policy/check-crate-package.sh index 4a105799..5bad2444 100755 --- a/tools/policy/check-crate-package.sh +++ b/tools/policy/check-crate-package.sh @@ -33,7 +33,7 @@ done rm -f target/package/*.crate package_oliphaunt_wasix() { - python3 tools/release/package_oliphaunt_wasix_sdk_crate.py --output-dir target/package >/dev/null + bun tools/release/package_oliphaunt_wasix_sdk_crate.mjs --output-dir target/package >/dev/null } default_packages() { diff --git a/tools/policy/check-release-policy.py b/tools/policy/check-release-policy.py index ad7b200d..30a60034 100644 --- a/tools/policy/check-release-policy.py +++ b/tools/policy/check-release-policy.py @@ -840,7 +840,7 @@ def check_release_workflow_policy() -> None: '"cargo", "metadata"', 'package.get("publish") == []', "package_oliphaunt_wasix", - "tools/release/package_oliphaunt_wasix_sdk_crate.py", + "bun tools/release/package_oliphaunt_wasix_sdk_crate.mjs", 'if [ "$package" = "oliphaunt-wasix" ]; then', ): if snippet not in crate_package_script: diff --git a/tools/release/build-sdk-ci-artifacts.sh b/tools/release/build-sdk-ci-artifacts.sh index 98e1c187..f25b84d4 100755 --- a/tools/release/build-sdk-ci-artifacts.sh +++ b/tools/release/build-sdk-ci-artifacts.sh @@ -204,10 +204,11 @@ case "$product" in ;; oliphaunt-wasix-rust) require cargo + require bun require python3 package_listing="$root/target/oliphaunt-wasix-rust/package/oliphaunt-wasix.package-files.txt" require_file "$package_listing" - python3 tools/release/package_oliphaunt_wasix_sdk_crate.py --output-dir "$artifact_root" + bun tools/release/package_oliphaunt_wasix_sdk_crate.mjs --output-dir "$artifact_root" cp "$package_listing" "$artifact_root/cargo-package-files.txt" ;; *) diff --git a/tools/release/package_oliphaunt_wasix_sdk_crate.mjs b/tools/release/package_oliphaunt_wasix_sdk_crate.mjs new file mode 100755 index 00000000..b814fca5 --- /dev/null +++ b/tools/release/package_oliphaunt_wasix_sdk_crate.mjs @@ -0,0 +1,339 @@ +#!/usr/bin/env bun +import { gzipSync } from 'node:zlib'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); +const cargoPackageSizeLimitBytes = 10 * 1024 * 1024; + +function fail(message) { + console.error(`package_oliphaunt_wasix_sdk_crate.mjs: ${message}`); + process.exit(2); +} + +function rel(target) { + const relative = path.relative(root, target); + return relative.startsWith('..') || path.isAbsolute(relative) + ? target + : relative.split(path.sep).join('/'); +} + +async function readText(relativePath) { + return await fs.readFile(path.join(root, relativePath), 'utf8'); +} + +function parseCargoPackageNameVersion(text, context) { + let inPackage = false; + let name = null; + let version = null; + for (const rawLine of text.split(/\r?\n/u)) { + const line = rawLine.trim(); + if (line === '[package]') { + inPackage = true; + continue; + } + if (inPackage && line.startsWith('[')) { + break; + } + if (!inPackage) { + continue; + } + name ??= line.match(/^name\s*=\s*"([^"]+)"/u)?.[1] ?? null; + version ??= line.match(/^version\s*=\s*"([^"]+)"/u)?.[1] ?? null; + } + if (!name || !version) { + fail(`${context} must declare package.name and package.version`); + } + return { name, version }; +} + +async function readCargoPackageNameVersion(manifest) { + return parseCargoPackageNameVersion(await fs.readFile(manifest, 'utf8'), rel(manifest)); +} + +async function currentOliphauntWasixSdkVersion() { + const text = await readText('src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml'); + return parseCargoPackageNameVersion( + text, + 'src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml', + ).version; +} + +async function currentLiboliphauntWasixVersion() { + const version = (await readText('src/runtimes/liboliphaunt/wasix/VERSION')).trim(); + if (!version) { + fail('src/runtimes/liboliphaunt/wasix/VERSION must not be empty'); + } + return version; +} + +async function wasixCargoRegistryPackages() { + const text = await readText('src/runtimes/liboliphaunt/wasix/release.toml'); + const match = text.match(/^registry_packages\s*=\s*\[([\s\S]*?)^\]/mu); + if (!match) { + fail('src/runtimes/liboliphaunt/wasix/release.toml must declare registry_packages'); + } + const packages = [...match[1].matchAll(/"crates:([^"]+)"/gu)].map((item) => item[1]); + if (packages.length === 0) { + fail('liboliphaunt-wasix registry_packages must include Cargo packages'); + } + return packages.sort(); +} + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'); +} + +function compareText(left, right) { + return left < right ? -1 : left > right ? 1 : 0; +} + +function packagedCargoManifestText(source) { + let text = source + .replaceAll('repository.workspace = true', 'repository = "https://github.com/f0rr0/oliphaunt"') + .replaceAll('homepage.workspace = true', 'homepage = "https://oliphaunt.dev"'); + text = text.replace(/, path = "[^"]+"/gu, ''); + if (!text.includes('\n[workspace]')) { + text = `${text.trimEnd()}\n\n[workspace]\n`; + } + return text; +} + +function renderOliphauntWasixReleaseCargoToml(source, runtimeVersion, registryPackages) { + let text = packagedCargoManifestText(source); + for (const crate of registryPackages) { + const pattern = new RegExp( + `^(${escapeRegExp(crate)}\\s*=\\s*\\{[^}\\n]*version\\s*=\\s*")=[^"]+("[^}\\n]*\\})$`, + 'mu', + ); + if (!pattern.test(text)) { + fail(`generated oliphaunt-wasix release source is missing dependency ${crate}`); + } + text = text.replace(pattern, `$1=${runtimeVersion}$2`); + } + return text; +} + +function validateGeneratedOliphauntWasixReleaseArtifactCoverage( + manifestText, + runtimeVersion, + registryPackages, +) { + if (/=\s*\{[^}\n]*path\s*=/u.test(manifestText)) { + fail('generated oliphaunt-wasix release source must not contain local path dependencies'); + } + const missing = registryPackages.filter( + (crate) => !manifestText.includes(`${crate} = { version = "=${runtimeVersion}"`), + ); + if (missing.length > 0) { + fail( + `generated oliphaunt-wasix release source is missing WASIX artifact dependency pins: ${missing.join(', ')}`, + ); + } +} + +async function copySourceTree(source, destination, ignoredNames) { + await fs.rm(destination, { recursive: true, force: true }); + await fs.mkdir(path.dirname(destination), { recursive: true }); + await fs.cp(source, destination, { + recursive: true, + filter: (sourcePath) => !ignoredNames.has(path.basename(sourcePath)), + }); +} + +async function prepareOliphauntWasixReleaseSource(version) { + const runtimeVersion = await currentLiboliphauntWasixVersion(); + const registryPackages = await wasixCargoRegistryPackages(); + const sourceDir = path.join(root, 'src/bindings/wasix-rust/crates/oliphaunt-wasix'); + const stageDir = path.join(root, 'target/release/cargo-package-sources/oliphaunt-wasix'); + await copySourceTree(sourceDir, stageDir, new Set(['target'])); + const cargoToml = path.join(stageDir, 'Cargo.toml'); + const rendered = renderOliphauntWasixReleaseCargoToml( + await fs.readFile(cargoToml, 'utf8'), + runtimeVersion, + registryPackages, + ); + const generatedPackage = parseCargoPackageNameVersion(rendered, rel(cargoToml)); + if (generatedPackage.version !== version) { + fail(`generated oliphaunt-wasix release source must keep SDK version ${version}`); + } + validateGeneratedOliphauntWasixReleaseArtifactCoverage( + rendered, + runtimeVersion, + registryPackages, + ); + await fs.writeFile(cargoToml, rendered); + return cargoToml; +} + +async function cargoMetadataPackageFromManifest(manifest) { + const proc = Bun.spawn( + ['cargo', 'metadata', '--manifest-path', manifest, '--format-version', '1', '--no-deps'], + { + cwd: root, + stdout: 'pipe', + stderr: 'pipe', + }, + ); + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); + if (exitCode !== 0) { + fail(`cargo metadata failed for ${rel(manifest)}: ${stderr.trim()}`); + } + const packages = JSON.parse(stdout).packages; + if (!Array.isArray(packages) || packages.length !== 1 || typeof packages[0] !== 'object') { + fail(`cargo metadata for ${rel(manifest)} did not return exactly one package`); + } + return packages[0]; +} + +async function listFilesRecursive(directory) { + const files = []; + const entries = await fs.readdir(directory, { withFileTypes: true }); + entries.sort((left, right) => compareText(left.name, right.name)); + for (const entry of entries) { + const fullPath = path.join(directory, entry.name); + if (entry.isDirectory()) { + files.push(...(await listFilesRecursive(fullPath))); + } else if (entry.isFile() || entry.isSymbolicLink()) { + files.push(fullPath); + } + } + return files; +} + +function tarPathParts(relativePath) { + const normalized = relativePath.split(path.sep).join('/'); + if (Buffer.byteLength(normalized) <= 100) { + return { name: normalized, prefix: '' }; + } + const parts = normalized.split('/'); + for (let index = 1; index < parts.length; index += 1) { + const prefix = parts.slice(0, index).join('/'); + const name = parts.slice(index).join('/'); + if (Buffer.byteLength(prefix) <= 155 && Buffer.byteLength(name) <= 100) { + return { name, prefix }; + } + } + fail(`crate archive path is too long for ustar: ${normalized}`); +} + +function writeString(buffer, offset, length, value) { + const bytes = Buffer.from(value); + if (bytes.length > length) { + fail(`tar header field overflow for '${value}'`); + } + bytes.copy(buffer, offset); +} + +function writeOctal(buffer, offset, length, value) { + const text = value.toString(8); + if (text.length > length - 1) { + fail(`tar header octal field overflow for '${value}'`); + } + writeString(buffer, offset, length, `${text.padStart(length - 1, '0')}\0`); +} + +function tarHeader(relativePath, size, mode) { + const header = Buffer.alloc(512, 0); + const { name, prefix } = tarPathParts(relativePath); + writeString(header, 0, 100, name); + writeOctal(header, 100, 8, mode); + writeOctal(header, 108, 8, 0); + writeOctal(header, 116, 8, 0); + writeOctal(header, 124, 12, size); + writeOctal(header, 136, 12, 0); + header.fill(0x20, 148, 156); + writeString(header, 156, 1, '0'); + writeString(header, 257, 6, 'ustar\0'); + writeString(header, 263, 2, '00'); + writeString(header, 345, 155, prefix); + let checksum = 0; + for (const byte of header) { + checksum += byte; + } + const checksumText = checksum.toString(8); + if (checksumText.length > 6) { + fail(`tar header checksum overflow for ${relativePath}`); + } + writeString(header, 148, 8, `${checksumText.padStart(6, '0')}\0 `); + return header; +} + +async function createTar(stageDir, packageRoot) { + const chunks = []; + const files = await listFilesRecursive(stageDir); + files.sort((left, right) => compareText(path.relative(stageDir, left), path.relative(stageDir, right))); + for (const file of files) { + const relative = path.relative(stageDir, file).split(path.sep).join('/'); + const archivePath = `${packageRoot}/${relative}`; + const stat = await fs.stat(file); + const data = await fs.readFile(file); + chunks.push(tarHeader(archivePath, data.length, stat.mode & 0o777)); + chunks.push(data); + const remainder = data.length % 512; + if (remainder !== 0) { + chunks.push(Buffer.alloc(512 - remainder, 0)); + } + } + chunks.push(Buffer.alloc(1024, 0)); + return Buffer.concat(chunks); +} + +async function manualCargoPackageSource(manifest, outputDir) { + const { name, version } = await readCargoPackageNameVersion(manifest); + const sourceDir = path.dirname(manifest); + const packageRoot = `${name}-${version}`; + const stageRoot = path.join(outputDir, 'manual-package-stage'); + const stageDir = path.join(stageRoot, packageRoot); + const cratePath = path.join(outputDir, `${packageRoot}.crate`); + await copySourceTree(sourceDir, stageDir, new Set(['target', '.git', '.DS_Store'])); + + const stagedManifest = path.join(stageDir, 'Cargo.toml'); + await fs.writeFile( + stagedManifest, + packagedCargoManifestText(await fs.readFile(stagedManifest, 'utf8')), + ); + const packageMetadata = await cargoMetadataPackageFromManifest(stagedManifest); + if (packageMetadata.name !== name || packageMetadata.version !== version) { + fail(`${rel(stagedManifest)} produced unexpected cargo metadata`); + } + + await fs.mkdir(outputDir, { recursive: true }); + await fs.rm(cratePath, { force: true }); + await fs.writeFile(cratePath, gzipSync(await createTar(stageDir, packageRoot), { mtime: 0 })); + const size = (await fs.stat(cratePath)).size; + if (size > cargoPackageSizeLimitBytes) { + fail(`${rel(cratePath)} is ${size} bytes, above the crates.io 10 MiB package limit`); + } + return cratePath; +} + +function parseArgs(argv) { + let outputDir = null; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === '--output-dir') { + outputDir = argv[index + 1] ?? null; + index += 1; + continue; + } + fail(`unknown argument: ${arg}`); + } + if (!outputDir) { + fail('usage: tools/release/package_oliphaunt_wasix_sdk_crate.mjs --output-dir '); + } + return { + outputDir: path.isAbsolute(outputDir) ? outputDir : path.join(root, outputDir), + }; +} + +const { outputDir } = parseArgs(Bun.argv.slice(2)); +const version = await currentOliphauntWasixSdkVersion(); +const manifest = await prepareOliphauntWasixReleaseSource(version); +const cratePath = await manualCargoPackageSource(manifest, outputDir); +console.log(rel(cratePath)); diff --git a/tools/release/package_oliphaunt_wasix_sdk_crate.py b/tools/release/package_oliphaunt_wasix_sdk_crate.py deleted file mode 100755 index 11ff9258..00000000 --- a/tools/release/package_oliphaunt_wasix_sdk_crate.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 -"""Package the WASIX Rust SDK publish-shaped crate without resolving dependencies.""" - -from __future__ import annotations - -import argparse -from pathlib import Path - -import local_registry_publish -import release - - -ROOT = Path(__file__).resolve().parents[2] - - -def main() -> int: - parser = argparse.ArgumentParser() - parser.add_argument("--output-dir", required=True, type=Path) - args = parser.parse_args() - - output_dir = args.output_dir - if not output_dir.is_absolute(): - output_dir = ROOT / output_dir - version = release.current_product_version("oliphaunt-wasix-rust") - manifest = release.prepare_oliphaunt_wasix_release_source(version) - crate_path = local_registry_publish.manual_cargo_package_source(manifest, output_dir) - print(crate_path.relative_to(ROOT)) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) From 1aedd6888064de12bc983a984e4e632fa715953d Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 08:25:06 +0000 Subject: [PATCH 050/137] fix: write release checksums with bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 4 + .../node-direct/tools/build-node-addon.sh | 3 +- tools/release/package-broker-assets.sh | 4 +- tools/release/release.py | 4 +- tools/release/write_checksum_manifest.mjs | 79 +++++++++++++++++++ tools/release/write_checksum_manifest.py | 52 ------------ 6 files changed, 90 insertions(+), 56 deletions(-) create mode 100755 tools/release/write_checksum_manifest.mjs delete mode 100755 tools/release/write_checksum_manifest.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 82f3baf9..ab23df68 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -159,6 +159,10 @@ review production pipelines, then normalize implementation details. SDK crate at 0.16 MiB, and `tools/release/build-sdk-ci-artifacts.sh oliphaunt-wasix-rust` staged the same crate through the SDK artifact path. +- Release checksum manifest generation now uses Bun instead of Python for the + broker and node-direct release asset paths. The helper preserves deterministic + basename-sorted SHA-256 output, streams large archive hashing, and is called + directly from `release.py`, broker packaging, and node-direct packaging. - CI/release producer-to-consumer audit found no P0/P1 mapping gaps across Cargo, npm, Maven, SwiftPM, or GitHub release assets. Existing `release.py check`, artifact-target, release-metadata, consumer-shape, and diff --git a/src/runtimes/node-direct/tools/build-node-addon.sh b/src/runtimes/node-direct/tools/build-node-addon.sh index 51b73de7..c4ab4f80 100755 --- a/src/runtimes/node-direct/tools/build-node-addon.sh +++ b/src/runtimes/node-direct/tools/build-node-addon.sh @@ -16,6 +16,7 @@ require() { require node require npm +require bun require python3 require tar @@ -231,7 +232,7 @@ if [ -n "$input_dirs" ]; then IFS="$old_ifs" fi -tools/release/write_checksum_manifest.py \ +tools/release/write_checksum_manifest.mjs \ --asset-dir "$asset_dir" \ --output "oliphaunt-node-direct-$version-release-assets.sha256" \ --pattern 'oliphaunt-node-direct-*.tar.gz' \ diff --git a/tools/release/package-broker-assets.sh b/tools/release/package-broker-assets.sh index a403fe7e..c3d4e5e7 100755 --- a/tools/release/package-broker-assets.sh +++ b/tools/release/package-broker-assets.sh @@ -18,6 +18,8 @@ fail() { exit 1 } +command -v bun >/dev/null 2>&1 || fail "missing required command: bun" + python_bin="${PYTHON:-python3}" if ! command -v "$python_bin" >/dev/null 2>&1; then if command -v python >/dev/null 2>&1; then @@ -86,7 +88,7 @@ if [ -n "$input_dirs" ]; then fi ( - tools/release/write_checksum_manifest.py \ + tools/release/write_checksum_manifest.mjs \ --asset-dir "$out_dir" \ --output "$checksum_asset" \ --pattern 'oliphaunt-broker-*.tar.gz' \ diff --git a/tools/release/release.py b/tools/release/release.py index 2c7c6fa4..b6f92161 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -1251,7 +1251,7 @@ def ensure_broker_release_assets() -> None: version = current_product_version("oliphaunt-broker") run( [ - "tools/release/write_checksum_manifest.py", + "tools/release/write_checksum_manifest.mjs", "--asset-dir", str(asset_dir.relative_to(ROOT)), "--output", @@ -1277,7 +1277,7 @@ def ensure_node_direct_release_assets() -> None: version = current_product_version("oliphaunt-node-direct") run( [ - "tools/release/write_checksum_manifest.py", + "tools/release/write_checksum_manifest.mjs", "--asset-dir", str(asset_dir.relative_to(ROOT)), "--output", diff --git a/tools/release/write_checksum_manifest.mjs b/tools/release/write_checksum_manifest.mjs new file mode 100755 index 00000000..546641b9 --- /dev/null +++ b/tools/release/write_checksum_manifest.mjs @@ -0,0 +1,79 @@ +#!/usr/bin/env bun +import { createHash } from 'node:crypto'; +import { createReadStream } from 'node:fs'; +import fs from 'node:fs/promises'; +import path from 'node:path'; + +function fail(message) { + console.error(`write_checksum_manifest.mjs: ${message}`); + process.exit(2); +} + +function parseArgs(argv) { + const patterns = []; + let assetDir = null; + let output = null; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + switch (arg) { + case '--asset-dir': + assetDir = argv[index + 1] ?? null; + index += 1; + break; + case '--output': + output = argv[index + 1] ?? null; + index += 1; + break; + case '--pattern': + patterns.push(argv[index + 1] ?? ''); + index += 1; + break; + default: + fail(`unknown argument: ${arg}`); + } + } + if (!assetDir || !output || patterns.length === 0 || patterns.some((pattern) => pattern.length === 0)) { + fail( + 'usage: tools/release/write_checksum_manifest.mjs --asset-dir

--output --pattern [--pattern ...]', + ); + } + return { + assetDir: path.resolve(assetDir), + output, + patterns, + }; +} + +async function sha256(file) { + const digest = createHash('sha256'); + for await (const chunk of createReadStream(file)) { + digest.update(chunk); + } + return digest.digest('hex'); +} + +function baseName(relativePath) { + return relativePath.split(/[\\/]/u).pop(); +} + +async function matchingAssets(assetDir, patterns) { + const assets = new Map(); + for (const pattern of patterns) { + const glob = new Bun.Glob(pattern); + for await (const relativePath of glob.scan({ cwd: assetDir, onlyFiles: true })) { + assets.set(baseName(relativePath), path.join(assetDir, relativePath)); + } + } + return [...assets.keys()].sort().map((name) => assets.get(name)); +} + +const args = parseArgs(Bun.argv.slice(2)); +const outputPath = path.join(args.assetDir, args.output); +const lines = []; +for (const asset of await matchingAssets(args.assetDir, args.patterns)) { + if (path.resolve(asset) === path.resolve(outputPath)) { + continue; + } + lines.push(`${await sha256(asset)} ${path.basename(asset)}\n`); +} +await fs.writeFile(outputPath, lines.join('')); diff --git a/tools/release/write_checksum_manifest.py b/tools/release/write_checksum_manifest.py deleted file mode 100755 index 0199ff4a..00000000 --- a/tools/release/write_checksum_manifest.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env python3 -"""Write a deterministic sha256 manifest for release assets.""" - -from __future__ import annotations - -import argparse -import hashlib -from pathlib import Path - - -def sha256(path: Path) -> str: - digest = hashlib.sha256() - with path.open("rb") as handle: - for chunk in iter(lambda: handle.read(1024 * 1024), b""): - digest.update(chunk) - return digest.hexdigest() - - -def matching_assets(asset_dir: Path, patterns: list[str]) -> list[Path]: - assets: dict[str, Path] = {} - for pattern in patterns: - for path in asset_dir.glob(pattern): - if path.is_file(): - assets[path.name] = path - return [assets[name] for name in sorted(assets)] - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--asset-dir", required=True, help="directory containing assets") - parser.add_argument("--output", required=True, help="checksum manifest file name") - parser.add_argument( - "--pattern", - action="append", - required=True, - help="glob pattern, relative to asset-dir; may be passed more than once", - ) - args = parser.parse_args() - - asset_dir = Path(args.asset_dir).resolve() - output = asset_dir / args.output - assets = matching_assets(asset_dir, args.pattern) - with output.open("w", encoding="utf-8", newline="\n") as handle: - for asset in assets: - if asset == output: - continue - handle.write(f"{sha256(asset)} {asset.name}\n") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) From 29b4898c16d705a34c57e97d5c70b9f4b4d76482 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 08:32:59 +0000 Subject: [PATCH 051/137] fix: check publish environment with bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 4 + tools/release/check_publish_environment.mjs | 177 ++++++++++++++++++ tools/release/check_publish_environment.py | 119 ------------ tools/release/release.py | 2 +- 4 files changed, 182 insertions(+), 120 deletions(-) create mode 100755 tools/release/check_publish_environment.mjs delete mode 100755 tools/release/check_publish_environment.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index ab23df68..aec0c5e8 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -163,6 +163,10 @@ review production pipelines, then normalize implementation details. broker and node-direct release asset paths. The helper preserves deterministic basename-sorted SHA-256 output, streams large archive hashing, and is called directly from `release.py`, broker packaging, and node-direct packaging. +- Release publish-environment validation now uses Bun instead of Python. The + helper scans product `release.toml` metadata directly, validates selected + product ids, and preserves the trusted-publishing, GitHub, Maven, and + forbidden-token checks. - CI/release producer-to-consumer audit found no P0/P1 mapping gaps across Cargo, npm, Maven, SwiftPM, or GitHub release assets. Existing `release.py check`, artifact-target, release-metadata, consumer-shape, and diff --git a/tools/release/check_publish_environment.mjs b/tools/release/check_publish_environment.mjs new file mode 100755 index 00000000..ca98f144 --- /dev/null +++ b/tools/release/check_publish_environment.mjs @@ -0,0 +1,177 @@ +#!/usr/bin/env bun +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); +const oidcTargets = new Set(['crates-io', 'npm', 'jsr']); +const mavenTargets = new Set(['maven-central']); +const githubTargets = new Set(['github-release', 'github-release-assets', 'swift-package-source-tag']); +const forbiddenEnvVars = { + CARGO_REGISTRY_TOKEN: [ + new Set(['crates-io']), + 'Cargo publishing uses crates.io trusted publishing through GitHub Actions OIDC', + ], + NPM_TOKEN: [ + new Set(['npm']), + 'npm publishing uses trusted publishing with provenance through GitHub Actions OIDC', + ], + NODE_AUTH_TOKEN: [ + new Set(['npm']), + 'npm publishing uses trusted publishing with provenance through GitHub Actions OIDC', + ], + JSR_TOKEN: [new Set(['jsr']), 'JSR publishing uses GitHub Actions OIDC'], + COCOAPODS_TRUNK_TOKEN: [ + new Set(), + 'Apple SDK releases use SwiftPM plus GitHub assets, not CocoaPods trunk', + ], + COCOAPODS_TRUNK_EMAIL: [ + new Set(), + 'Apple SDK releases use SwiftPM plus GitHub assets, not CocoaPods trunk', + ], +}; + +function fail(message) { + console.error(`check_publish_environment.mjs: ${message}`); + process.exit(1); +} + +function parseArgs(argv) { + let productsJson = null; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === '--products-json') { + productsJson = argv[index + 1] ?? null; + index += 1; + continue; + } + fail(`unknown argument: ${arg}`); + } + if (productsJson === null) { + fail('usage: tools/release/check_publish_environment.mjs --products-json '); + } + return { productsJson }; +} + +function parseProducts(raw) { + let value; + try { + value = JSON.parse(raw); + } catch (error) { + fail(`--products-json must be valid JSON: ${error.message}`); + } + if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) { + fail('--products-json must be a JSON string list'); + } + return new Set(value); +} + +async function productConfigs() { + const releasePlease = JSON.parse(await fs.readFile(path.join(root, 'release-please-config.json'), 'utf8')); + if (typeof releasePlease.packages !== 'object' || releasePlease.packages === null) { + fail('release-please-config.json must define packages'); + } + const products = new Map(); + const packageEntries = Object.entries(releasePlease.packages).sort(([left], [right]) => + left < right ? -1 : left > right ? 1 : 0, + ); + for (const [packagePath, packageConfig] of packageEntries) { + if (path.isAbsolute(packagePath) || packagePath.split(/[\\/]/u).includes('..')) { + fail(`release-please package path must stay inside the repository: ${packagePath}`); + } + const component = packageConfig?.component; + if (typeof component !== 'string' || component.length === 0) { + fail(`${packagePath}.component must be a non-empty string`); + } + const file = path.join(root, packagePath, 'release.toml'); + const metadata = Bun.TOML.parse(await fs.readFile(file, 'utf8')); + const id = metadata.id; + if (id !== component) { + fail(`${path.relative(root, file)} must declare id = "${component}"`); + } + if (products.has(id)) { + fail(`duplicate release product id ${id}`); + } + const publishTargets = metadata.publish_targets ?? []; + if ( + !Array.isArray(publishTargets) || + publishTargets.some((target) => typeof target !== 'string') + ) { + fail(`${id}.publish_targets must be a string list`); + } + products.set(id, { publishTargets }); + } + return products; +} + +function requireEnv(name, context, failures) { + if (!process.env[name]) { + failures.push(`${context} requires ${name}`); + } +} + +function requireAnyEnv(names, context, failures) { + if (!names.some((name) => process.env[name])) { + failures.push(`${context} requires one of ${names.join(', ')}`); + } +} + +function intersects(left, right) { + for (const value of left) { + if (right.has(value)) { + return true; + } + } + return false; +} + +const args = parseArgs(Bun.argv.slice(2)); +const products = parseProducts(args.productsJson); +const configs = await productConfigs(); +const unknown = [...products].filter((product) => !configs.has(product)).sort(); +if (unknown.length > 0) { + fail(`unknown release products: ${unknown.join(', ')}`); +} + +const publishTargets = new Set(); +for (const product of products) { + for (const target of configs.get(product).publishTargets) { + publishTargets.add(target); + } +} + +const failures = []; +for (const [name, [blockedTargets, reason]] of Object.entries(forbiddenEnvVars).sort()) { + const appliesToSelection = + products.size > 0 && (blockedTargets.size === 0 || intersects(publishTargets, blockedTargets)); + if (appliesToSelection && process.env[name]) { + failures.push(`forbidden release credential ${name} is set: ${reason}`); + } +} + +if (intersects(publishTargets, oidcTargets)) { + requireEnv('ACTIONS_ID_TOKEN_REQUEST_TOKEN', 'trusted publishing', failures); + requireEnv('ACTIONS_ID_TOKEN_REQUEST_URL', 'trusted publishing', failures); +} + +if (intersects(publishTargets, githubTargets)) { + requireAnyEnv(['GH_TOKEN', 'GITHUB_TOKEN'], 'GitHub release assets and tags', failures); +} + +if (intersects(publishTargets, mavenTargets)) { + for (const name of [ + 'ORG_GRADLE_PROJECT_mavenCentralUsername', + 'ORG_GRADLE_PROJECT_mavenCentralPassword', + 'ORG_GRADLE_PROJECT_signingInMemoryKey', + 'ORG_GRADLE_PROJECT_signingInMemoryKeyId', + 'ORG_GRADLE_PROJECT_signingInMemoryKeyPassword', + ]) { + requireEnv(name, 'Maven Central publish', failures); + } +} + +if (failures.length > 0) { + fail(`missing publish environment:\n - ${failures.join('\n - ')}`); +} + +console.log('publish environment checks passed'); diff --git a/tools/release/check_publish_environment.py b/tools/release/check_publish_environment.py deleted file mode 100755 index 0607122c..00000000 --- a/tools/release/check_publish_environment.py +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env python3 -"""Fail fast when selected release products are missing publish credentials.""" - -from __future__ import annotations - -import argparse -import json -import os -import sys -from typing import NoReturn - -import product_metadata - -OIDC_TARGETS = {"crates-io", "npm", "jsr"} -MAVEN_TARGETS = {"maven-central"} -GITHUB_TARGETS = {"github-release", "github-release-assets", "swift-package-source-tag"} -FORBIDDEN_ENV_VARS = { - "CARGO_REGISTRY_TOKEN": ( - {"crates-io"}, - "Cargo publishing uses crates.io trusted publishing through GitHub Actions OIDC", - ), - "NPM_TOKEN": ( - {"npm"}, - "npm publishing uses trusted publishing with provenance through GitHub Actions OIDC", - ), - "NODE_AUTH_TOKEN": ( - {"npm"}, - "npm publishing uses trusted publishing with provenance through GitHub Actions OIDC", - ), - "JSR_TOKEN": ({"jsr"}, "JSR publishing uses GitHub Actions OIDC"), - "COCOAPODS_TRUNK_TOKEN": ( - set(), - "Apple SDK releases use SwiftPM plus GitHub assets, not CocoaPods trunk", - ), - "COCOAPODS_TRUNK_EMAIL": ( - set(), - "Apple SDK releases use SwiftPM plus GitHub assets, not CocoaPods trunk", - ), -} - - -def fail(message: str) -> NoReturn: - print(f"check_publish_environment.py: {message}", file=sys.stderr) - raise SystemExit(1) - - -def parse_products(raw: str) -> set[str]: - value = json.loads(raw) - if not isinstance(value, list) or not all(isinstance(item, str) for item in value): - fail("--products-json must be a JSON string list") - products = set(value) - known = set(product_metadata.product_ids()) - unknown = sorted(products - known) - if unknown: - fail(f"unknown release products: {', '.join(unknown)}") - return products - - -def require_env(name: str, context: str, failures: list[str]) -> None: - if not os.environ.get(name): - failures.append(f"{context} requires {name}") - - -def require_any_env(names: list[str], context: str, failures: list[str]) -> None: - if not any(os.environ.get(name) for name in names): - failures.append(f"{context} requires one of {', '.join(names)}") - - -def selected_publish_targets(products: set[str]) -> set[str]: - targets: set[str] = set() - graph = product_metadata.load_graph() - for product in products: - config = product_metadata.product_config(product, graph) - targets.update(product_metadata.string_list(config, "publish_targets", product)) - return targets - - -def main(argv: list[str]) -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--products-json", required=True) - args = parser.parse_args(argv) - - products = parse_products(args.products_json) - publish_targets = selected_publish_targets(products) - failures: list[str] = [] - - for name, (blocked_targets, reason) in sorted(FORBIDDEN_ENV_VARS.items()): - applies_to_selection = bool(products) and ( - not blocked_targets or bool(publish_targets & blocked_targets) - ) - if applies_to_selection and os.environ.get(name): - failures.append(f"forbidden release credential {name} is set: {reason}") - - if publish_targets & OIDC_TARGETS: - require_env("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "trusted publishing", failures) - require_env("ACTIONS_ID_TOKEN_REQUEST_URL", "trusted publishing", failures) - - if publish_targets & GITHUB_TARGETS: - require_any_env(["GH_TOKEN", "GITHUB_TOKEN"], "GitHub release assets and tags", failures) - - if publish_targets & MAVEN_TARGETS: - for name in [ - "ORG_GRADLE_PROJECT_mavenCentralUsername", - "ORG_GRADLE_PROJECT_mavenCentralPassword", - "ORG_GRADLE_PROJECT_signingInMemoryKey", - "ORG_GRADLE_PROJECT_signingInMemoryKeyId", - "ORG_GRADLE_PROJECT_signingInMemoryKeyPassword", - ]: - require_env(name, "Maven Central publish", failures) - - if failures: - fail("missing publish environment:\n - " + "\n - ".join(failures)) - - print("publish environment checks passed") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) diff --git a/tools/release/release.py b/tools/release/release.py index b6f92161..c8f3eab0 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -3188,7 +3188,7 @@ def command_publish(args: argparse.Namespace, passthrough: list[str]) -> None: command_publish_product_step(args) return products_args = passthrough - run(["tools/release/check_publish_environment.py", *products_args]) + run(["tools/release/check_publish_environment.mjs", *products_args]) command_publish_dry_run(args, passthrough) print("publish environment and dry-run checks passed; package-native publish steps run in the Release workflow") From 64a8144b7b9186eabd433b57aa59e6c8622452c7 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 08:37:32 +0000 Subject: [PATCH 052/137] fix: verify product tags with bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 3 + tools/release/release.py | 2 +- tools/release/verify_product_tag.mjs | 154 ++++++++++++++++++ tools/release/verify_product_tag.py | 81 --------- 4 files changed, 158 insertions(+), 82 deletions(-) create mode 100755 tools/release/verify_product_tag.mjs delete mode 100755 tools/release/verify_product_tag.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index aec0c5e8..02af59f6 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -167,6 +167,9 @@ review production pipelines, then normalize implementation details. helper scans product `release.toml` metadata directly, validates selected product ids, and preserves the trusted-publishing, GitHub, Maven, and forbidden-token checks. +- Product release-tag verification now uses Bun instead of Python. The helper + reads release-please product config, resolves the product's current version, + and verifies the product-scoped tag points at the release commit. - CI/release producer-to-consumer audit found no P0/P1 mapping gaps across Cargo, npm, Maven, SwiftPM, or GitHub release assets. Existing `release.py check`, artifact-target, release-metadata, consumer-shape, and diff --git a/tools/release/release.py b/tools/release/release.py index c8f3eab0..8192bc8a 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -447,7 +447,7 @@ def current_product_version(product: str) -> str: def verify_release_tag(product: str, head_ref: str) -> None: - run(["tools/release/verify_product_tag.py", product, "--target", head_ref]) + run(["tools/release/verify_product_tag.mjs", product, "--target", head_ref]) def glob_release_assets(asset_dir: Path, suffixes: tuple[str, ...]) -> list[str]: diff --git a/tools/release/verify_product_tag.mjs b/tools/release/verify_product_tag.mjs new file mode 100755 index 00000000..35573127 --- /dev/null +++ b/tools/release/verify_product_tag.mjs @@ -0,0 +1,154 @@ +#!/usr/bin/env bun +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); +const decoder = new TextDecoder(); + +function fail(message) { + console.error(`verify_product_tag.mjs: ${message}`); + process.exit(1); +} + +function parseArgs(argv) { + let product = null; + let target = process.env.GITHUB_SHA || 'HEAD'; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === '--target') { + target = argv[index + 1] ?? ''; + index += 1; + continue; + } + if (arg.startsWith('--')) { + fail(`unknown argument: ${arg}`); + } + if (product !== null) { + fail('usage: tools/release/verify_product_tag.mjs [--target ]'); + } + product = arg; + } + if (!product || !target) { + fail('usage: tools/release/verify_product_tag.mjs [--target ]'); + } + return { product, target }; +} + +function git(args, { check = true } = {}) { + const result = Bun.spawnSync(['git', ...args], { + cwd: root, + stdout: 'pipe', + stderr: 'pipe', + }); + if (check && result.exitCode !== 0) { + const stderr = decoder.decode(result.stderr).trim(); + fail(`git ${args.join(' ')} failed${stderr ? `: ${stderr}` : ''}`); + } + return { + exitCode: result.exitCode, + stdout: decoder.decode(result.stdout).trim(), + }; +} + +function commitForRef(ref) { + return git(['rev-parse', `${ref}^{commit}`]).stdout; +} + +function tagCommit(tag) { + const result = git(['rev-parse', '--verify', '--quiet', `refs/tags/${tag}^{commit}`], { + check: false, + }); + return result.exitCode === 0 ? result.stdout : null; +} + +async function releasePleaseProduct(product) { + const config = JSON.parse(await fs.readFile(path.join(root, 'release-please-config.json'), 'utf8')); + if (config['include-v-in-tag'] !== true) { + fail('release-please must include v in product tags'); + } + if (config['tag-separator'] !== '-') { + fail("release-please tag-separator must be '-'"); + } + const packages = config.packages; + if (typeof packages !== 'object' || packages === null) { + fail('release-please-config.json must define packages'); + } + for (const [packagePath, packageConfig] of Object.entries(packages)) { + if (packageConfig?.component === product) { + return { packagePath, packageConfig }; + } + } + fail(`unknown release product '${product}'`); +} + +function parseCargoVersion(text) { + let inPackage = false; + for (const rawLine of text.split(/\r?\n/u)) { + const line = rawLine.trim(); + if (line === '[package]') { + inPackage = true; + continue; + } + if (inPackage && line.startsWith('[')) { + break; + } + if (!inPackage) { + continue; + } + const match = line.match(/^version\s*=\s*"([^"]+)"/u); + if (match) { + return match[1]; + } + } + return ''; +} + +async function currentProductVersion(product) { + const { packagePath, packageConfig } = await releasePleaseProduct(product); + const releaseType = packageConfig['release-type']; + const versionFile = + typeof packageConfig['version-file'] === 'string' + ? packageConfig['version-file'] + : releaseType === 'rust' + ? 'Cargo.toml' + : releaseType === 'node' || releaseType === 'expo' + ? 'package.json' + : null; + if (!versionFile) { + fail(`${product} release-please config must declare version-file for release type '${releaseType}'`); + } + if (path.isAbsolute(versionFile) || versionFile.split(/[\\/]/u).includes('..')) { + fail(`${product}.version-file must stay inside release package path`); + } + const versionPath = path.join(root, packagePath, versionFile); + const text = await fs.readFile(versionPath, 'utf8'); + const fileName = path.basename(versionFile); + let version = ''; + if (fileName === 'Cargo.toml') { + version = parseCargoVersion(text); + } else if (fileName === 'package.json') { + version = JSON.parse(text).version ?? ''; + } else if (fileName === 'VERSION' || fileName === 'LIBOLIPHAUNT_VERSION') { + version = text.trim(); + } else { + fail(`${product}.version-file has unsupported version file type: ${versionFile}`); + } + if (typeof version !== 'string' || version.length === 0) { + fail(`${path.relative(root, versionPath)} does not define a release version for ${product}`); + } + return version; +} + +const { product, target } = parseArgs(Bun.argv.slice(2)); +const version = await currentProductVersion(product); +const tag = `${product}-v${version}`; +const targetCommit = commitForRef(target); +const existing = tagCommit(tag); +if (existing === null) { + fail(`${tag} does not exist. Run release-please before package-native publish steps.`); +} +if (existing !== targetCommit) { + fail(`${tag} points at ${existing}, not release commit ${targetCommit}`); +} +console.log(`${tag} points at ${targetCommit}`); diff --git a/tools/release/verify_product_tag.py b/tools/release/verify_product_tag.py deleted file mode 100755 index 4309aa17..00000000 --- a/tools/release/verify_product_tag.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 -"""Verify a product-scoped release-please tag points at the release commit.""" - -from __future__ import annotations - -import argparse -import os -import subprocess -import sys -from typing import NoReturn - -import product_metadata - - -def fail(message: str) -> NoReturn: - print(f"verify_product_tag.py: {message}", file=sys.stderr) - raise SystemExit(1) - - -def git_output(args: list[str]) -> str: - return subprocess.check_output(["git", *args], text=True).strip() - - -def commit_for_ref(ref: str) -> str: - return git_output(["rev-parse", f"{ref}^{{commit}}"]) - - -def tag_ref(tag: str) -> str: - return f"refs/tags/{tag}" - - -def tag_commit(tag: str) -> str | None: - result = subprocess.run( - ["git", "rev-parse", "--verify", "--quiet", f"{tag_ref(tag)}^{{commit}}"], - check=False, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - ) - if result.returncode == 0: - return result.stdout.strip() - return None - - -def product_tag(product: str) -> str: - prefix = product_metadata.tag_prefix(product) - version = product_metadata.read_current_version(product) - return f"{prefix}{version}" - - -def verify_tag(product: str, target: str) -> str: - tag = product_tag(product) - target_commit = commit_for_ref(target) - existing = tag_commit(tag) - if existing is None: - fail(f"{tag} does not exist. Run release-please before package-native publish steps.") - if existing != target_commit: - fail(f"{tag} points at {existing}, not release commit {target_commit}") - print(f"{tag} points at {target_commit}") - return tag - - -def parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("product", help="release product id") - parser.add_argument( - "--target", - default=os.environ.get("GITHUB_SHA", "HEAD"), - help="commitish that the tag must point at", - ) - return parser.parse_args(argv) - - -def main(argv: list[str]) -> int: - args = parse_args(argv) - verify_tag(args.product, args.target) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) From 4ad2e6bd9a84ee4571bd78d5bbf1986218b50204 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 08:42:24 +0000 Subject: [PATCH 053/137] fix: check release please config with bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 3 + tools/release/check_release_please_config.mjs | 288 ++++++++++++++++++ tools/release/check_release_please_config.py | 165 ---------- tools/release/release.py | 2 +- 4 files changed, 292 insertions(+), 166 deletions(-) create mode 100755 tools/release/check_release_please_config.mjs delete mode 100755 tools/release/check_release_please_config.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 02af59f6..544d1007 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -170,6 +170,9 @@ review production pipelines, then normalize implementation details. - Product release-tag verification now uses Bun instead of Python. The helper reads release-please product config, resolves the product's current version, and verifies the product-scoped tag points at the release commit. +- Release-please manifest-mode validation now uses Bun instead of Python. The + helper derives release products from Moon, validates release-please packages + and manifest paths, and checks product versions, changelogs, and extra files. - CI/release producer-to-consumer audit found no P0/P1 mapping gaps across Cargo, npm, Maven, SwiftPM, or GitHub release assets. Existing `release.py check`, artifact-target, release-metadata, consumer-shape, and diff --git a/tools/release/check_release_please_config.mjs b/tools/release/check_release_please_config.mjs new file mode 100755 index 00000000..d1a392fc --- /dev/null +++ b/tools/release/check_release_please_config.mjs @@ -0,0 +1,288 @@ +#!/usr/bin/env bun +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); +const configPath = path.join(root, 'release-please-config.json'); +const manifestPath = path.join(root, '.release-please-manifest.json'); +const decoder = new TextDecoder(); + +function fail(message) { + console.error(`check_release_please_config.mjs: ${message}`); + process.exit(2); +} + +function rel(file) { + return path.relative(root, file).split(path.sep).join('/'); +} + +async function readJson(file) { + let value; + try { + value = JSON.parse(await fs.readFile(file, 'utf8')); + } catch (error) { + fail(`failed to read ${rel(file)}: ${error.message}`); + } + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + fail(`${rel(file)} must contain a JSON object`); + } + return value; +} + +async function requireFile(file, context) { + try { + const stat = await fs.stat(file); + if (stat.isFile()) { + return; + } + } catch { + // handled below + } + fail(`${context} references missing file ${rel(file)}`); +} + +function rejectUnsafeRelativePath(value, context) { + if ( + typeof value !== 'string' || + value.length === 0 || + path.isAbsolute(value) || + value.split(/[\\/]/u).includes('..') + ) { + fail(`${context} must stay inside its release-please package path: ${JSON.stringify(value)}`); + } +} + +function moonBin() { + if (process.env.MOON_BIN) { + return process.env.MOON_BIN; + } + const protoBin = path.join(process.env.HOME ?? '', '.proto/bin/moon'); + return Bun.file(protoBin).exists() ? protoBin : 'moon'; +} + +function runMoonProjects() { + const result = Bun.spawnSync([moonBin(), 'query', 'projects'], { + cwd: root, + stdout: 'pipe', + stderr: 'pipe', + }); + if (result.exitCode !== 0) { + const stderr = decoder.decode(result.stderr).trim(); + fail(`moon query projects failed${stderr ? `: ${stderr}` : ''}`); + } + const value = JSON.parse(decoder.decode(result.stdout)); + if (!Array.isArray(value.projects)) { + fail('moon query projects did not return a projects array'); + } + return value.projects; +} + +function moonReleaseProducts() { + const products = new Map(); + for (const project of runMoonProjects()) { + const projectId = project?.id; + const config = project?.config ?? {}; + const tags = Array.isArray(config.tags) ? config.tags : []; + const release = config.project?.metadata?.release; + if (!tags.includes('release-product')) { + if (release !== undefined) { + fail(`Moon project ${projectId} declares release metadata but is not tagged release-product`); + } + continue; + } + if (typeof projectId !== 'string' || !projectId) { + fail('Moon release product must have a project id'); + } + if (typeof release !== 'object' || release === null || Array.isArray(release)) { + fail(`Moon release product ${projectId} must declare project.metadata.release`); + } + const component = release.component; + const packagePath = release.packagePath; + if (component !== projectId) { + fail(`Moon release product ${projectId} release.component must match the project id`); + } + if (typeof packagePath !== 'string' || !packagePath) { + fail(`Moon release product ${projectId} must declare release.packagePath`); + } + rejectUnsafeRelativePath(packagePath, `${projectId}.release.packagePath`); + if (products.has(component)) { + fail(`duplicate Moon release component ${component}`); + } + products.set(component, packagePath); + } + if (products.size === 0) { + fail('Moon project graph does not contain any release-product projects'); + } + return products; +} + +function parseCargoVersion(text) { + let inPackage = false; + for (const rawLine of text.split(/\r?\n/u)) { + const line = rawLine.trim(); + if (line === '[package]') { + inPackage = true; + continue; + } + if (inPackage && line.startsWith('[')) { + break; + } + if (!inPackage) { + continue; + } + const match = line.match(/^version\s*=\s*"([^"]+)"/u); + if (match) { + return match[1]; + } + } + return ''; +} + +function canonicalVersionFile(packagePath, packageConfig, product) { + const versionFile = packageConfig['version-file']; + if (versionFile !== undefined) { + if (typeof versionFile !== 'string' || !versionFile) { + fail(`${packagePath}.version-file must be a non-empty string`); + } + rejectUnsafeRelativePath(versionFile, `${packagePath}.version-file`); + return versionFile; + } + const releaseType = packageConfig['release-type']; + if (releaseType === 'rust') { + return 'Cargo.toml'; + } + if (releaseType === 'node' || releaseType === 'expo') { + return 'package.json'; + } + fail(`${product} release-please config must declare version-file for release type ${JSON.stringify(releaseType)}`); +} + +async function currentVersion(product, packagePath, packageConfig) { + const versionFile = canonicalVersionFile(packagePath, packageConfig, product); + const file = path.join(root, packagePath, versionFile); + await requireFile(file, `${packagePath}.version-file`); + const text = await fs.readFile(file, 'utf8'); + const name = path.basename(versionFile); + let version = ''; + if (name === 'Cargo.toml') { + version = parseCargoVersion(text); + } else if (name === 'package.json') { + const data = JSON.parse(text); + version = typeof data.version === 'string' ? data.version : ''; + } else if (name === 'VERSION' || name === 'LIBOLIPHAUNT_VERSION') { + version = text.trim(); + } else { + fail(`${product}.version-file has unsupported version file type: ${versionFile}`); + } + if (!version) { + fail(`${rel(file)} does not define a release version for ${product}`); + } + return version; +} + +async function validateExtraFiles(packagePath, packageConfig) { + const extraFiles = packageConfig['extra-files'] ?? []; + if (!Array.isArray(extraFiles)) { + fail(`${packagePath}.extra-files must be a list`); + } + for (const [index, entry] of extraFiles.entries()) { + const context = `${packagePath}.extra-files[${index}]`; + if (typeof entry === 'string') { + rejectUnsafeRelativePath(entry, context); + await requireFile(path.join(root, packagePath, entry), context); + continue; + } + if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) { + fail(`${context} must be a path string or object`); + } + const entryPath = entry.path; + if (typeof entryPath !== 'string' || !entryPath) { + fail(`${context}.path must be a non-empty string`); + } + rejectUnsafeRelativePath(entryPath, `${context}.path`); + await requireFile(path.join(root, packagePath, entryPath), context); + const entryType = entry.type; + if (['json', 'toml', 'yaml'].includes(entryType) && typeof entry.jsonpath !== 'string') { + fail(`${context} type ${JSON.stringify(entryType)} requires jsonpath`); + } + if (entryType === 'xml' && typeof entry.xpath !== 'string') { + fail(`${context} type 'xml' requires xpath`); + } + } +} + +const config = await readJson(configPath); +const manifest = await readJson(manifestPath); +const packages = config.packages; +if (typeof packages !== 'object' || packages === null || Array.isArray(packages) || Object.keys(packages).length === 0) { + fail('release-please-config.json must define non-empty packages'); +} + +const pathsById = moonReleaseProducts(); +const expectedPaths = new Set(pathsById.values()); +const actualPaths = new Set(Object.keys(packages)); +const manifestPaths = new Set(Object.keys(manifest)); +const sortedDifference = (left, right) => [...left].filter((item) => !right.has(item)).sort(); +if (actualPaths.size !== expectedPaths.size || sortedDifference(expectedPaths, actualPaths).length > 0) { + fail( + `release-please packages must match release products:\nmissing=${JSON.stringify(sortedDifference(expectedPaths, actualPaths))}\nextra=${JSON.stringify(sortedDifference(actualPaths, expectedPaths))}`, + ); +} +if (manifestPaths.size !== expectedPaths.size || sortedDifference(expectedPaths, manifestPaths).length > 0) { + fail( + `.release-please-manifest.json paths must match release products:\nmissing=${JSON.stringify(sortedDifference(expectedPaths, manifestPaths))}\nextra=${JSON.stringify(sortedDifference(manifestPaths, expectedPaths))}`, + ); +} + +if (config['tag-separator'] !== '-') { + fail("release-please tag-separator must be '-' for -v tags"); +} +if (config['include-v-in-tag'] !== true) { + fail('release-please must include v in tags'); +} +if (config['pull-request-title-pattern'] !== 'chore${scope}: release${component} ${version}') { + fail("release-please pull-request-title-pattern must keep release-please's parseable default shape"); +} +if (config['initial-version'] !== '0.1.0') { + fail('release-please initial-version must bootstrap the first generated release PR to 0.1.0'); +} +if (config['bump-minor-pre-major'] !== true) { + fail('release-please must minor-bump breaking changes while product versions are below 1.0.0'); +} +if (config['bump-patch-for-minor-pre-major'] !== true) { + fail('release-please must patch-bump feat commits after the 0.1.0 bootstrap while versions stay below 1.0.0'); +} +if (JSON.stringify(config.plugins ?? []) !== JSON.stringify(['node-workspace'])) { + fail('release-please plugins must stay minimal: use node-workspace only'); +} + +const idsByPath = new Map([...pathsById.entries()].map(([product, packagePath]) => [packagePath, product])); +for (const [packagePath, packageConfig] of Object.entries(packages)) { + if (typeof packageConfig !== 'object' || packageConfig === null || Array.isArray(packageConfig)) { + fail(`${packagePath} config must be an object`); + } + const product = idsByPath.get(packagePath); + const component = packageConfig.component; + if (component !== product) { + fail(`${packagePath}.component must be ${JSON.stringify(product)}, got ${JSON.stringify(component)}`); + } + const tagPrefix = `${component}-v`; + if (tagPrefix !== `${product}-v`) { + fail(`${product} release-please component does not match tag prefix ${JSON.stringify(tagPrefix)}`); + } + const manifestVersion = manifest[packagePath]; + const version = await currentVersion(product, packagePath, packageConfig); + if (manifestVersion !== version) { + fail(`${packagePath} manifest version ${JSON.stringify(manifestVersion)} does not match current ${product} version ${JSON.stringify(version)}`); + } + const changelogPath = packageConfig['changelog-path'] ?? 'CHANGELOG.md'; + if (typeof changelogPath !== 'string' || !changelogPath) { + fail(`${packagePath}.changelog-path must be a non-empty string`); + } + rejectUnsafeRelativePath(changelogPath, `${packagePath}.changelog-path`); + await requireFile(path.join(root, packagePath, changelogPath), `${packagePath}.changelog-path`); + await validateExtraFiles(packagePath, packageConfig); +} + +console.log('release-please config checks passed'); diff --git a/tools/release/check_release_please_config.py b/tools/release/check_release_please_config.py deleted file mode 100755 index 323b3c33..00000000 --- a/tools/release/check_release_please_config.py +++ /dev/null @@ -1,165 +0,0 @@ -#!/usr/bin/env python3 -"""Validate release-please manifest-mode configuration. - -This is a transition guard while release-please becomes the version, changelog, -and tag owner. It checks the standard release-please files against current -product versions without re-implementing release planning. -""" - -from __future__ import annotations - -import json -import sys -from pathlib import Path -from typing import Any, NoReturn - -import product_metadata - - -ROOT = Path(__file__).resolve().parents[2] -CONFIG_PATH = ROOT / "release-please-config.json" -MANIFEST_PATH = ROOT / ".release-please-manifest.json" - - -def fail(message: str) -> NoReturn: - print(f"check_release_please_config.py: {message}", file=sys.stderr) - raise SystemExit(2) - - -def rel(path: Path) -> str: - return path.relative_to(ROOT).as_posix() - - -def read_json(path: Path) -> dict[str, Any]: - if not path.is_file(): - fail(f"missing {rel(path)}") - with path.open(encoding="utf-8") as handle: - value = json.load(handle) - if not isinstance(value, dict): - fail(f"{rel(path)} must contain a JSON object") - return value - - -def require_file(path: Path, context: str) -> None: - if not path.is_file(): - fail(f"{context} references missing file {rel(path)}") - - -def reject_unsafe_relative_path(value: str, context: str) -> None: - parts = Path(value).parts - if Path(value).is_absolute() or ".." in parts: - fail(f"{context} must stay inside its release-please package path: {value!r}") - - -def package_version_file(package_path: str, package_config: dict[str, Any]) -> Path | None: - version_file = package_config.get("version-file") - if version_file is None: - return None - if not isinstance(version_file, str) or not version_file: - fail(f"{package_path}.version-file must be a non-empty string") - return ROOT / package_path / version_file - - -def read_raw_version(path: Path) -> str: - require_file(path, "release-please version-file") - return path.read_text(encoding="utf-8").strip() - - -def validate_extra_files(package_path: str, package_config: dict[str, Any]) -> None: - extra_files = package_config.get("extra-files", []) - if not isinstance(extra_files, list): - fail(f"{package_path}.extra-files must be a list") - for index, entry in enumerate(extra_files): - context = f"{package_path}.extra-files[{index}]" - if isinstance(entry, str): - reject_unsafe_relative_path(entry, context) - require_file(ROOT / package_path / entry, context) - continue - if not isinstance(entry, dict): - fail(f"{context} must be a path string or object") - path = entry.get("path") - if not isinstance(path, str) or not path: - fail(f"{context}.path must be a non-empty string") - reject_unsafe_relative_path(path, f"{context}.path") - require_file(ROOT / package_path / path, context) - entry_type = entry.get("type") - if entry_type in {"json", "toml", "yaml"} and not isinstance(entry.get("jsonpath"), str): - fail(f"{context} type {entry_type!r} requires jsonpath") - if entry_type == "xml" and not isinstance(entry.get("xpath"), str): - fail(f"{context} type 'xml' requires xpath") - - -def main() -> int: - config = read_json(CONFIG_PATH) - manifest = read_json(MANIFEST_PATH) - packages = config.get("packages") - if not isinstance(packages, dict) or not packages: - fail("release-please-config.json must define non-empty packages") - - products = product_metadata.graph_products() - paths_by_id = {product: product_metadata.package_path(product) for product in products} - expected_paths = {paths_by_id[product] for product in products} - actual_paths = set(packages) - if actual_paths != expected_paths: - fail( - "release-please packages must match release products:\n" - f"missing={sorted(expected_paths - actual_paths)}\n" - f"extra={sorted(actual_paths - expected_paths)}" - ) - if set(manifest) != expected_paths: - fail( - ".release-please-manifest.json paths must match release products:\n" - f"missing={sorted(expected_paths - set(manifest))}\n" - f"extra={sorted(set(manifest) - expected_paths)}" - ) - - if config.get("tag-separator") != "-": - fail("release-please tag-separator must be '-' for -v tags") - if config.get("include-v-in-tag") is not True: - fail("release-please must include v in tags") - if config.get("pull-request-title-pattern") != "chore${scope}: release${component} ${version}": - fail("release-please pull-request-title-pattern must keep release-please's parseable default shape") - if config.get("initial-version") != "0.1.0": - fail("release-please initial-version must bootstrap the first generated release PR to 0.1.0") - if config.get("bump-minor-pre-major") is not True: - fail("release-please must minor-bump breaking changes while product versions are below 1.0.0") - if config.get("bump-patch-for-minor-pre-major") is not True: - fail("release-please must patch-bump feat commits after the 0.1.0 bootstrap while versions stay below 1.0.0") - plugins = config.get("plugins", []) - if plugins != ["node-workspace"]: - fail("release-please plugins must stay minimal: use node-workspace only") - - ids_by_path = {path: product for product, path in paths_by_id.items()} - for package_path, package_config in packages.items(): - if not isinstance(package_config, dict): - fail(f"{package_path} config must be an object") - product = ids_by_path[package_path] - component = package_config.get("component") - if component != product: - fail(f"{package_path}.component must be {product!r}, got {component!r}") - tag_prefix = product_metadata.tag_prefix(product) - if tag_prefix != f"{component}-v": - fail(f"{product} release-please component does not match tag prefix {tag_prefix!r}") - manifest_version = manifest.get(package_path) - current_version = product_metadata.read_current_version(product) - if manifest_version != current_version: - fail( - f"{package_path} manifest version {manifest_version!r} " - f"does not match current {product} version {current_version!r}" - ) - changelog_path = package_config.get("changelog-path", "CHANGELOG.md") - if not isinstance(changelog_path, str) or not changelog_path: - fail(f"{package_path}.changelog-path must be a non-empty string") - reject_unsafe_relative_path(changelog_path, f"{package_path}.changelog-path") - require_file(ROOT / package_path / changelog_path, f"{package_path}.changelog-path") - version_file = package_version_file(package_path, package_config) - if version_file is not None and read_raw_version(version_file) != current_version: - fail(f"{rel(version_file)} must match current {product} version {current_version}") - validate_extra_files(package_path, package_config) - - print("release-please config checks passed") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/release/release.py b/tools/release/release.py index 8192bc8a..2956ecbb 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -1672,7 +1672,7 @@ def command_plan(args: list[str]) -> None: def command_check(args: list[str]) -> None: run(["python3", "tools/policy/check-release-policy.py"]) - run(["python3", "tools/release/check_release_please_config.py"]) + run(["tools/release/check_release_please_config.mjs"]) run(["python3", "tools/release/check_artifact_targets.py"]) run(["tools/release/sync_release_pr.py", "--check"]) run(["python3", "tools/release/check_release_pr_coverage.py"]) From db795333bd78cf3267f5f251f288be2c763c9cce Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 08:52:01 +0000 Subject: [PATCH 054/137] fix: archive release directories with bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 4 + tools/release/archive_dir.mjs | 269 ++++++++++++++++++ tools/release/archive_dir.py | 114 -------- tools/release/package-broker-assets.sh | 2 +- .../package-liboliphaunt-linux-assets.sh | 3 +- .../package-liboliphaunt-macos-assets.sh | 3 +- .../package-liboliphaunt-mobile-assets.sh | 3 +- .../package-liboliphaunt-windows-assets.ps1 | 6 +- 8 files changed, 285 insertions(+), 119 deletions(-) create mode 100755 tools/release/archive_dir.mjs delete mode 100755 tools/release/archive_dir.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 544d1007..e74c5321 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -173,6 +173,10 @@ review production pipelines, then normalize implementation details. - Release-please manifest-mode validation now uses Bun instead of Python. The helper derives release products from Moon, validates release-please packages and manifest paths, and checks product versions, changelogs, and extra files. +- Deterministic release directory archiving now uses Bun instead of Python for + tar.gz and zip payloads. Native, mobile, broker, and Windows package scripts + now call the Bun helper while preserving fixed timestamps, modes, and sorted + entries. - CI/release producer-to-consumer audit found no P0/P1 mapping gaps across Cargo, npm, Maven, SwiftPM, or GitHub release assets. Existing `release.py check`, artifact-target, release-metadata, consumer-shape, and diff --git a/tools/release/archive_dir.mjs b/tools/release/archive_dir.mjs new file mode 100755 index 00000000..b6338e40 --- /dev/null +++ b/tools/release/archive_dir.mjs @@ -0,0 +1,269 @@ +#!/usr/bin/env bun +import { deflateRawSync, gzipSync } from 'node:zlib'; +import fs from 'node:fs/promises'; +import path from 'node:path'; + +function fail(message) { + console.error(`archive_dir.mjs: ${message}`); + process.exit(2); +} + +function compareText(left, right) { + return left < right ? -1 : left > right ? 1 : 0; +} + +function normalizedMode(stat, isDirectory) { + if (isDirectory) { + return 0o755; + } + return stat.mode & 0o100 ? 0o755 : 0o644; +} + +function posixRelative(root, item) { + const relative = path.relative(root, item).split(path.sep).join('/'); + return relative === '' ? '.' : relative; +} + +async function archiveEntries(root) { + const entries = [{ fullPath: root, name: '.', isDirectory: true }]; + + async function walk(directory) { + const dirents = await fs.readdir(directory, { withFileTypes: true }); + const directories = []; + const files = []; + for (const entry of dirents) { + const fullPath = path.join(directory, entry.name); + const stat = await fs.stat(fullPath); + if (stat.isDirectory()) { + directories.push({ entry, fullPath, recurse: !entry.isSymbolicLink() }); + } else if (stat.isFile()) { + files.push({ entry, fullPath }); + } + } + directories.sort((left, right) => compareText(left.entry.name, right.entry.name)); + files.sort((left, right) => compareText(left.entry.name, right.entry.name)); + for (const entry of directories) { + entries.push({ fullPath: entry.fullPath, name: posixRelative(root, entry.fullPath), isDirectory: true }); + } + for (const entry of files) { + entries.push({ fullPath: entry.fullPath, name: posixRelative(root, entry.fullPath), isDirectory: false }); + } + for (const entry of directories) { + if (entry.recurse) { + await walk(entry.fullPath); + } + } + } + + await walk(root); + return entries; +} + +function tarPathParts(relativePath) { + if (Buffer.byteLength(relativePath) <= 100) { + return { name: relativePath, prefix: '' }; + } + const parts = relativePath.split('/'); + for (let index = 1; index < parts.length; index += 1) { + const prefix = parts.slice(0, index).join('/'); + const name = parts.slice(index).join('/'); + if (Buffer.byteLength(prefix) <= 155 && Buffer.byteLength(name) <= 100) { + return { name, prefix }; + } + } + fail(`archive path is too long for ustar: ${relativePath}`); +} + +function writeString(buffer, offset, length, value) { + const bytes = Buffer.from(value); + if (bytes.length > length) { + fail(`tar header field overflow for '${value}'`); + } + bytes.copy(buffer, offset); +} + +function writeOctal(buffer, offset, length, value) { + const text = value.toString(8); + if (text.length > length - 1) { + fail(`tar header octal field overflow for '${value}'`); + } + writeString(buffer, offset, length, `${text.padStart(length - 1, '0')}\0`); +} + +function tarHeader(entry, size, mode) { + const header = Buffer.alloc(512, 0); + const { name, prefix } = tarPathParts(entry.name); + writeString(header, 0, 100, name); + writeOctal(header, 100, 8, mode); + writeOctal(header, 108, 8, 0); + writeOctal(header, 116, 8, 0); + writeOctal(header, 124, 12, size); + writeOctal(header, 136, 12, 0); + header.fill(0x20, 148, 156); + writeString(header, 156, 1, entry.isDirectory ? '5' : '0'); + writeString(header, 257, 6, 'ustar\0'); + writeString(header, 263, 2, '00'); + writeString(header, 345, 155, prefix); + let checksum = 0; + for (const byte of header) { + checksum += byte; + } + const checksumText = checksum.toString(8); + if (checksumText.length > 6) { + fail(`tar header checksum overflow for ${entry.name}`); + } + writeString(header, 148, 8, `${checksumText.padStart(6, '0')}\0 `); + return header; +} + +async function createTar(root) { + const chunks = []; + for (const entry of await archiveEntries(root)) { + const stat = await fs.stat(entry.fullPath); + const mode = normalizedMode(stat, entry.isDirectory); + const data = entry.isDirectory ? Buffer.alloc(0) : await fs.readFile(entry.fullPath); + chunks.push(tarHeader(entry, data.length, mode)); + if (data.length > 0) { + chunks.push(data); + const remainder = data.length % 512; + if (remainder !== 0) { + chunks.push(Buffer.alloc(512 - remainder, 0)); + } + } + } + chunks.push(Buffer.alloc(1024, 0)); + return Buffer.concat(chunks); +} + +const crcTable = new Uint32Array(256); +for (let index = 0; index < crcTable.length; index += 1) { + let value = index; + for (let bit = 0; bit < 8; bit += 1) { + value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1; + } + crcTable[index] = value >>> 0; +} + +function crc32(data) { + let crc = 0xffffffff; + for (const byte of data) { + crc = crcTable[(crc ^ byte) & 0xff] ^ (crc >>> 8); + } + return (crc ^ 0xffffffff) >>> 0; +} + +function dosDateTime() { + return { + time: 0, + date: ((1980 - 1980) << 9) | (1 << 5) | 1, + }; +} + +function writeUInt16(value) { + const buffer = Buffer.alloc(2); + buffer.writeUInt16LE(value); + return buffer; +} + +function writeUInt32(value) { + const buffer = Buffer.alloc(4); + buffer.writeUInt32LE(value >>> 0); + return buffer; +} + +function zipName(entry) { + return entry.isDirectory && entry.name !== '.' ? `${entry.name}/` : entry.name; +} + +async function createZip(root) { + const localChunks = []; + const centralChunks = []; + let offset = 0; + const { time, date } = dosDateTime(); + + for (const entry of await archiveEntries(root)) { + const stat = await fs.stat(entry.fullPath); + const mode = normalizedMode(stat, entry.isDirectory); + const name = Buffer.from(zipName(entry)); + const data = entry.isDirectory ? Buffer.alloc(0) : await fs.readFile(entry.fullPath); + const compressed = entry.isDirectory ? Buffer.alloc(0) : deflateRawSync(data, { level: 9 }); + const method = entry.isDirectory ? 0 : 8; + const crc = crc32(data); + const externalAttributes = ((mode & 0o777) << 16) | (entry.isDirectory ? 0x10 : 0); + const localHeader = Buffer.concat([ + writeUInt32(0x04034b50), + writeUInt16(20), + writeUInt16(0), + writeUInt16(method), + writeUInt16(time), + writeUInt16(date), + writeUInt32(crc), + writeUInt32(compressed.length), + writeUInt32(data.length), + writeUInt16(name.length), + writeUInt16(0), + name, + ]); + localChunks.push(localHeader, compressed); + centralChunks.push( + Buffer.concat([ + writeUInt32(0x02014b50), + writeUInt16((3 << 8) | 20), + writeUInt16(20), + writeUInt16(0), + writeUInt16(method), + writeUInt16(time), + writeUInt16(date), + writeUInt32(crc), + writeUInt32(compressed.length), + writeUInt32(data.length), + writeUInt16(name.length), + writeUInt16(0), + writeUInt16(0), + writeUInt16(0), + writeUInt16(0), + writeUInt32(externalAttributes), + writeUInt32(offset), + name, + ]), + ); + offset += localHeader.length + compressed.length; + } + + const centralDirectory = Buffer.concat(centralChunks); + const end = Buffer.concat([ + writeUInt32(0x06054b50), + writeUInt16(0), + writeUInt16(0), + writeUInt16(centralChunks.length), + writeUInt16(centralChunks.length), + writeUInt32(centralDirectory.length), + writeUInt32(offset), + writeUInt16(0), + ]); + return Buffer.concat([...localChunks, centralDirectory, end]); +} + +function parseArgs(argv) { + if (argv.length !== 2) { + fail('usage: tools/release/archive_dir.mjs '); + } + return { + source: path.resolve(argv[0]), + output: path.resolve(argv[1]), + }; +} + +const { source, output } = parseArgs(Bun.argv.slice(2)); +const sourceStat = await fs.stat(source).catch(() => null); +if (!sourceStat?.isDirectory()) { + fail(`source is not a directory: ${source}`); +} +await fs.mkdir(path.dirname(output), { recursive: true }); +if (output.endsWith('.tar.gz')) { + await fs.writeFile(output, gzipSync(await createTar(source), { mtime: 0 })); +} else if (path.extname(output) === '.zip') { + await fs.writeFile(output, await createZip(source)); +} else { + fail(`unsupported archive extension: ${output}`); +} diff --git a/tools/release/archive_dir.py b/tools/release/archive_dir.py deleted file mode 100755 index 99fe5b8b..00000000 --- a/tools/release/archive_dir.py +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env python3 -"""Create a deterministic tar.gz or zip archive from a directory.""" - -from __future__ import annotations - -import gzip -import os -import stat -import sys -import tarfile -import zipfile -from pathlib import Path -from typing import NoReturn - - -def fail(message: str) -> "NoReturn": - print(f"archive_dir.py: {message}", file=sys.stderr) - raise SystemExit(2) - - -def normalized_mode(path: Path) -> int: - mode = path.stat().st_mode - if path.is_dir(): - return stat.S_IFDIR | 0o755 - executable = bool(mode & stat.S_IXUSR) - return stat.S_IFREG | (0o755 if executable else 0o644) - - -def add_path(archive: tarfile.TarFile, root: Path, path: Path) -> None: - relative = path.relative_to(root) - name = "." if str(relative) == "." else relative.as_posix() - info = tarfile.TarInfo(name) - info.uid = 0 - info.gid = 0 - info.uname = "" - info.gname = "" - info.mtime = 0 - info.mode = normalized_mode(path) & 0o777 - if path.is_dir(): - info.type = tarfile.DIRTYPE - archive.addfile(info) - return - if not path.is_file(): - fail(f"unsupported archive entry type: {path}") - info.size = path.stat().st_size - with path.open("rb") as file: - archive.addfile(info, file) - - -def add_zip_path(archive: zipfile.ZipFile, root: Path, path: Path) -> None: - relative = path.relative_to(root) - name = "." if str(relative) == "." else relative.as_posix() - if path.is_dir() and name != ".": - name = f"{name}/" - info = zipfile.ZipInfo(name) - info.date_time = (1980, 1, 1, 0, 0, 0) - info.create_system = 3 - info.external_attr = (normalized_mode(path) & 0o777) << 16 - if path.is_dir(): - info.external_attr |= 0x10 - archive.writestr(info, b"") - return - if not path.is_file(): - fail(f"unsupported archive entry type: {path}") - info.compress_type = zipfile.ZIP_DEFLATED - with path.open("rb") as file: - archive.writestr(info, file.read()) - - -def write_tar_gz(source: Path, output: Path) -> None: - with output.open("wb") as raw: - with gzip.GzipFile(filename="", mode="wb", fileobj=raw, mtime=0) as gzip_file: - with tarfile.open(fileobj=gzip_file, mode="w") as archive: - add_path(archive, source, source) - for directory, dirnames, filenames in os.walk(source): - dirnames.sort() - filenames.sort() - for dirname in dirnames: - add_path(archive, source, Path(directory) / dirname) - for filename in filenames: - add_path(archive, source, Path(directory) / filename) - - -def write_zip(source: Path, output: Path) -> None: - with zipfile.ZipFile(output, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive: - add_zip_path(archive, source, source) - for directory, dirnames, filenames in os.walk(source): - dirnames.sort() - filenames.sort() - for dirname in dirnames: - add_zip_path(archive, source, Path(directory) / dirname) - for filename in filenames: - add_zip_path(archive, source, Path(directory) / filename) - - -def main(argv: list[str]) -> int: - if len(argv) != 3: - fail("usage: tools/release/archive_dir.py ") - source = Path(argv[1]).resolve() - output = Path(argv[2]).resolve() - if not source.is_dir(): - fail(f"source is not a directory: {source}") - output.parent.mkdir(parents=True, exist_ok=True) - if output.name.endswith(".tar.gz"): - write_tar_gz(source, output) - elif output.suffix == ".zip": - write_zip(source, output) - else: - fail(f"unsupported archive extension: {output}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv)) diff --git a/tools/release/package-broker-assets.sh b/tools/release/package-broker-assets.sh index c3d4e5e7..bf4c90e5 100755 --- a/tools/release/package-broker-assets.sh +++ b/tools/release/package-broker-assets.sh @@ -72,7 +72,7 @@ target=$target_id binary=bin/$broker_stage_name EOF -tools/release/archive_dir.py "$stage" "$out_dir/$asset" +tools/release/archive_dir.mjs "$stage" "$out_dir/$asset" input_dirs="${OLIPHAUNT_BROKER_RELEASE_ASSET_INPUT_DIRS:-${OLIPHAUNT_RELEASE_ASSET_INPUT_DIRS:-}}" if [ -n "$input_dirs" ]; then diff --git a/tools/release/package-liboliphaunt-linux-assets.sh b/tools/release/package-liboliphaunt-linux-assets.sh index 23595ad9..cc07c782 100755 --- a/tools/release/package-liboliphaunt-linux-assets.sh +++ b/tools/release/package-liboliphaunt-linux-assets.sh @@ -37,6 +37,7 @@ case "$(uname -m)" in esac require cargo +require bun require python3 version="$(python3 tools/release/product_metadata.py version liboliphaunt-native)" @@ -87,5 +88,5 @@ env \ OLIPHAUNT_SMOKE_ROOT="$stage_root/smoke-root-$target_id" \ node src/runtimes/liboliphaunt/native/tools/run-host-c-smoke.mjs -tools/release/archive_dir.py "$stage" "$out_dir/$asset" +tools/release/archive_dir.mjs "$stage" "$out_dir/$asset" echo "liboliphauntLinuxReleaseAsset=$out_dir/$asset" diff --git a/tools/release/package-liboliphaunt-macos-assets.sh b/tools/release/package-liboliphaunt-macos-assets.sh index 81d3e5d8..46b0032f 100755 --- a/tools/release/package-liboliphaunt-macos-assets.sh +++ b/tools/release/package-liboliphaunt-macos-assets.sh @@ -31,6 +31,7 @@ case "$(uname -m)" in esac version="$(python3 tools/release/product_metadata.py version liboliphaunt-native)" +command -v bun >/dev/null 2>&1 || fail "missing required command: bun" out_dir="${OLIPHAUNT_LIBOLIPHAUNT_RELEASE_ASSETS:-$root/target/liboliphaunt/release-assets}" stage_root="$root/target/liboliphaunt/release-stage-$target_id" work_root="${OLIPHAUNT_WORK_ROOT:-$root/target/liboliphaunt-pg18}" @@ -79,5 +80,5 @@ env \ OLIPHAUNT_SMOKE_ROOT="$stage_root/smoke-root-$target_id" \ node src/runtimes/liboliphaunt/native/tools/run-host-c-smoke.mjs -tools/release/archive_dir.py "$stage" "$out_dir/$asset" +tools/release/archive_dir.mjs "$stage" "$out_dir/$asset" echo "liboliphauntMacosReleaseAsset=$out_dir/$asset" diff --git a/tools/release/package-liboliphaunt-mobile-assets.sh b/tools/release/package-liboliphaunt-mobile-assets.sh index 3734bea2..b4afcd11 100755 --- a/tools/release/package-liboliphaunt-mobile-assets.sh +++ b/tools/release/package-liboliphaunt-mobile-assets.sh @@ -19,6 +19,7 @@ require() { source "$root/tools/release/liboliphaunt-extension-guard.sh" require cargo +require bun require python3 require rsync @@ -47,7 +48,7 @@ archive_staged_dir() { local staged="$1" local name name="$(basename "$staged")" - tools/release/archive_dir.py "$staged" "$out_dir/${name}.tar.gz" + tools/release/archive_dir.mjs "$staged" "$out_dir/${name}.tar.gz" } archive_swiftpm_xcframework() { diff --git a/tools/release/package-liboliphaunt-windows-assets.ps1 b/tools/release/package-liboliphaunt-windows-assets.ps1 index b01cd2af..6af8e068 100644 --- a/tools/release/package-liboliphaunt-windows-assets.ps1 +++ b/tools/release/package-liboliphaunt-windows-assets.ps1 @@ -58,6 +58,10 @@ if (-not $IsWindows) { Fail "Windows liboliphaunt release assets must be built on Windows" } +if (-not (Get-Command bun -ErrorAction SilentlyContinue)) { + Fail "missing required command: bun" +} + if ($env:OLIPHAUNT_RELEASE_FETCH_ASSETS -ne "0") { Write-Output "==> Fetching pinned source assets" bun tools/policy/fetch-sources.mjs native-runtime *> "$env:TEMP\liboliphaunt-release-windows-assets-fetch.log" @@ -157,7 +161,7 @@ if ($LASTEXITCODE -ne 0) { Fail "staged Windows liboliphaunt release smoke failed" } -python tools/release/archive_dir.py $Stage (Join-Path $OutDir $Asset) +bun tools/release/archive_dir.mjs $Stage (Join-Path $OutDir $Asset) if ($LASTEXITCODE -ne 0) { Fail "failed to archive Windows liboliphaunt asset" } From e405eb14d43b354c41b23377d667cdbe3db8dbed Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 09:00:52 +0000 Subject: [PATCH 055/137] fix: sync example lockfiles with bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 3 + docs/internal/IMPLEMENTATION_CHECKLIST.md | 2 +- examples/tools/check-lockfiles.sh | 4 +- tools/release/sync-example-lockfiles.mjs | 216 ++++++++++++++++++ tools/release/sync-example-lockfiles.py | 175 -------------- 5 files changed, 222 insertions(+), 178 deletions(-) create mode 100755 tools/release/sync-example-lockfiles.mjs delete mode 100755 tools/release/sync-example-lockfiles.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index e74c5321..7800b0b5 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -177,6 +177,9 @@ review production pipelines, then normalize implementation details. tar.gz and zip payloads. Native, mobile, broker, and Windows package scripts now call the Bun helper while preserving fixed timestamps, modes, and sorted entries. +- WASIX example Cargo lockfile synchronization now uses Bun instead of Python, + keeping the nested Tauri SQLx example aligned with local internal WASIX crate + versions without invoking Cargo when only source-tree versions changed. - CI/release producer-to-consumer audit found no P0/P1 mapping gaps across Cargo, npm, Maven, SwiftPM, or GitHub release assets. Existing `release.py check`, artifact-target, release-metadata, consumer-shape, and diff --git a/docs/internal/IMPLEMENTATION_CHECKLIST.md b/docs/internal/IMPLEMENTATION_CHECKLIST.md index 3d25988b..522fb069 100644 --- a/docs/internal/IMPLEMENTATION_CHECKLIST.md +++ b/docs/internal/IMPLEMENTATION_CHECKLIST.md @@ -992,7 +992,7 @@ Run before claiming this architecture complete: crates, pins `oliphaunt-wasix` runtime crate dependencies to `=0.6.0`, refreshes root and Tauri example lockfiles, and updates the optional perf-runner dependency. Local checks passed after the bump: `tools/release/release.py - check`, `tools/release/sync-example-lockfiles.py --check`, `cargo metadata + check`, `tools/release/sync-example-lockfiles.mjs --check`, `cargo metadata --locked --format-version 1 --no-deps`, `tools/release/release.py check-registries --products-json "$(cat target/release-dry-run-local/products.json)" --head-ref HEAD`, and diff --git a/examples/tools/check-lockfiles.sh b/examples/tools/check-lockfiles.sh index 54f68a25..e58beca3 100755 --- a/examples/tools/check-lockfiles.sh +++ b/examples/tools/check-lockfiles.sh @@ -39,7 +39,7 @@ changed="$( examples/electron-wasix/src-wasix/Cargo.toml \ examples/electron-wasix/src-wasix/Cargo.lock \ examples/tools/check-lockfiles.sh \ - tools/release/sync-example-lockfiles.py + tools/release/sync-example-lockfiles.mjs )" if [[ -z "$changed" ]]; then @@ -47,4 +47,4 @@ if [[ -z "$changed" ]]; then exit 0 fi -tools/release/sync-example-lockfiles.py --check +tools/release/sync-example-lockfiles.mjs --check diff --git a/tools/release/sync-example-lockfiles.mjs b/tools/release/sync-example-lockfiles.mjs new file mode 100755 index 00000000..d1cb464a --- /dev/null +++ b/tools/release/sync-example-lockfiles.mjs @@ -0,0 +1,216 @@ +#!/usr/bin/env bun +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); +const lockfiles = [ + 'src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock', +]; +const internalPackageManifests = [ + 'src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml', + 'src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml', + 'src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml', + 'src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/Cargo.toml', + 'src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/Cargo.toml', + 'src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/Cargo.toml', + 'src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/Cargo.toml', + 'src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/Cargo.toml', + 'src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/Cargo.toml', + 'src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/Cargo.toml', + 'src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/Cargo.toml', +]; +const packageStartRe = /^\s*\[\[package\]\]\s*$/u; +const stringKeyRe = /^\s*([A-Za-z0-9_-]+)\s*=\s*"([^"]*)"\s*(?:#.*)?$/u; +const versionLineRe = /^(\s*version\s*=\s*)"[^"]*"(\s*(?:#.*)?)$/u; + +function fail(message) { + console.error(message); + process.exit(1); +} + +function rel(file) { + return path.relative(root, file).split(path.sep).join('/'); +} + +async function loadInternalVersions() { + const versions = new Map(); + for (const relative of internalPackageManifests) { + const manifest = path.join(root, relative); + const data = Bun.TOML.parse(await fs.readFile(manifest, 'utf8')); + const pkg = data.package; + if (typeof pkg !== 'object' || pkg === null || Array.isArray(pkg)) { + fail(`${relative} is missing [package]`); + } + const { name, version } = pkg; + if (typeof name !== 'string' || typeof version !== 'string') { + fail(`${relative} is missing package.name/version`); + } + versions.set(name, version); + } + return versions; +} + +function stripNewline(line) { + if (line.endsWith('\r\n')) { + return [line.slice(0, -2), '\r\n']; + } + if (line.endsWith('\n')) { + return [line.slice(0, -1), '\n']; + } + return [line, '']; +} + +function stringKey(line, key) { + const [body] = stripNewline(line); + const match = body.match(stringKeyRe); + return match?.[1] === key ? match[2] : null; +} + +function replaceVersionLine(line, version) { + const [body, newline] = stripNewline(line); + const match = body.match(versionLineRe); + if (!match) { + fail(`cannot update Cargo.lock version line: ${line.trimEnd()}`); + } + return `${match[1]}"${version}"${match[2]}${newline}`; +} + +function packageBlockRanges(lines) { + const starts = []; + for (const [index, line] of lines.entries()) { + if (packageStartRe.test(line)) { + starts.push(index); + } + } + return starts.map((start, index) => [start, index + 1 < starts.length ? starts[index + 1] : lines.length]); +} + +function splitLinesKeepEnds(text) { + const lines = []; + let start = 0; + for (let index = 0; index < text.length; index += 1) { + if (text[index] === '\n') { + lines.push(text.slice(start, index + 1)); + start = index + 1; + } + } + if (start < text.length) { + lines.push(text.slice(start)); + } + return lines; +} + +async function checkLockfileContainsInternalPackages(lockfile, versions) { + const data = Bun.TOML.parse(await fs.readFile(lockfile, 'utf8')); + if (!Array.isArray(data.package)) { + fail(`${rel(lockfile)} is missing [[package]] entries`); + } + const present = new Set( + data.package + .filter((pkg) => typeof pkg === 'object' && pkg !== null && typeof pkg.name === 'string') + .map((pkg) => pkg.name), + ); + const missing = [...versions.keys()].filter((name) => !present.has(name)).sort(); + if (missing.length > 0) { + fail(`${rel(lockfile)} is missing internal Oliphaunt packages: ${missing.join(', ')}`); + } +} + +async function syncLockfile(lockfile, versions, { check }) { + await checkLockfileContainsInternalPackages(lockfile, versions); + const text = await fs.readFile(lockfile, 'utf8'); + const lines = splitLinesKeepEnds(text); + const changes = []; + const registryChanges = []; + + for (const [start, end] of packageBlockRanges(lines)) { + const block = lines.slice(start, end); + let name = null; + let versionIndex = null; + let currentVersion = null; + let hasSource = false; + + for (const [offset, line] of block.entries()) { + if (stringKey(line, 'source') !== null) { + hasSource = true; + } + const keyName = stringKey(line, 'name'); + if (keyName !== null) { + name = keyName; + } + const keyVersion = stringKey(line, 'version'); + if (keyVersion !== null) { + versionIndex = start + offset; + currentVersion = keyVersion; + } + } + + if (!versions.has(name) || hasSource) { + continue; + } + if (versionIndex === null || currentVersion === null) { + fail(`${rel(lockfile)} package ${name} is missing version`); + } + + const expectedVersion = versions.get(name); + if (currentVersion !== expectedVersion) { + if (hasSource) { + registryChanges.push(`${rel(lockfile)}: ${name} ${currentVersion} -> ${expectedVersion}`); + continue; + } + if (!check) { + lines[versionIndex] = replaceVersionLine(lines[versionIndex], expectedVersion); + } + changes.push(`${rel(lockfile)}: ${name} ${currentVersion} -> ${expectedVersion}`); + } + } + + if (registryChanges.length > 0) { + for (const change of registryChanges) { + console.error(change); + } + fail( + 'registry-sourced example lockfiles are stale; run Cargo update through `examples/tools/with-local-registries.sh` after staging the local registry', + ); + } + if (changes.length > 0 && !check) { + await fs.writeFile(lockfile, lines.join('')); + } + return changes; +} + +function parseArgs(argv) { + let check = false; + for (const arg of argv) { + if (arg === '--check') { + check = true; + } else { + fail(`unknown argument: ${arg}`); + } + } + return { check }; +} + +const args = parseArgs(Bun.argv.slice(2)); +const versions = await loadInternalVersions(); +const allChanges = []; +for (const relative of lockfiles) { + const lockfile = path.join(root, relative); + allChanges.push(...(await syncLockfile(lockfile, versions, { check: args.check }))); +} + +if (allChanges.length === 0) { + console.log('example lockfiles match internal package versions'); + process.exit(0); +} + +for (const change of allChanges) { + console.error(change); +} +if (args.check) { + console.error('example lockfiles are stale; run `tools/release/sync-example-lockfiles.mjs`'); + process.exit(1); +} + +console.log('updated example lockfiles'); diff --git a/tools/release/sync-example-lockfiles.py b/tools/release/sync-example-lockfiles.py deleted file mode 100755 index 3f4a05d4..00000000 --- a/tools/release/sync-example-lockfiles.py +++ /dev/null @@ -1,175 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import pathlib -import re -import sys -import tomllib - - -ROOT = pathlib.Path(__file__).resolve().parents[2] -LOCKFILES = [ - ROOT / "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock", -] -INTERNAL_PACKAGE_MANIFESTS = [ - ROOT / "src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml", - ROOT / "src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml", - ROOT / "src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml", - ROOT / "src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/Cargo.toml", - ROOT / "src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/Cargo.toml", - ROOT / "src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/Cargo.toml", - ROOT / "src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/Cargo.toml", - ROOT / "src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/Cargo.toml", - ROOT / "src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/Cargo.toml", - ROOT / "src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/Cargo.toml", - ROOT / "src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/Cargo.toml", -] -PACKAGE_START_RE = re.compile(r"^\s*\[\[package\]\]\s*$") -STRING_KEY_RE = re.compile(r'^\s*([A-Za-z0-9_-]+)\s*=\s*"([^"]*)"\s*(?:#.*)?$') -VERSION_LINE_RE = re.compile(r'^(\s*version\s*=\s*)"[^"]*"(\s*(?:#.*)?)$') - - -def load_internal_versions() -> dict[str, str]: - versions = {} - for manifest in INTERNAL_PACKAGE_MANIFESTS: - data = tomllib.loads(manifest.read_text(encoding="utf-8")) - package = data.get("package") - if not isinstance(package, dict): - raise SystemExit(f"{manifest.relative_to(ROOT)} is missing [package]") - name = package.get("name") - version = package.get("version") - if not isinstance(name, str) or not isinstance(version, str): - raise SystemExit(f"{manifest.relative_to(ROOT)} is missing package.name/version") - versions[name] = version - return versions - - -def strip_newline(line: str) -> tuple[str, str]: - if line.endswith("\r\n"): - return line[:-2], "\r\n" - if line.endswith("\n"): - return line[:-1], "\n" - return line, "" - - -def string_key(line: str, key: str) -> str | None: - body, _ = strip_newline(line) - match = STRING_KEY_RE.match(body) - if match and match.group(1) == key: - return match.group(2) - return None - - -def replace_version_line(line: str, version: str) -> str: - body, newline = strip_newline(line) - match = VERSION_LINE_RE.match(body) - if not match: - raise SystemExit(f"cannot update Cargo.lock version line: {line.rstrip()}") - return f'{match.group(1)}"{version}"{match.group(2)}{newline}' - - -def package_block_ranges(lines: list[str]) -> list[tuple[int, int]]: - starts = [idx for idx, line in enumerate(lines) if PACKAGE_START_RE.match(line)] - return [ - (start, starts[pos + 1] if pos + 1 < len(starts) else len(lines)) - for pos, start in enumerate(starts) - ] - - -def check_lockfile_contains_internal_packages(lockfile: pathlib.Path, versions: dict[str, str]) -> None: - data = tomllib.loads(lockfile.read_text(encoding="utf-8")) - packages = data.get("package") - if not isinstance(packages, list): - raise SystemExit(f"{lockfile.relative_to(ROOT)} is missing [[package]] entries") - - present = {package.get("name") for package in packages if isinstance(package, dict)} - missing = sorted(set(versions) - present) - if missing: - raise SystemExit( - f"{lockfile.relative_to(ROOT)} is missing internal Oliphaunt packages: {', '.join(missing)}" - ) - - -def sync_lockfile(lockfile: pathlib.Path, versions: dict[str, str], *, check: bool) -> list[str]: - check_lockfile_contains_internal_packages(lockfile, versions) - lines = lockfile.read_text(encoding="utf-8").splitlines(keepends=True) - changes = [] - registry_changes = [] - - for start, end in package_block_ranges(lines): - block = lines[start:end] - name = None - version_idx = None - current_version = None - has_source = False - - for offset, line in enumerate(block): - if string_key(line, "source") is not None: - has_source = True - key_name = string_key(line, "name") - if key_name is not None: - name = key_name - key_version = string_key(line, "version") - if key_version is not None: - version_idx = start + offset - current_version = key_version - - if name not in versions or has_source: - continue - if version_idx is None or current_version is None: - raise SystemExit(f"{lockfile.relative_to(ROOT)} package {name} is missing version") - - expected_version = versions[name] - if current_version != expected_version: - if has_source: - registry_changes.append( - f"{lockfile.relative_to(ROOT)}: {name} {current_version} -> {expected_version}" - ) - continue - if not check: - lines[version_idx] = replace_version_line(lines[version_idx], expected_version) - changes.append( - f"{lockfile.relative_to(ROOT)}: {name} {current_version} -> {expected_version}" - ) - - if registry_changes: - for change in registry_changes: - print(change, file=sys.stderr) - raise SystemExit( - "registry-sourced example lockfiles are stale; run Cargo update through " - "`examples/tools/with-local-registries.sh` after staging the local registry" - ) - if changes: - lockfile.write_text("".join(lines), encoding="utf-8") - return changes - - -def main() -> int: - parser = argparse.ArgumentParser() - parser.add_argument("--check", action="store_true", help="fail instead of writing updates") - args = parser.parse_args() - - versions = load_internal_versions() - all_changes = [] - for lockfile in LOCKFILES: - before = lockfile.read_text(encoding="utf-8") - changes = sync_lockfile(lockfile, versions, check=args.check) - if args.check and changes: - lockfile.write_text(before, encoding="utf-8") - all_changes.extend(changes) - - if not all_changes: - print("example lockfiles match internal package versions") - return 0 - - for change in all_changes: - print(change, file=sys.stderr) - if args.check: - print("example lockfiles are stale; run `tools/release/sync-example-lockfiles.py`", file=sys.stderr) - return 1 - - print("updated example lockfiles") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) From ca15d854c56480089e83aa04a584adc06e07d8ce Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 09:07:19 +0000 Subject: [PATCH 056/137] docs: record local example e2e validation --- docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 7800b0b5..005ed902 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -106,6 +106,16 @@ review production pipelines, then normalize implementation details. extension crates from `oliphaunt-local`; WASIX Tauri exercised the split WASIX runtime/tools/AOT and selected extension package graph through WebDriver. +- On 2026-06-26 after the Bun lockfile-sync conversion, the four GUI smoke + commands passed again against the staged local Cargo and Verdaccio registries: + `examples/tools/run-electron-driver-smoke.sh examples/electron`, + `examples/tools/run-electron-driver-smoke.sh examples/electron-wasix`, + `examples/tools/run-tauri-webdriver-smoke.sh examples/tauri`, and + `examples/tools/run-tauri-webdriver-smoke.sh examples/tauri-wasix`. The + product-local WASIX SQLx example check also passed and compiled + `oliphaunt-wasix-tools` plus + `oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu` from + `registry oliphaunt-local`. - `tools/release/sync_release_pr.py --check`, `check_release_metadata.py`, `check_consumer_shape.py`, `check_artifact_targets.py`, and the full `tools/release/release.py check` pass after refreshing the WASIX asset input fingerprint and extension evidence digests. - Extension Maven publication is now explicit in each exact-extension `release.toml`: the metadata lists `maven-central` and the two Android Maven From 97bfbc51438c23b532f3b4f446d7156ba6020f39 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 09:23:04 +0000 Subject: [PATCH 057/137] fix: harden sdk runtime metadata resolution --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 7 ++ docs/maintainers/sdk-parity-policy.md | 21 ++++- src/sdks/js/ARCHITECTURE.md | 2 +- src/sdks/js/README.md | 4 +- .../js/src/__tests__/asset-resolver.test.ts | 41 ++++++++- src/sdks/js/src/native/assets-deno.ts | 56 +++++++++++-- src/sdks/js/src/native/assets-node.ts | 84 +++++++++++++++++-- .../react-native/src/__tests__/client.test.ts | 3 + src/sdks/react-native/src/client.ts | 9 +- tools/policy/check-sdk-parity.sh | 14 ++++ 10 files changed, 216 insertions(+), 25 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 005ed902..8d6a6ad7 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -234,6 +234,13 @@ review production pipelines, then normalize implementation details. artifact-resolution comparison, identify any remaining feature gaps across SDKs, and add parity checks for invariants that are still documented only in prose. +- React Native capability reporting now clears backup/restore support and + format lists when the New Architecture JSI ArrayBuffer transport is missing. + TypeScript package metadata path resolution now rejects absolute paths, URLs, + NUL bytes, and traversal for Node and Deno runtime, ICU, extension, and split + tools package paths. SDK parity policy now documents the desktop TypeScript + `throughput` + `safe` default and Node prebuilt optional adapter path, with + machine checks for those invariants. - Subagent CI/release audit found these remaining release-surface fixes: remove or validate the duplicated native Maven artifact manifest rows, derive Kotlin Maven existing-version probes from the declared package set, add coverage diff --git a/docs/maintainers/sdk-parity-policy.md b/docs/maintainers/sdk-parity-policy.md index 75706a08..4d6a3d39 100644 --- a/docs/maintainers/sdk-parity-policy.md +++ b/docs/maintainers/sdk-parity-policy.md @@ -130,7 +130,7 @@ reason for any unavailable mode. | Mode support discovery | `EngineCapabilities::rust_sdk_support()` | `OliphauntDatabase.supportedModes()` | `OliphauntDatabase.supportedModes()` and `OliphauntAndroid.supportedModes()` | `Oliphaunt.supportedModes()` delegated from Swift/Kotlin | | Handle/executor ownership | Cloned Rust `Oliphaunt` handles share one SDK executor, FIFO owner queue, session pin, cancel handle, and close state in direct, broker, and server modes; cloning is not a connection pool | Swift database values are actor-owned session handles guarded by a FIFO async serial gate; additional references share the same actor/session and server-mode independent clients must use server support when implemented | Kotlin database values are coroutine session handles guarded by `executionMutex`; additional references share the same coroutine/session boundary and server-mode independent clients must use server support when implemented | React Native `OliphauntDatabase` objects wrap the delegated Swift/Kotlin session handle and delegate ordering to the platform serial session; JS references do not create independent sessions | | Connection identity | `Oliphaunt::builder().username(...).database(...)` feeds direct, broker, and server startup identity; invalid empty/NUL values are rejected before runtime open | `OliphauntConfiguration(username:database:)` feeds native-direct startup identity and rejects invalid empty/NUL values before engine open | `OliphauntConfig(username, database)` feeds native-direct startup identity and rejects invalid empty/NUL values before engine open | `open({ username, database })` forwards the same identity through Swift/Kotlin and rejects invalid empty/NUL values before the TurboModule call | -| Runtime footprint profiles | `RuntimeFootprintProfile::{Throughput,BalancedMobile,SmallMobile}` defines the shared PostgreSQL startup-GUC contract; balanced/small mobile lower slot counts, shared buffers, WAL footprint, and PG18 AIO concurrency | `OliphauntRuntimeFootprintProfile` carries the same three profiles and generated startup args for Apple direct mode; the Apple SDK default is `balancedMobile` + `balanced` | `RuntimeFootprintProfile` carries the same three profiles and generated startup args for Android/Kotlin direct mode; the Android/Kotlin default is `BalancedMobile` + `Balanced` | `runtimeFootprint: 'throughput' | 'balancedMobile' | 'smallMobile'` forwards the selected profile through Swift/Kotlin; the TypeScript default is `balancedMobile` + `balanced` | +| Runtime footprint profiles | `RuntimeFootprintProfile::{Throughput,BalancedMobile,SmallMobile}` defines the shared PostgreSQL startup-GUC contract; balanced/small mobile lower slot counts, shared buffers, WAL footprint, and PG18 AIO concurrency | `OliphauntRuntimeFootprintProfile` carries the same three profiles and generated startup args for Apple direct mode; the Apple SDK default is `balancedMobile` + `balanced` | `RuntimeFootprintProfile` carries the same three profiles and generated startup args for Android/Kotlin direct mode; the Android/Kotlin default is `BalancedMobile` + `Balanced` | `runtimeFootprint: 'throughput' | 'balancedMobile' | 'smallMobile'` forwards the selected profile through Swift/Kotlin; the React Native default is `balancedMobile` + `balanced` | | Startup GUC overrides | `startup_guc`/`startup_gucs` append validated `name=value` overrides after durability and footprint profiles so benchmark/device sweeps can override profile defaults | `startupGUCs` appends validated overrides after the selected profile before the Swift engine call | `startupGucs` appends validated overrides after the selected profile before the Kotlin engine call | `startupGUCs` accepts validated string or object values in TypeScript and forwards string assignments through the TurboModule to Swift/Kotlin | | Extensions | yes | yes | yes | via Swift/Kotlin | | Packaged runtime resources | yes, producer | yes, consumer | yes, consumer | via platform SDK consumers | @@ -141,6 +141,25 @@ reason for any unavailable mode. | Close behavior | `Oliphaunt::close` rejects queued work, waits for active work, then closes/detaches; use `cancel()` explicitly to interrupt SQL | `OliphauntDatabase.close` rejects queued work, waits for active work, then detaches; use `cancel()` explicitly to interrupt SQL | `OliphauntDatabase.close` rejects queued work, waits for active work, then detaches; use `cancel()` explicitly to interrupt SQL | `OliphauntDatabase.close` delegates the same wait-and-detach behavior through Swift/Kotlin | | True concurrent sessions | server mode only | server mode only | server mode only | server mode only | +### Desktop TypeScript Deltas + +`@oliphaunt/ts` is a peer SDK for Node.js, Bun, Deno, and Tauri JavaScript +apps, but it is not a separate mobile runtime layer. It owns desktop +JavaScript concerns that do not map one-for-one to the Swift/Kotlin mobile +table above: + +- Direct, broker, and server modes are all exposed for desktop JavaScript. +- The default open profile is `runtimeFootprint: 'throughput'` with + `durability: 'safe'`, matching the desktop-first default rather than the + mobile `balancedMobile` + `balanced` default. +- Node.js direct mode resolves the prebuilt `@oliphaunt/node-direct-*` + optional package; Bun and Deno use their native FFI surfaces. +- Native runtime artifacts come from `@oliphaunt/liboliphaunt-*` optional npm + packages, PostgreSQL client tools come from split `@oliphaunt/tools-*` + optional npm packages, and Node/Bun extensions come from exact extension npm + packages. Deno requires an explicit prepared `runtimeDirectory` for extension + materialization. + ## Current Platform Stance | SDK | Primary app target | Runtime owner | Current native mode | Non-parity that is allowed today | diff --git a/src/sdks/js/ARCHITECTURE.md b/src/sdks/js/ARCHITECTURE.md index 37381bbd..2f6cb2c0 100644 --- a/src/sdks/js/ARCHITECTURE.md +++ b/src/sdks/js/ARCHITECTURE.md @@ -127,7 +127,7 @@ When `engine` is omitted, the default is consistent: - `nativeDirect`: available when `liboliphaunt` loads and the runtime has a direct adapter. Bun and Deno use built-in FFI. Node resolves the verified - `oliphaunt-node-direct-*` Node-API adapter release asset and loads it + `@oliphaunt/node-direct-*` Node-API adapter optional package and loads it without `postinstall`, node-gyp, Rust, Cargo, or third-party FFI packages; - native direct extension package materialization is shared by Node and Bun. Deno direct mode may use extensions only with an explicit prepared diff --git a/src/sdks/js/README.md b/src/sdks/js/README.md index 905bef04..9310075a 100644 --- a/src/sdks/js/README.md +++ b/src/sdks/js/README.md @@ -157,8 +157,8 @@ import { createDenoNativeBinding } from '@oliphaunt/ts/deno'; SDKs. For this SDK: - `nativeDirect` is available when liboliphaunt can be loaded and the runtime - has an FFI surface. Bun and Deno provide one; Node.js direct mode requires an - explicit app-provided FFI dependency. + has an FFI surface. Bun and Deno provide one; Node.js resolves the matching + prebuilt Node-API adapter from installed optional packages. - `nativeBroker` is available when the matching broker helper and `liboliphaunt` release assets can be resolved. - `nativeServer` is available when the PostgreSQL server executable can be diff --git a/src/sdks/js/src/__tests__/asset-resolver.test.ts b/src/sdks/js/src/__tests__/asset-resolver.test.ts index e0dea74a..43a618d2 100644 --- a/src/sdks/js/src/__tests__/asset-resolver.test.ts +++ b/src/sdks/js/src/__tests__/asset-resolver.test.ts @@ -2,11 +2,12 @@ import assert from 'node:assert/strict'; import { test } from 'vitest'; import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; -import { dirname, join } from 'node:path'; +import { dirname, join, resolve } from 'node:path'; import { deflateRawSync, inflateRawSync } from 'node:zlib'; import { liboliphauntPackageTarget } from '../native/common.js'; -import { resolveNodeNativeInstall } from '../native/assets-node.js'; +import { resolvePackageRelativeUrl } from '../native/assets-deno.js'; +import { resolveNodeNativeInstall, resolvePackageRelativePath } from '../native/assets-node.js'; import { extractTarArchive } from '../native/tar.js'; import { extractZipArchive } from '../native/zip.js'; import { brokerModeSupport } from '../runtime/broker.js'; @@ -29,6 +30,7 @@ async function main(): Promise { packageTargetsMatchLiboliphauntPackages(); await tarExtractionRejectsTraversal(); await zipExtractionWritesFilesAndRejectsTraversal(); + packageMetadataPathsAreConfinedToPackageRoot(); await nodeResolverUsesInstalledPackages(); await typeScriptPackageMetadataMatchesRuntimePackages(); await brokerSupportUsesInstalledPackages(); @@ -101,6 +103,41 @@ function packageTargetsMatchLiboliphauntPackages(): void { assert.equal(windowsTarget.toolsRuntimeRelativePath, 'runtime'); } +function packageMetadataPathsAreConfinedToPackageRoot(): void { + const packageRoot = resolve('/tmp/oliphaunt-package-root'); + assert.equal( + resolvePackageRelativePath(packageRoot, 'runtime/bin/postgres', 'test package metadata'), + join(packageRoot, 'runtime/bin/postgres'), + ); + const packageRootUrl = new URL('file:///tmp/oliphaunt-package-root/'); + assert.equal( + resolvePackageRelativeUrl(packageRootUrl, 'runtime/bin/postgres', 'test package metadata').href, + 'file:///tmp/oliphaunt-package-root/runtime/bin/postgres', + ); + for (const unsafePath of [ + '', + '../outside', + 'runtime/../outside', + 'runtime/%2e%2e/outside', + '/tmp/outside', + 'file:///tmp/outside', + 'https://example.invalid/runtime', + 'C:\\outside', + 'runtime\0outside', + ]) { + assert.throws( + () => resolvePackageRelativePath(packageRoot, unsafePath, 'test package metadata'), + /unsafe package metadata path/, + unsafePath, + ); + assert.throws( + () => resolvePackageRelativeUrl(packageRootUrl, unsafePath, 'test package metadata'), + /unsafe package metadata path/, + unsafePath, + ); + } +} + async function tarExtractionRejectsTraversal(): Promise { const root = await mkdtemp(join(tmpdir(), 'oliphaunt-js-tar-')); try { diff --git a/src/sdks/js/src/native/assets-deno.ts b/src/sdks/js/src/native/assets-deno.ts index 5606542c..ac257eb7 100644 --- a/src/sdks/js/src/native/assets-deno.ts +++ b/src/sdks/js/src/native/assets-deno.ts @@ -128,14 +128,16 @@ async function resolvePackageNativeInstall( throw new Error(`${target.packageName} package metadata does not target ${target.id}`); } const packageRoot = new URL('.', packageJsonUrl); - const libraryUrl = new URL( - packageJson.oliphaunt?.libraryRelativePath ?? target.libraryRelativePath, + const libraryUrl = resolvePackageRelativeUrl( packageRoot, + packageJson.oliphaunt?.libraryRelativePath ?? target.libraryRelativePath, + `${target.packageName} liboliphaunt library metadata`, ); await requireFile(deno, libraryUrl, `${target.packageName} liboliphaunt library`); - const runtimeUrl = new URL( - `${packageJson.oliphaunt?.runtimeRelativePath ?? target.runtimeRelativePath}/`, - new URL('.', packageJsonUrl), + const runtimeUrl = resolvePackageRelativeUrl( + packageRoot, + packageJson.oliphaunt?.runtimeRelativePath ?? target.runtimeRelativePath, + `${target.packageName} runtime directory metadata`, ); await requireDirectory(deno, runtimeUrl, `${target.packageName} runtime directory`); return { @@ -172,11 +174,53 @@ async function resolveDenoIcuDataDirectory( if (packageJson.oliphaunt?.target !== 'portable') { throw new Error(`${packageName} package metadata must target portable ICU data`); } - const dataUrl = new URL(packageJson.oliphaunt.dataRelativePath ?? 'share/icu', new URL('.', packageJsonUrl)); + const dataUrl = resolvePackageRelativeUrl( + new URL('.', packageJsonUrl), + packageJson.oliphaunt.dataRelativePath ?? 'share/icu', + `${packageName} ICU data directory metadata`, + ); await requireIcuDataDirectory(deno, dataUrl, `${packageName} ICU data directory`); return decodeURIComponent(dataUrl.pathname.replace(/\/+$/, '')); } +export function resolvePackageRelativeUrl( + packageRoot: URL, + metadataPath: string, + source: string, +): URL { + const relativePath = safePackageRelativePath(metadataPath, source); + const resolved = new URL(relativePath, packageRoot); + const rootHref = packageRoot.href.endsWith('/') ? packageRoot.href : `${packageRoot.href}/`; + if (resolved.protocol !== packageRoot.protocol || !resolved.href.startsWith(rootHref)) { + throw new Error(`${source} contains unsafe package metadata path: ${metadataPath}`); + } + return resolved; +} + +function safePackageRelativePath(metadataPath: string, source: string): string { + if (metadataPath.length === 0) { + throw new Error(`${source} contains unsafe package metadata path: `); + } + if (metadataPath.includes('\0')) { + throw new Error(`${source} contains unsafe package metadata path: ${metadataPath}`); + } + let decoded: string; + try { + decoded = decodeURIComponent(metadataPath); + } catch { + throw new Error(`${source} contains unsafe package metadata path: ${metadataPath}`); + } + const normalized = decoded.replaceAll('\\', '/'); + if ( + normalized.startsWith('/') || + /^[A-Za-z][A-Za-z0-9+.-]*:/.test(normalized) || + normalized.split('/').includes('..') + ) { + throw new Error(`${source} contains unsafe package metadata path: ${metadataPath}`); + } + return normalized; +} + function resolvePackageJsonUrl(packageName: string): URL { const resolver = (import.meta as ImportMeta & { resolve?: (specifier: string) => string }) .resolve; diff --git a/src/sdks/js/src/native/assets-node.ts b/src/sdks/js/src/native/assets-node.ts index f5094e4c..02f6ebf5 100644 --- a/src/sdks/js/src/native/assets-node.ts +++ b/src/sdks/js/src/native/assets-node.ts @@ -2,7 +2,7 @@ import { createHash } from 'node:crypto'; import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; import { arch, platform, tmpdir } from 'node:os'; -import { dirname, join } from 'node:path'; +import { dirname, isAbsolute, join, relative, resolve } from 'node:path'; import { liboliphauntPackageTarget, @@ -202,7 +202,11 @@ export async function resolveNodeIcuDataDirectory( if (packageJson.oliphaunt?.target !== 'portable') { throw new Error(`${name} package metadata must target portable ICU data`); } - const dataDirectory = join(packageRoot, packageJson.oliphaunt.dataRelativePath ?? 'share/icu'); + const dataDirectory = resolvePackageRelativePath( + packageRoot, + packageJson.oliphaunt.dataRelativePath ?? 'share/icu', + `${name} ICU data directory metadata`, + ); await requireIcuDataDirectory(dataDirectory, `${name} ICU data directory`); return dataDirectory; } @@ -303,12 +307,22 @@ async function resolveExtensionPackage( } } } else { - const runtimeDirectory = join(packageRoot, packageJson.oliphaunt.runtimeRelativePath ?? 'runtime'); + const runtimeDirectory = resolvePackageRelativePath( + packageRoot, + packageJson.oliphaunt.runtimeRelativePath ?? 'runtime', + `${targetPackageName} extension runtime directory metadata`, + ); await requireDirectory(runtimeDirectory, `${targetPackageName} extension runtime directory`); runtimeDirectories.push(runtimeDirectory); const moduleRelativePath = packageJson.oliphaunt.moduleRelativePath; const moduleDirectory = - moduleRelativePath === undefined ? undefined : join(packageRoot, moduleRelativePath); + moduleRelativePath === undefined + ? undefined + : resolvePackageRelativePath( + packageRoot, + moduleRelativePath, + `${targetPackageName} extension module directory metadata`, + ); if (moduleDirectory !== undefined) { await requireDirectory(moduleDirectory, `${targetPackageName} extension module directory`); moduleDirectories.push(moduleDirectory); @@ -364,11 +378,21 @@ async function resolveExtensionPayloadPackage( `${packageName} liboliphauntVersion ${packageJson.oliphaunt?.liboliphauntVersion ?? ''} does not match @oliphaunt/ts liboliphauntVersion ${liboliphauntVersion}`, ); } - const runtimeDirectory = join(packageRoot, packageJson.oliphaunt.runtimeRelativePath ?? 'runtime'); + const runtimeDirectory = resolvePackageRelativePath( + packageRoot, + packageJson.oliphaunt.runtimeRelativePath ?? 'runtime', + `${packageName} extension runtime directory metadata`, + ); await requireDirectory(runtimeDirectory, `${packageName} extension runtime directory`); const moduleRelativePath = packageJson.oliphaunt.moduleRelativePath; const moduleDirectory = - moduleRelativePath === undefined ? undefined : join(packageRoot, moduleRelativePath); + moduleRelativePath === undefined + ? undefined + : resolvePackageRelativePath( + packageRoot, + moduleRelativePath, + `${packageName} extension module directory metadata`, + ); if (moduleDirectory !== undefined) { await requireDirectory(moduleDirectory, `${packageName} extension module directory`); } @@ -398,14 +422,16 @@ async function resolvePackageNativeInstall( if (packageJson.oliphaunt?.target !== target.id) { throw new Error(`${target.packageName} package metadata does not target ${target.id}`); } - const libraryPath = join( + const libraryPath = resolvePackageRelativePath( packageRoot, packageJson.oliphaunt?.libraryRelativePath ?? target.libraryRelativePath, + `${target.packageName} liboliphaunt library metadata`, ); await requireFile(libraryPath, `${target.packageName} liboliphaunt library`); - const runtimeDirectory = join( + const runtimeDirectory = resolvePackageRelativePath( packageRoot, packageJson.oliphaunt?.runtimeRelativePath ?? target.runtimeRelativePath, + `${target.packageName} runtime directory metadata`, ); await requireDirectory(runtimeDirectory, `${target.packageName} runtime directory`); for (const tool of nativeRuntimeToolsForTarget(target.id)) { @@ -465,9 +491,10 @@ async function resolveNativeToolsPackage( if (packageJson.oliphaunt?.target !== target.id) { throw new Error(`${target.toolsPackageName} package metadata does not target ${target.id}`); } - const runtimeDirectory = join( + const runtimeDirectory = resolvePackageRelativePath( packageRoot, packageJson.oliphaunt?.runtimeRelativePath ?? target.toolsRuntimeRelativePath, + `${target.toolsPackageName} runtime directory metadata`, ); await requireDirectory(runtimeDirectory, `${target.toolsPackageName} runtime directory`); for (const tool of nativeClientToolsForTarget(target.id)) { @@ -586,6 +613,45 @@ function optionalResolvePackageJson(packageName: string): string | undefined { } } +export function resolvePackageRelativePath( + packageRoot: string, + metadataPath: string, + source: string, +): string { + const relativePath = safePackageRelativePath(metadataPath, source); + const root = resolve(packageRoot); + const resolved = resolve(root, relativePath); + const fromRoot = relative(root, resolved); + if (fromRoot.startsWith('..') || isAbsolute(fromRoot)) { + throw new Error(`${source} contains unsafe package metadata path: ${metadataPath}`); + } + return resolved; +} + +function safePackageRelativePath(metadataPath: string, source: string): string { + if (metadataPath.length === 0) { + throw new Error(`${source} contains unsafe package metadata path: `); + } + if (metadataPath.includes('\0')) { + throw new Error(`${source} contains unsafe package metadata path: ${metadataPath}`); + } + let decoded: string; + try { + decoded = decodeURIComponent(metadataPath); + } catch { + throw new Error(`${source} contains unsafe package metadata path: ${metadataPath}`); + } + const normalized = decoded.replaceAll('\\', '/'); + if ( + normalized.startsWith('/') || + /^[A-Za-z][A-Za-z0-9+.-]*:/.test(normalized) || + normalized.split('/').includes('..') + ) { + throw new Error(`${source} contains unsafe package metadata path: ${metadataPath}`); + } + return normalized; +} + async function requireFile(path: string, source: string): Promise { try { if ((await stat(path)).isFile()) { diff --git a/src/sdks/react-native/src/__tests__/client.test.ts b/src/sdks/react-native/src/__tests__/client.test.ts index 93b88b40..2374dd42 100644 --- a/src/sdks/react-native/src/__tests__/client.test.ts +++ b/src/sdks/react-native/src/__tests__/client.test.ts @@ -432,6 +432,9 @@ async function testOpenRequiresJsiTransportBeforeNativeCall(): Promise { support[0]?.unavailableReason ?? '', /New Architecture JSI ArrayBuffer transport is not installed/, ); + assert.equal(support[0]?.capabilities.backupRestore, false); + assert.deepEqual(support[0]?.capabilities.backupFormats, []); + assert.deepEqual(support[0]?.capabilities.restoreFormats, []); await assert.rejects( () => client.open(), /requires React Native New Architecture JSI ArrayBuffer bindings/, diff --git a/src/sdks/react-native/src/client.ts b/src/sdks/react-native/src/client.ts index 6bb3b360..23fc2f71 100644 --- a/src/sdks/react-native/src/client.ts +++ b/src/sdks/react-native/src/client.ts @@ -719,6 +719,7 @@ function normalizeCapabilities( native: NativeCapabilities, jsiTransport: JsiRawProtocolTransport | null = resolveJsiRawProtocolTransport(), ): EngineCapabilities { + const jsiAvailable = jsiTransport != null; return { engine: parseEngine(native.engine), processIsolated: native.processIsolated, @@ -729,12 +730,12 @@ function normalizeCapabilities( crashRestartable: native.crashRestartable, independentSessions: native.independentSessions, maxClientSessions: native.maxClientSessions, - protocolRaw: native.protocolRaw && jsiTransport != null, + protocolRaw: native.protocolRaw && jsiAvailable, protocolStream: native.protocolStream && jsiTransportSupportsProtocolStream(jsiTransport), queryCancel: native.queryCancel, - backupRestore: native.backupRestore, - backupFormats: native.backupFormats.map(parseBackupFormat), - restoreFormats: native.restoreFormats.map(parseBackupFormat), + backupRestore: native.backupRestore && jsiAvailable, + backupFormats: jsiAvailable ? native.backupFormats.map(parseBackupFormat) : [], + restoreFormats: jsiAvailable ? native.restoreFormats.map(parseBackupFormat) : [], simpleQuery: native.simpleQuery, extensions: native.extensions, connectionString: native.connectionString, diff --git a/tools/policy/check-sdk-parity.sh b/tools/policy/check-sdk-parity.sh index 345f0bfb..b8a7720a 100755 --- a/tools/policy/check-sdk-parity.sh +++ b/tools/policy/check-sdk-parity.sh @@ -274,6 +274,12 @@ require_text docs/maintainers/sdk-parity-policy.md "split \`@oliphaunt/tools-*\` "SDK parity docs must describe TypeScript split tools npm resolution" require_text docs/maintainers/sdk-parity-policy.md "Deno requires an explicit prepared \`runtimeDirectory\` for extension materialization" \ "SDK parity docs must document the Deno extension-resolution deviation" +require_text docs/maintainers/sdk-parity-policy.md "### Desktop TypeScript Deltas" \ + "SDK parity docs must describe desktop TypeScript deltas explicitly" +require_text docs/maintainers/sdk-parity-policy.md "The default open profile is \`runtimeFootprint: 'throughput'\` with" \ + "SDK parity docs must document the desktop TypeScript default profile" +require_text docs/maintainers/sdk-parity-policy.md "Node.js direct mode resolves the prebuilt \`@oliphaunt/node-direct-*\`" \ + "SDK parity docs must document Node direct optional adapter resolution" require_text docs/maintainers/sdk-parity-policy.md "not exposed in Android native-direct mode" \ "SDK parity docs must state Android native-direct does not expose standalone PostgreSQL tools" require_text docs/maintainers/sdk-parity-policy.md "delegated SwiftPM and Maven platform SDK resolution" \ @@ -378,6 +384,14 @@ require_text src/sdks/react-native/src/client.ts "config.runtimeFootprint ?? 'ba "React Native SDK default opens must use the mobile runtime footprint profile" require_text src/sdks/react-native/src/client.ts "durability: config.durability ?? 'balanced'" \ "React Native SDK default opens must use the SQLite-like balanced durability profile" +require_text src/sdks/js/src/config.ts "config.runtimeFootprint ?? 'throughput'" \ + "TypeScript SDK default opens must keep the desktop throughput runtime footprint profile" +require_text src/sdks/js/src/config.ts "config.durability ?? 'safe'" \ + "TypeScript SDK default opens must keep the crash-safe desktop durability profile" +require_text src/sdks/js/README.md "Node.js resolves the matching" \ + "TypeScript README must say Node direct mode uses the prebuilt optional adapter" +require_text src/sdks/js/ARCHITECTURE.md "\`@oliphaunt/node-direct-*\` Node-API adapter optional package" \ + "TypeScript architecture docs must say Node direct uses the installed optional adapter package" require_text src/sdks/swift/Sources/Oliphaunt/Oliphaunt.swift "durability: OliphauntDurability = .balanced" \ "Swift SDK default opens must use the SQLite-like balanced durability profile" require_text src/sdks/swift/Sources/Oliphaunt/Oliphaunt.swift "runtimeFootprint: OliphauntRuntimeFootprintProfile = .balancedMobile" \ From c896d0cb9e2930b31657cc4b8383585e15d40ca4 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 09:28:47 +0000 Subject: [PATCH 058/137] chore: remove affected planner wrapper --- .github/scripts/plan-affected.py | 17 ----------------- .github/workflows/ci.yml | 2 +- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 3 +++ docs/internal/IMPLEMENTATION_CHECKLIST.md | 10 +++++----- tools/policy/check-repo-structure.sh | 4 +--- tools/policy/check-tooling-stack.sh | 2 +- 6 files changed, 11 insertions(+), 27 deletions(-) delete mode 100644 .github/scripts/plan-affected.py diff --git a/.github/scripts/plan-affected.py b/.github/scripts/plan-affected.py deleted file mode 100644 index 6e821948..00000000 --- a/.github/scripts/plan-affected.py +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env python3 -"""GitHub Actions wrapper for the shared Moon affected CI planner.""" - -from __future__ import annotations - -import sys -from pathlib import Path - - -ROOT = Path(__file__).resolve().parents[2] -sys.path.insert(0, str(ROOT / "tools" / "graph")) - -import ci_plan # noqa: E402 - - -if __name__ == "__main__": - raise SystemExit(ci_plan.emit_github_outputs()) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5f0bf76..5ccc55d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -125,7 +125,7 @@ jobs: WASM_TARGET: ${{ github.event_name == 'workflow_dispatch' && inputs.wasm_target || 'all' }} NATIVE_TARGET: ${{ github.event_name == 'workflow_dispatch' && inputs.native_target || 'all' }} MOBILE_TARGET: ${{ github.event_name == 'workflow_dispatch' && inputs.mobile_target || 'all' }} - run: python3 .github/scripts/plan-affected.py + run: python3 tools/graph/ci_plan.py - name: Plan check and test jobs id: target-matrices diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 8d6a6ad7..ab9282d9 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -190,6 +190,9 @@ review production pipelines, then normalize implementation details. - WASIX example Cargo lockfile synchronization now uses Bun instead of Python, keeping the nested Tauri SQLx example aligned with local internal WASIX crate versions without invoking Cargo when only source-tree versions changed. +- The CI affected-plan wrapper `.github/scripts/plan-affected.py` was removed; + the workflow now invokes `python3 tools/graph/ci_plan.py` directly, keeping + the shared planner as the single Python entrypoint for CI job selection. - CI/release producer-to-consumer audit found no P0/P1 mapping gaps across Cargo, npm, Maven, SwiftPM, or GitHub release assets. Existing `release.py check`, artifact-target, release-metadata, consumer-shape, and diff --git a/docs/internal/IMPLEMENTATION_CHECKLIST.md b/docs/internal/IMPLEMENTATION_CHECKLIST.md index 522fb069..7899dbda 100644 --- a/docs/internal/IMPLEMENTATION_CHECKLIST.md +++ b/docs/internal/IMPLEMENTATION_CHECKLIST.md @@ -577,7 +577,7 @@ Run before claiming this architecture complete: in the Builds workflow. - [x] `GITHUB_EVENT_NAME=workflow_dispatch NATIVE_TARGET=all WASM_TARGET=linux-x64-gnu MOBILE_TARGET=all - python3 .github/scripts/plan-affected.py` now selects only + python3 tools/graph/ci_plan.py` now selects only `affected`, `liboliphaunt-wasix-runtime`, and `liboliphaunt-wasix-aot`; it does not select `liboliphaunt-wasix-release-assets`, `wasix-rust-package`, SDK packages, extension packages, or mobile builders. @@ -612,9 +612,9 @@ Run before claiming this architecture complete: oliphaunt-swift`. The CI `liboliphaunt-native-ios` builder still owns proof that the real native Apple XCFramework asset is produced. - [x] `GITHUB_EVENT_NAME=workflow_dispatch NATIVE_TARGET=all - WASM_TARGET=all MOBILE_TARGET=ios python3 .github/scripts/plan-affected.py` + WASM_TARGET=all MOBILE_TARGET=ios python3 tools/graph/ci_plan.py` - [x] `GITHUB_EVENT_NAME=workflow_dispatch NATIVE_TARGET=all - WASM_TARGET=all MOBILE_TARGET=android python3 .github/scripts/plan-affected.py` + WASM_TARGET=all MOBILE_TARGET=android python3 tools/graph/ci_plan.py` - [x] `tools/graph/ci_plan.py` direct probe for `{"extension-artifacts-native:build-target"}` selects `extension-artifacts-native` without `liboliphaunt-native`, proving extension @@ -670,10 +670,10 @@ Run before claiming this architecture complete: `_liboliphaunt_selected_static_extensions` plus vector registry symbols, and Maestro sees `liboliphaunt-smoke-status-passed`. - [x] `GITHUB_EVENT_NAME=workflow_dispatch NATIVE_TARGET=ios-xcframework - WASM_TARGET=all MOBILE_TARGET=all python3 .github/scripts/plan-affected.py` + WASM_TARGET=all MOBILE_TARGET=all python3 tools/graph/ci_plan.py` - [x] Focused mobile builder plans are target-consistent: `GITHUB_EVENT_NAME=workflow_dispatch NATIVE_TARGET=android-arm64-v8a - WASM_TARGET=all MOBILE_TARGET=android python3 .github/scripts/plan-affected.py` + WASM_TARGET=all MOBILE_TARGET=android python3 tools/graph/ci_plan.py` emits one Android exact-extension row, one Android app row, and `mobile_extension_package_native_targets=["android-arm64-v8a"]`; the matching iOS probe emits only `ios-xcframework`. Incompatible focused inputs such as diff --git a/tools/policy/check-repo-structure.sh b/tools/policy/check-repo-structure.sh index 8a33d722..ffa7dc7e 100755 --- a/tools/policy/check-repo-structure.sh +++ b/tools/policy/check-repo-structure.sh @@ -222,7 +222,6 @@ require_file src/shared/contracts/test-matrix.toml require_file src/shared/contracts/tools/check-test-matrix.py require_file src/shared/fixtures/moon.yml require_file src/shared/fixtures/manifest.toml -require_file .github/scripts/plan-affected.py require_file .github/scripts/run-affected-moon-task.sh require_file .github/scripts/select-affected-moon-targets.mjs require_file .github/scripts/run-moon-targets.sh @@ -504,7 +503,7 @@ require_text .github/workflows/ci.yml 'name: Builds / native-runtime-android (${ require_text .github/workflows/ci.yml 'name: Builds / native-runtime-ios (${{ matrix.target }})' require_text .github/workflows/ci.yml 'name: Builds / liboliphaunt-wasix-runtime' require_text .github/workflows/ci.yml 'name: Builds / liboliphaunt-wasix-aot (${{ matrix.target_id }})' -require_text .github/workflows/ci.yml 'python3 .github/scripts/plan-affected.py' +require_text .github/workflows/ci.yml 'python3 tools/graph/ci_plan.py' require_text .github/workflows/ci.yml 'name: Plan' require_text .github/workflows/ci.yml 'path: target/graph/ci-plan.json' require_text .github/workflows/ci.yml 'job_targets: ${{ steps.plan.outputs.job_targets }}' @@ -532,7 +531,6 @@ reject_path .github/scripts/run-moon-ci.sh reject_text .github/scripts/run-affected-moon-task.sh 'pnpm moon' reject_text .github/scripts/select-affected-moon-targets.mjs 'pnpm moon' reject_text .github/scripts/run-moon-targets.sh 'pnpm moon' -require_text .github/scripts/plan-affected.py 'ci_plan.emit_github_outputs()' require_text tools/graph/affected.py 'moon(["query", "affected", "--upstream", "none", "--downstream", "none"])' require_text tools/graph/affected.py 'moon(["query", "affected", "--upstream", "none", "--downstream", "deep"])' reject_path tools/graph/jobs.toml diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index dd49e1f0..cdf832c0 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -245,7 +245,7 @@ grep -Fq 'ANDROID_SDKMANAGER_INSTALL_ATTEMPTS' tools/dev/setup-android-sdk.sh || fail "Android SDK setup must retry sdkmanager package installation for transient/corrupt downloads" grep -Fq 'cleanup_partial_sdk_packages' tools/dev/setup-android-sdk.sh || fail "Android SDK setup must clean partial sdkmanager package directories before retrying" -grep -Fq 'python3 .github/scripts/plan-affected.py' .github/workflows/ci.yml || +grep -Fq 'python3 tools/graph/ci_plan.py' .github/workflows/ci.yml || fail "CI must derive product job startup from the Moon affected planner" grep -Fq "contains(fromJson(needs.affected.outputs.jobs), 'liboliphaunt-wasix-runtime')" .github/workflows/ci.yml || fail "CI must gate expensive WASIX runtime work from the Moon affected job list" From ad4c06970fd1089ac183078a9d939757959c0630 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 09:33:53 +0000 Subject: [PATCH 059/137] chore: port graph cache witness to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 4 + tools/graph/cache-witness.mjs | 120 ++++++++++++++++++ tools/graph/cache-witness.py | 105 --------------- tools/graph/moon.yml | 8 +- tools/policy/check-policy-tools.sh | 2 +- tools/policy/check-repo-structure.sh | 3 +- tools/policy/check-tooling-stack.sh | 4 +- 7 files changed, 134 insertions(+), 112 deletions(-) create mode 100644 tools/graph/cache-witness.mjs delete mode 100755 tools/graph/cache-witness.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index ab9282d9..e1b1856e 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -193,6 +193,10 @@ review production pipelines, then normalize implementation details. - The CI affected-plan wrapper `.github/scripts/plan-affected.py` was removed; the workflow now invokes `python3 tools/graph/ci_plan.py` directly, keeping the shared planner as the single Python entrypoint for CI job selection. +- The Moon cache witness helper now uses Bun instead of Python. The converted + `tools/graph/cache-witness.mjs` preserves the two-step output-cache + assertion and resolves `MOON_BIN` or the local proto Moon shim for reliable + local runs. - CI/release producer-to-consumer audit found no P0/P1 mapping gaps across Cargo, npm, Maven, SwiftPM, or GitHub release assets. Existing `release.py check`, artifact-target, release-metadata, consumer-shape, and diff --git a/tools/graph/cache-witness.mjs b/tools/graph/cache-witness.mjs new file mode 100644 index 00000000..f5419f8f --- /dev/null +++ b/tools/graph/cache-witness.mjs @@ -0,0 +1,120 @@ +#!/usr/bin/env bun +import { randomUUID } from 'node:crypto'; +import { existsSync } from 'node:fs'; +import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { homedir } from 'node:os'; +import { dirname, relative, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { spawnSync } from 'node:child_process'; + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..'); +const WITNESS_ROOT = resolve(ROOT, 'target', 'graph', 'cache-witness'); +const INPUT = resolve(WITNESS_ROOT, 'input.txt'); +const OUTPUT = resolve(WITNESS_ROOT, 'output.txt'); +const RUNS = resolve(WITNESS_ROOT, 'runs.txt'); + +function fail(message) { + throw new Error(`cache-witness.mjs: ${message}`); +} + +async function readRequiredText(path) { + if (!existsSync(path)) { + fail(`missing expected file: ${relative(ROOT, path)}`); + } + return await readFile(path, 'utf8'); +} + +async function fixture() { + const value = (await readRequiredText(INPUT)).trim(); + await mkdir(WITNESS_ROOT, { recursive: true }); + let runs = 0; + if (existsSync(RUNS)) { + runs = Number.parseInt((await readFile(RUNS, 'utf8')).trim(), 10); + } + runs += 1; + await writeFile(RUNS, `${runs}\n`, 'utf8'); + await writeFile(OUTPUT, `moon-cache-witness:${value}\n`, 'utf8'); +} + +function moonBin() { + if (process.env.MOON_BIN) { + return process.env.MOON_BIN; + } + for (const candidate of [ + resolve(homedir(), '.proto', 'shims', 'moon'), + resolve(homedir(), '.proto', 'bin', 'moon'), + ]) { + if (existsSync(candidate)) { + return candidate; + } + } + return 'moon'; +} + +function runMoonFixture() { + const completed = spawnSync(moonBin(), ['run', 'graph-tools:cache-witness-fixture'], { + cwd: ROOT, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + const output = `${completed.stdout ?? ''}${completed.stderr ?? ''}`; + if (completed.status !== 0) { + process.stdout.write(output); + process.exit(completed.status ?? 1); + } + return output; +} + +async function assertCache() { + await mkdir(WITNESS_ROOT, { recursive: true }); + const token = randomUUID().replaceAll('-', ''); + await writeFile(INPUT, `${token}\n`, 'utf8'); + await Promise.all([rm(OUTPUT, { force: true }), rm(RUNS, { force: true })]); + + const firstLog = runMoonFixture(); + const expected = `moon-cache-witness:${token}\n`; + if ((await readRequiredText(OUTPUT)) !== expected) { + fail('first run did not write the expected fixture output'); + } + if ((await readRequiredText(RUNS)) !== '1\n') { + fail('first run did not execute the fixture exactly once'); + } + + await rm(OUTPUT, { force: true }); + const secondLog = runMoonFixture(); + if ((await readRequiredText(OUTPUT)) !== expected) { + fail('second run did not restore the expected fixture output'); + } + if ((await readRequiredText(RUNS)) !== '1\n') { + fail( + 'Moon reran the fixture instead of hydrating the declared output from cache ' + + '(runs counter changed)', + ); + } + + console.log('Moon cache witness passed'); + console.log('first run:'); + console.log(firstLog.trimEnd()); + console.log('second run:'); + console.log(secondLog.trimEnd()); +} + +async function main() { + const [command] = process.argv.slice(2); + if (command === 'fixture') { + await fixture(); + return; + } + if (command === 'assert') { + await assertCache(); + return; + } + fail('usage: cache-witness.mjs '); +} + +try { + await main(); +} catch (error) { + console.error(error instanceof Error ? error.message : error); + process.exit(1); +} diff --git a/tools/graph/cache-witness.py b/tools/graph/cache-witness.py deleted file mode 100755 index 6101c852..00000000 --- a/tools/graph/cache-witness.py +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env python3 -"""Exercise Moon's local output cache with a deterministic tiny fixture.""" - -from __future__ import annotations - -import argparse -import os -import subprocess -import sys -import uuid -from pathlib import Path - - -ROOT = Path(__file__).resolve().parents[2] -WITNESS_ROOT = ROOT / "target" / "graph" / "cache-witness" -INPUT = WITNESS_ROOT / "input.txt" -OUTPUT = WITNESS_ROOT / "output.txt" -RUNS = WITNESS_ROOT / "runs.txt" - - -def fail(message: str) -> None: - raise SystemExit(f"cache-witness.py: {message}") - - -def read_text(path: Path) -> str: - if not path.is_file(): - fail(f"missing expected file: {path.relative_to(ROOT)}") - return path.read_text(encoding="utf-8") - - -def fixture() -> int: - value = read_text(INPUT).strip() - WITNESS_ROOT.mkdir(parents=True, exist_ok=True) - runs = 0 - if RUNS.is_file(): - runs = int(RUNS.read_text(encoding="utf-8").strip()) - runs += 1 - RUNS.write_text(f"{runs}\n", encoding="utf-8") - OUTPUT.write_text(f"moon-cache-witness:{value}\n", encoding="utf-8") - return 0 - - -def run_moon_fixture() -> str: - completed = subprocess.run( - ["moon", "run", "graph-tools:cache-witness-fixture"], - cwd=ROOT, - check=True, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) - return completed.stdout - - -def assert_cache() -> int: - WITNESS_ROOT.mkdir(parents=True, exist_ok=True) - token = uuid.uuid4().hex - INPUT.write_text(f"{token}\n", encoding="utf-8") - for path in (OUTPUT, RUNS): - path.unlink(missing_ok=True) - - first_log = run_moon_fixture() - expected = f"moon-cache-witness:{token}\n" - if read_text(OUTPUT) != expected: - fail("first run did not write the expected fixture output") - if read_text(RUNS) != "1\n": - fail("first run did not execute the fixture exactly once") - - OUTPUT.unlink() - second_log = run_moon_fixture() - if read_text(OUTPUT) != expected: - fail("second run did not restore the expected fixture output") - if read_text(RUNS) != "1\n": - fail( - "Moon reran the fixture instead of hydrating the declared output from cache " - "(runs counter changed)" - ) - - print("Moon cache witness passed") - print("first run:") - print(first_log.rstrip()) - print("second run:") - print(second_log.rstrip()) - return 0 - - -def parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - subparsers = parser.add_subparsers(dest="command", required=True) - subparsers.add_parser("fixture") - subparsers.add_parser("assert") - return parser.parse_args(argv) - - -def main(argv: list[str]) -> int: - args = parse_args(argv) - if args.command == "fixture": - return fixture() - if args.command == "assert": - return assert_cache() - fail(f"unsupported command {args.command}") - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) diff --git a/tools/graph/moon.yml b/tools/graph/moon.yml index 96d1b60d..f1ae74d9 100644 --- a/tools/graph/moon.yml +++ b/tools/graph/moon.yml @@ -69,13 +69,13 @@ tasks: runFromWorkspaceRoot: true cache-witness: tags: ["cache", "witness"] - command: "tools/graph/cache-witness.py assert" + command: "bun tools/graph/cache-witness.mjs assert" inputs: - "/.moon/workspace.yml" - "/.moon/toolchains.yml" - "/package.json" - "/pnpm-lock.yaml" - - "/tools/graph/cache-witness.py" + - "/tools/graph/cache-witness.mjs" - "/tools/graph/moon.yml" options: cache: false @@ -83,10 +83,10 @@ tasks: runInCI: false cache-witness-fixture: tags: ["cache", "witness", "generated"] - command: "tools/graph/cache-witness.py fixture" + command: "bun tools/graph/cache-witness.mjs fixture" inputs: - "/target/graph/cache-witness/input.txt" - - "/tools/graph/cache-witness.py" + - "/tools/graph/cache-witness.mjs" - "/tools/graph/moon.yml" outputs: - "/target/graph/cache-witness/output.txt" diff --git a/tools/policy/check-policy-tools.sh b/tools/policy/check-policy-tools.sh index 99a0f52c..aa97fd0b 100755 --- a/tools/policy/check-policy-tools.sh +++ b/tools/policy/check-policy-tools.sh @@ -34,7 +34,7 @@ while IFS= read -r script; do output_name="${output_name//\//__}" output_name="${output_name%.mjs}.js" run bun build "$script" --target=bun --outfile="$js_check_root/$output_name" -done < <(find tools/policy -type f -name '*.mjs' | LC_ALL=C sort) +done < <(find tools/policy tools/graph -type f -name '*.mjs' | LC_ALL=C sort) python_files=() while IFS= read -r script; do diff --git a/tools/policy/check-repo-structure.sh b/tools/policy/check-repo-structure.sh index ffa7dc7e..4610c392 100755 --- a/tools/policy/check-repo-structure.sh +++ b/tools/policy/check-repo-structure.sh @@ -412,8 +412,9 @@ require_text tools/policy/check-tooling-stack.sh 'tools/policy/assertions/assert require_text tools/policy/moon.yml '/tools/graph/**/*' require_text tools/graph/moon.yml 'id: "graph-tools"' require_text tools/graph/moon.yml 'tools/graph/graph.py check' -require_file tools/graph/cache-witness.py +require_file tools/graph/cache-witness.mjs require_text tools/graph/moon.yml 'cache-witness-fixture:' +require_text tools/graph/moon.yml 'bun tools/graph/cache-witness.mjs assert' require_text moon.yml 'cacheStrategy: "outputs"' require_text src/docs/moon.yml 'cacheStrategy: "outputs"' require_text tools/policy/moon.yml '/tools/test/**/*' diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index cdf832c0..3d0e3bf7 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -37,7 +37,7 @@ require_file .moon/workspace.yml require_file docs/maintainers/tooling.md require_file tools/test/moon.yml require_file tools/test/run-js-tests.mjs -require_file tools/graph/cache-witness.py +require_file tools/graph/cache-witness.mjs require_file tools/runtime/preflight.sh require_file tools/dev/bun.sh require_file tools/dev/deno.sh @@ -333,6 +333,8 @@ grep -Fq 'target/liboliphaunt-sdk-check/oliphaunt-js' src/sdks/js/tools/check-sd fail "TypeScript SDK checks must use an isolated scratch root so Moon can run SDK checks in parallel" grep -Fq 'cache-witness-fixture:' tools/graph/moon.yml || fail "graph-tools must keep a cache witness fixture task" +grep -Fq 'bun tools/graph/cache-witness.mjs assert' tools/graph/moon.yml || + fail "graph-tools cache witness must use the Bun helper" grep -Fq 'cacheStrategy: "outputs"' moon.yml || fail "repo coverage aggregate must use Moon dependency cacheStrategy=outputs" grep -Fq 'cacheStrategy: "outputs"' src/docs/moon.yml || From eb1d370b609a39b490b543802646168b5a000dab Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 09:40:20 +0000 Subject: [PATCH 060/137] chore: remove inline python from github workflows --- .github/actions/setup-deno/action.yml | 10 +---- .github/scripts/resolve-release-please-pr.mjs | 45 +++++++++++++++++++ .github/workflows/release.yml | 27 +---------- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 4 ++ tools/policy/check-policy-tools.sh | 2 +- tools/policy/check-repo-structure.sh | 3 ++ tools/policy/check-workflows.sh | 4 ++ 7 files changed, 60 insertions(+), 35 deletions(-) create mode 100644 .github/scripts/resolve-release-please-pr.mjs diff --git a/.github/actions/setup-deno/action.yml b/.github/actions/setup-deno/action.yml index 8bd3e97c..a1d7b6ae 100644 --- a/.github/actions/setup-deno/action.yml +++ b/.github/actions/setup-deno/action.yml @@ -107,14 +107,8 @@ runs: --connect-timeout 20 \ --output "$tmp/deno.zip" \ "$url" - python3 - "$tmp/deno.zip" "$DENO_CACHE_DIR" <<'PY' - import sys - import zipfile - - archive, output = sys.argv[1], sys.argv[2] - with zipfile.ZipFile(archive) as zip_file: - zip_file.extractall(output) - PY + mkdir -p "$DENO_CACHE_DIR" + unzip -oq "$tmp/deno.zip" -d "$DENO_CACHE_DIR" chmod +x "$DENO_BINARY" echo "$DENO_CACHE_DIR" >> "$GITHUB_PATH" diff --git a/.github/scripts/resolve-release-please-pr.mjs b/.github/scripts/resolve-release-please-pr.mjs new file mode 100644 index 00000000..e4a50f00 --- /dev/null +++ b/.github/scripts/resolve-release-please-pr.mjs @@ -0,0 +1,45 @@ +#!/usr/bin/env bun + +function candidateObjectsFromEnv(name) { + const raw = process.env[name]?.trim(); + if (!raw) { + return []; + } + let value; + try { + value = JSON.parse(raw); + } catch { + return []; + } + if (Array.isArray(value)) { + return value.filter((item) => item !== null && typeof item === 'object'); + } + if (value !== null && typeof value === 'object') { + return [value]; + } + return []; +} + +function pullRequestNumber(item) { + const value = item.number ?? item.pullRequestNumber; + if (typeof value === 'number' && Number.isInteger(value) && value > 0) { + return String(value); + } + if (typeof value === 'string' && value.trim().length > 0) { + return value.trim(); + } + return undefined; +} + +const candidates = [ + ...candidateObjectsFromEnv('RELEASE_PLEASE_PR'), + ...candidateObjectsFromEnv('RELEASE_PLEASE_PRS'), +]; + +for (const item of candidates) { + const number = pullRequestNumber(item); + if (number !== undefined) { + console.log(number); + process.exit(0); + } +} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 51a3ef14..3ebf66c8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -119,32 +119,7 @@ jobs: run: | set -euo pipefail - release_pr_number="$( - python3 - <<'PY' - import json - import os - - candidates = [] - for name in ("RELEASE_PLEASE_PR", "RELEASE_PLEASE_PRS"): - raw = os.environ.get(name, "").strip() - if not raw: - continue - try: - value = json.loads(raw) - except json.JSONDecodeError: - continue - if isinstance(value, dict): - candidates.append(value) - elif isinstance(value, list): - candidates.extend(item for item in value if isinstance(item, dict)) - - for item in candidates: - number = item.get("number") or item.get("pullRequestNumber") - if number: - print(number) - break - PY - )" + release_pr_number="$(bun .github/scripts/resolve-release-please-pr.mjs)" if [[ -z "${release_pr_number}" ]]; then release_pr_number="$( gh pr list \ diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index e1b1856e..1c7e798c 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -197,6 +197,10 @@ review production pipelines, then normalize implementation details. `tools/graph/cache-witness.mjs` preserves the two-step output-cache assertion and resolves `MOON_BIN` or the local proto Moon shim for reliable local runs. +- GitHub workflow/action inline Python heredocs were removed from the release + PR sync path and Deno fallback installer. Release PR number extraction now + uses `bun .github/scripts/resolve-release-please-pr.mjs`, and the Deno + fallback installer extracts the downloaded archive with `unzip`. - CI/release producer-to-consumer audit found no P0/P1 mapping gaps across Cargo, npm, Maven, SwiftPM, or GitHub release assets. Existing `release.py check`, artifact-target, release-metadata, consumer-shape, and diff --git a/tools/policy/check-policy-tools.sh b/tools/policy/check-policy-tools.sh index aa97fd0b..f522319b 100755 --- a/tools/policy/check-policy-tools.sh +++ b/tools/policy/check-policy-tools.sh @@ -34,7 +34,7 @@ while IFS= read -r script; do output_name="${output_name//\//__}" output_name="${output_name%.mjs}.js" run bun build "$script" --target=bun --outfile="$js_check_root/$output_name" -done < <(find tools/policy tools/graph -type f -name '*.mjs' | LC_ALL=C sort) +done < <(find .github/scripts tools/policy tools/graph -type f -name '*.mjs' | LC_ALL=C sort) python_files=() while IFS= read -r script; do diff --git a/tools/policy/check-repo-structure.sh b/tools/policy/check-repo-structure.sh index 4610c392..453a54af 100755 --- a/tools/policy/check-repo-structure.sh +++ b/tools/policy/check-repo-structure.sh @@ -227,6 +227,7 @@ require_file .github/scripts/select-affected-moon-targets.mjs require_file .github/scripts/run-moon-targets.sh require_file .github/scripts/run-planned-moon-job.sh require_file .github/scripts/select-planned-moon-targets.mjs +require_file .github/scripts/resolve-release-please-pr.mjs require_file src/runtimes/liboliphaunt/native/tools/check-patch-stack.mjs require_file src/runtimes/liboliphaunt/native/THIRD_PARTY_NOTICES.md require_file src/runtimes/liboliphaunt/wasix/tools/check-patch-stack.mjs @@ -505,6 +506,8 @@ require_text .github/workflows/ci.yml 'name: Builds / native-runtime-ios (${{ ma require_text .github/workflows/ci.yml 'name: Builds / liboliphaunt-wasix-runtime' require_text .github/workflows/ci.yml 'name: Builds / liboliphaunt-wasix-aot (${{ matrix.target_id }})' require_text .github/workflows/ci.yml 'python3 tools/graph/ci_plan.py' +require_text .github/workflows/release.yml 'bun .github/scripts/resolve-release-please-pr.mjs' +require_text .github/actions/setup-deno/action.yml 'unzip -oq "$tmp/deno.zip" -d "$DENO_CACHE_DIR"' require_text .github/workflows/ci.yml 'name: Plan' require_text .github/workflows/ci.yml 'path: target/graph/ci-plan.json' require_text .github/workflows/ci.yml 'job_targets: ${{ steps.plan.outputs.job_targets }}' diff --git a/tools/policy/check-workflows.sh b/tools/policy/check-workflows.sh index b3760596..ad2809db 100755 --- a/tools/policy/check-workflows.sh +++ b/tools/policy/check-workflows.sh @@ -28,5 +28,9 @@ if grep -R --line-number --fixed-strings 'pnpm moon run' .github/workflows; then echo "GitHub workflows must invoke Moon through .github/scripts/run-moon-targets.sh" >&2 exit 1 fi +if grep -R --line-number --fixed-strings 'python3 - <<' .github/workflows .github/actions; then + echo "GitHub workflows and actions must not embed inline Python heredocs" >&2 + exit 1 +fi run actionlint run zizmor --config .github/zizmor.yml --min-severity medium --persona auditor .github/workflows .github/actions From 9769221665b500677b2372f3a1e3536134990ab0 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 09:45:03 +0000 Subject: [PATCH 061/137] chore: list cargo packages with bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 4 ++++ tools/policy/check-crate-package.sh | 19 +--------------- tools/policy/check-release-policy.py | 14 ++++++++++-- tools/policy/check-repo-structure.sh | 2 ++ .../list-publishable-cargo-packages.mjs | 22 +++++++++++++++++++ 5 files changed, 41 insertions(+), 20 deletions(-) create mode 100644 tools/policy/list-publishable-cargo-packages.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 1c7e798c..512ea774 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -201,6 +201,10 @@ review production pipelines, then normalize implementation details. PR sync path and Deno fallback installer. Release PR number extraction now uses `bun .github/scripts/resolve-release-please-pr.mjs`, and the Deno fallback installer extracts the downloaded archive with `unzip`. +- `tools/policy/check-crate-package.sh` now derives the default publishable + Cargo package set through `bun tools/policy/list-publishable-cargo-packages.mjs` + instead of an inline Python `cargo metadata` parser, while keeping + `oliphaunt-wasix` on the release-shaped package helper path. - CI/release producer-to-consumer audit found no P0/P1 mapping gaps across Cargo, npm, Maven, SwiftPM, or GitHub release assets. Existing `release.py check`, artifact-target, release-metadata, consumer-shape, and diff --git a/tools/policy/check-crate-package.sh b/tools/policy/check-crate-package.sh index 5bad2444..e896d2c6 100755 --- a/tools/policy/check-crate-package.sh +++ b/tools/policy/check-crate-package.sh @@ -37,24 +37,7 @@ package_oliphaunt_wasix() { } default_packages() { - python3 - <<'PY' -import json -import subprocess - -metadata = json.loads( - subprocess.check_output( - ["cargo", "metadata", "--no-deps", "--format-version", "1"], - text=True, - ) -) -for package in sorted(metadata["packages"], key=lambda item: item["name"]): - if package.get("publish") == []: - continue - name = package["name"] - if name == "oliphaunt-wasix": - continue - print(name) -PY + bun tools/policy/list-publishable-cargo-packages.mjs } if [ "${#packages[@]}" -eq 0 ]; then diff --git a/tools/policy/check-release-policy.py b/tools/policy/check-release-policy.py index 30a60034..389068fa 100644 --- a/tools/policy/check-release-policy.py +++ b/tools/policy/check-release-policy.py @@ -836,9 +836,9 @@ def check_release_workflow_policy() -> None: fail(f"release dry-runs and package publishes must cover registry-native checks: missing {snippet!r}") crate_package_script = read_text("tools/policy/check-crate-package.sh") + crate_package_helper = read_text("tools/policy/list-publishable-cargo-packages.mjs") for snippet in ( - '"cargo", "metadata"', - 'package.get("publish") == []', + "bun tools/policy/list-publishable-cargo-packages.mjs", "package_oliphaunt_wasix", "bun tools/release/package_oliphaunt_wasix_sdk_crate.mjs", 'if [ "$package" = "oliphaunt-wasix" ]; then', @@ -848,6 +848,16 @@ def check_release_workflow_policy() -> None: "crate package policy must package oliphaunt-wasix through the " f"release-shaped local helper instead of crates.io resolution: missing {snippet!r}" ) + for snippet in ( + "'cargo', ['metadata', '--no-deps', '--format-version', '1']", + "Array.isArray(cargoPackage.publish) && cargoPackage.publish.length === 0", + "cargoPackage.name === 'oliphaunt-wasix'", + ): + if snippet not in crate_package_helper: + fail( + "crate package policy must derive default publishable crates from cargo metadata " + f"with oliphaunt-wasix handled by the release-shaped helper: missing {snippet!r}" + ) release_head_script = read_text(".github/scripts/resolve-release-head.sh") for snippet in ( diff --git a/tools/policy/check-repo-structure.sh b/tools/policy/check-repo-structure.sh index 453a54af..68bf0654 100755 --- a/tools/policy/check-repo-structure.sh +++ b/tools/policy/check-repo-structure.sh @@ -236,6 +236,7 @@ require_file tools/policy/check-react-native-boundary.sh require_file tools/policy/check-sdk-mobile-extension-surface.sh require_file tools/policy/check-test-strategy.mjs require_file tools/policy/check-coverage.sh +require_file tools/policy/list-publishable-cargo-packages.mjs require_file tools/policy/sdk-check-lib.sh require_file tools/test/moon.yml require_file tools/test/run-js-tests.mjs @@ -611,6 +612,7 @@ require_text docs/maintainers/tooling.md 'src/bindings/wasix-rust/crates/oliphau require_text docs/maintainers/tooling.md 'src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/postgres_mod/stdio.rs' require_text docs/maintainers/tooling.md 'src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/postgres_mod/wasix_fs.rs' require_text docs/maintainers/tooling.md 'tools/policy/check-sdk-mobile-extension-surface.sh' +require_text tools/policy/check-crate-package.sh 'bun tools/policy/list-publishable-cargo-packages.mjs' require_text src/bindings/wasix-rust/tools/check-examples.sh '--target-dir target/oliphaunt-wasix-rust/examples/tauri-sqlx-vanilla/src-tauri' require_text src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh 'oliphaunt_resolve_repo_root' require_text src/runtimes/liboliphaunt/native/bin/common.sh 'git -C "$script_dir" rev-parse --show-toplevel' diff --git a/tools/policy/list-publishable-cargo-packages.mjs b/tools/policy/list-publishable-cargo-packages.mjs new file mode 100644 index 00000000..1c9fa133 --- /dev/null +++ b/tools/policy/list-publishable-cargo-packages.mjs @@ -0,0 +1,22 @@ +#!/usr/bin/env bun +import { execFileSync } from 'node:child_process'; + +const metadata = JSON.parse( + execFileSync('cargo', ['metadata', '--no-deps', '--format-version', '1'], { + encoding: 'utf8', + }), +); + +const packages = [...metadata.packages].sort((left, right) => + left.name.localeCompare(right.name), +); + +for (const cargoPackage of packages) { + if (Array.isArray(cargoPackage.publish) && cargoPackage.publish.length === 0) { + continue; + } + if (cargoPackage.name === 'oliphaunt-wasix') { + continue; + } + console.log(cargoPackage.name); +} From 2ab433b251a8632fb576a632cb10ecfb93750602 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 09:50:23 +0000 Subject: [PATCH 062/137] chore: merge artifact checksums with bun --- .github/scripts/download-build-artifacts.sh | 50 +--------------- .github/scripts/merge-checksum-manifest.mjs | 57 +++++++++++++++++++ .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 4 ++ tools/policy/check-repo-structure.sh | 2 + 4 files changed, 64 insertions(+), 49 deletions(-) create mode 100644 .github/scripts/merge-checksum-manifest.mjs diff --git a/.github/scripts/download-build-artifacts.sh b/.github/scripts/download-build-artifacts.sh index 91109d98..669871fc 100755 --- a/.github/scripts/download-build-artifacts.sh +++ b/.github/scripts/download-build-artifacts.sh @@ -58,55 +58,7 @@ artifact_present() { merge_checksum_manifest() { local existing="$1" local incoming="$2" - python3 - "$existing" "$incoming" <<'PY' -from __future__ import annotations - -import sys -import tempfile -from pathlib import Path - -existing = Path(sys.argv[1]) -incoming = Path(sys.argv[2]) -entries: dict[str, str] = {} - - -def read_manifest(path: Path) -> None: - with path.open("r", encoding="utf-8") as handle: - for line_number, line in enumerate(handle, 1): - stripped = line.strip() - if not stripped: - continue - parts = stripped.split(None, 1) - if len(parts) != 2: - raise SystemExit(f"{path}: invalid checksum line {line_number}: {line.rstrip()}") - digest, raw_name = parts[0], parts[1].strip() - if len(digest) != 64 or any(char not in "0123456789abcdef" for char in digest): - raise SystemExit(f"{path}: invalid checksum digest on line {line_number}: {digest}") - name = raw_name.removeprefix("./") - if not name or "/" in name: - raise SystemExit(f"{path}: invalid checksum asset name on line {line_number}: {raw_name}") - previous = entries.get(name) - if previous is not None and previous != digest: - raise SystemExit( - f"{path}: conflicting checksum for {name}: {previous} vs {digest}" - ) - entries[name] = digest - - -read_manifest(existing) -read_manifest(incoming) -with tempfile.NamedTemporaryFile( - "w", - encoding="utf-8", - newline="\n", - dir=str(existing.parent), - delete=False, -) as handle: - temp_path = Path(handle.name) - for name in sorted(entries): - handle.write(f"{entries[name]} ./{name}\n") -temp_path.replace(existing) -PY + bun .github/scripts/merge-checksum-manifest.mjs "$existing" "$incoming" } merge_downloaded_artifact() { diff --git a/.github/scripts/merge-checksum-manifest.mjs b/.github/scripts/merge-checksum-manifest.mjs new file mode 100644 index 00000000..292c2e61 --- /dev/null +++ b/.github/scripts/merge-checksum-manifest.mjs @@ -0,0 +1,57 @@ +#!/usr/bin/env bun +import { mkdtempSync, renameSync, rmSync, writeFileSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; + +function fail(message) { + console.error(`merge-checksum-manifest.mjs: ${message}`); + process.exit(1); +} + +function parseManifest(path, text, entries) { + for (const [index, line] of text.split(/\r?\n/).entries()) { + const lineNumber = index + 1; + const stripped = line.trim(); + if (stripped.length === 0) { + continue; + } + const match = /^([0-9a-f]{64})\s+(.+)$/.exec(stripped); + if (match === null) { + fail(`${path}: invalid checksum line ${lineNumber}: ${line}`); + } + const digest = match[1]; + const rawName = match[2].trim(); + const name = rawName.startsWith('./') ? rawName.slice(2) : rawName; + if (name.length === 0 || name.includes('/')) { + fail(`${path}: invalid checksum asset name on line ${lineNumber}: ${rawName}`); + } + const previous = entries.get(name); + if (previous !== undefined && previous !== digest) { + fail(`${path}: conflicting checksum for ${name}: ${previous} vs ${digest}`); + } + entries.set(name, digest); + } +} + +const [existing, incoming] = process.argv.slice(2); +if (existing === undefined || incoming === undefined) { + fail('usage: merge-checksum-manifest.mjs '); +} + +const entries = new Map(); +parseManifest(existing, await readFile(existing, 'utf8'), entries); +parseManifest(incoming, await readFile(incoming, 'utf8'), entries); + +const merged = [...entries] + .sort(([left], [right]) => left.localeCompare(right)) + .map(([name, digest]) => `${digest} ./${name}\n`) + .join(''); + +const tempDir = mkdtempSync(join(dirname(existing), '.oliphaunt-checksums-')); +const tempPath = join(tempDir, 'checksums.sha256'); +try { + writeFileSync(tempPath, merged, { encoding: 'utf8' }); + renameSync(tempPath, existing); +} finally { + rmSync(tempDir, { force: true, recursive: true }); +} diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 512ea774..e39f0550 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -205,6 +205,10 @@ review production pipelines, then normalize implementation details. Cargo package set through `bun tools/policy/list-publishable-cargo-packages.mjs` instead of an inline Python `cargo metadata` parser, while keeping `oliphaunt-wasix` on the release-shaped package helper path. +- `.github/scripts/download-build-artifacts.sh` now merges duplicate release + checksum manifests through `bun .github/scripts/merge-checksum-manifest.mjs` + instead of an inline Python parser, preserving sorted output and conflicting + checksum rejection. - CI/release producer-to-consumer audit found no P0/P1 mapping gaps across Cargo, npm, Maven, SwiftPM, or GitHub release assets. Existing `release.py check`, artifact-target, release-metadata, consumer-shape, and diff --git a/tools/policy/check-repo-structure.sh b/tools/policy/check-repo-structure.sh index 68bf0654..1bfe42c1 100755 --- a/tools/policy/check-repo-structure.sh +++ b/tools/policy/check-repo-structure.sh @@ -228,6 +228,7 @@ require_file .github/scripts/run-moon-targets.sh require_file .github/scripts/run-planned-moon-job.sh require_file .github/scripts/select-planned-moon-targets.mjs require_file .github/scripts/resolve-release-please-pr.mjs +require_file .github/scripts/merge-checksum-manifest.mjs require_file src/runtimes/liboliphaunt/native/tools/check-patch-stack.mjs require_file src/runtimes/liboliphaunt/native/THIRD_PARTY_NOTICES.md require_file src/runtimes/liboliphaunt/wasix/tools/check-patch-stack.mjs @@ -532,6 +533,7 @@ require_text .github/scripts/run-affected-moon-task.sh 'exec .github/scripts/run require_text .github/scripts/run-planned-moon-job.sh 'bun .github/scripts/select-planned-moon-targets.mjs "$job"' require_text .github/scripts/run-planned-moon-job.sh 'exec .github/scripts/run-moon-targets.sh' require_text .github/scripts/run-moon-targets.sh 'exec "$moon_bin" run "$@"' +require_text .github/scripts/download-build-artifacts.sh 'bun .github/scripts/merge-checksum-manifest.mjs "$existing" "$incoming"' reject_path .github/scripts/run-moon-ci.sh reject_text .github/scripts/run-affected-moon-task.sh 'pnpm moon' reject_text .github/scripts/select-affected-moon-targets.mjs 'pnpm moon' From 1c149d2378f63a4257ea19bcf69ddbe6dc0d4eed Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 09:53:00 +0000 Subject: [PATCH 063/137] chore: validate coverage baseline with bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 4 + tools/policy/check-coverage-baseline.mjs | 83 +++++++++++++++++++ tools/policy/check-coverage.sh | 51 +----------- tools/policy/check-repo-structure.sh | 2 + 4 files changed, 90 insertions(+), 50 deletions(-) create mode 100644 tools/policy/check-coverage-baseline.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index e39f0550..5bf1279d 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -209,6 +209,10 @@ review production pipelines, then normalize implementation details. checksum manifests through `bun .github/scripts/merge-checksum-manifest.mjs` instead of an inline Python parser, preserving sorted output and conflicting checksum rejection. +- `tools/policy/check-coverage.sh` now delegates structured + `coverage/baseline.toml` validation to + `bun tools/policy/check-coverage-baseline.mjs`, removing another inline + Python TOML parser from policy checks. - CI/release producer-to-consumer audit found no P0/P1 mapping gaps across Cargo, npm, Maven, SwiftPM, or GitHub release assets. Existing `release.py check`, artifact-target, release-metadata, consumer-shape, and diff --git a/tools/policy/check-coverage-baseline.mjs b/tools/policy/check-coverage-baseline.mjs new file mode 100644 index 00000000..67bda848 --- /dev/null +++ b/tools/policy/check-coverage-baseline.mjs @@ -0,0 +1,83 @@ +#!/usr/bin/env bun + +const EXPECTED_PRODUCTS = [ + 'oliphaunt-rust', + 'oliphaunt-swift', + 'oliphaunt-kotlin', + 'oliphaunt-js', + 'oliphaunt-react-native', + 'oliphaunt-wasix-rust', +]; + +function fail(message) { + console.error(message); + process.exit(1); +} + +function numberValue(value) { + if (typeof value === 'number') { + return value; + } + if (typeof value === 'string' && value.trim().length > 0) { + return Number(value); + } + return Number.NaN; +} + +function requireString(value, context) { + if (typeof value !== 'string' || value.trim().length === 0) { + fail(`${context} must be a non-empty string`); + } +} + +const selected = process.argv[2] ?? 'all'; +const targets = selected === 'all' ? EXPECTED_PRODUCTS : [selected]; +const baseline = Bun.TOML.parse(await Bun.file('coverage/baseline.toml').text()); +const products = baseline.products ?? {}; + +for (const product of targets) { + const config = products[product]; + if (config === undefined || config === null || typeof config !== 'object') { + fail(`missing coverage product config: ${product}`); + } + if ('include_globs' in config) { + fail(`${product}: coverage must use source_globs, not include_globs`); + } + const sourceGlobs = config.source_globs; + if ( + !Array.isArray(sourceGlobs) || + sourceGlobs.length === 0 || + !sourceGlobs.every((item) => typeof item === 'string') + ) { + fail(`${product}: source_globs must be a non-empty string array`); + } + const lineThreshold = numberValue(config.line_threshold); + if (Number.isNaN(lineThreshold) || lineThreshold < 80.0) { + fail(`${product}: aggregate line_threshold must stay at or above 80`); + } + const perFileLineThreshold = numberValue(config.per_file_line_threshold); + if (Number.isNaN(perFileLineThreshold) || perFileLineThreshold < 50.0) { + fail(`${product}: per_file_line_threshold must stay at or above 50`); + } + const measuredLineCoverage = numberValue(config.measured_line_coverage); + if (Number.isNaN(measuredLineCoverage) || measuredLineCoverage < lineThreshold) { + fail(`${product}: measured_line_coverage audit snapshot is below the aggregate threshold`); + } + const waivers = config.waivers; + if (!Array.isArray(waivers) || waivers.length === 0) { + fail(`${product}: coverage waivers must be explicit even when the list is short`); + } + for (const waiver of waivers) { + if (waiver === null || typeof waiver !== 'object' || Array.isArray(waiver)) { + fail(`${product}: waiver must be a TOML table`); + } + const hasPath = typeof waiver.path === 'string'; + const hasGlob = typeof waiver.glob === 'string'; + if (hasPath === hasGlob) { + fail(`${product}: waiver must define exactly one of path or glob`); + } + for (const key of ['reason', 'evidence', 'owner', 'expires']) { + requireString(waiver[key], `${product}: waiver ${key}`); + } + } +} diff --git a/tools/policy/check-coverage.sh b/tools/policy/check-coverage.sh index 4827b42c..2e5c3811 100755 --- a/tools/policy/check-coverage.sh +++ b/tools/policy/check-coverage.sh @@ -92,55 +92,6 @@ case "$product" in ;; esac -python3 - "$product" <<'PY' -from __future__ import annotations - -import sys -import tomllib -from pathlib import Path - -selected = sys.argv[1] -expected = [ - "oliphaunt-rust", - "oliphaunt-swift", - "oliphaunt-kotlin", - "oliphaunt-js", - "oliphaunt-react-native", - "oliphaunt-wasix-rust", -] -with Path("coverage/baseline.toml").open("rb") as handle: - baseline = tomllib.load(handle) -products = baseline.get("products", {}) -targets = expected if selected == "all" else [selected] -for product in targets: - config = products.get(product) - if not isinstance(config, dict): - raise SystemExit(f"missing coverage product config: {product}") - if "include_globs" in config: - raise SystemExit(f"{product}: coverage must use source_globs, not include_globs") - source_globs = config.get("source_globs") - if not isinstance(source_globs, list) or not source_globs or not all(isinstance(item, str) for item in source_globs): - raise SystemExit(f"{product}: source_globs must be a non-empty string array") - if float(config.get("line_threshold", 0.0)) < 80.0: - raise SystemExit(f"{product}: aggregate line_threshold must stay at or above 80") - if float(config.get("per_file_line_threshold", 0.0)) < 50.0: - raise SystemExit(f"{product}: per_file_line_threshold must stay at or above 50") - if float(config.get("measured_line_coverage", 0.0)) < float(config.get("line_threshold", 0.0)): - raise SystemExit(f"{product}: measured_line_coverage audit snapshot is below the aggregate threshold") - waivers = config.get("waivers", []) - if not isinstance(waivers, list) or not waivers: - raise SystemExit(f"{product}: coverage waivers must be explicit even when the list is short") - for waiver in waivers: - if not isinstance(waiver, dict): - raise SystemExit(f"{product}: waiver must be a TOML table") - has_path = isinstance(waiver.get("path"), str) - has_glob = isinstance(waiver.get("glob"), str) - if has_path == has_glob: - raise SystemExit(f"{product}: waiver must define exactly one of path or glob") - for key in ("reason", "evidence", "owner", "expires"): - value = waiver.get(key) - if not isinstance(value, str) or not value.strip(): - raise SystemExit(f"{product}: waiver {key} must be a non-empty string") -PY +bun tools/policy/check-coverage-baseline.mjs "$product" printf 'measured coverage policy is modeled for %s\n' "$product" diff --git a/tools/policy/check-repo-structure.sh b/tools/policy/check-repo-structure.sh index 1bfe42c1..65107558 100755 --- a/tools/policy/check-repo-structure.sh +++ b/tools/policy/check-repo-structure.sh @@ -237,6 +237,7 @@ require_file tools/policy/check-react-native-boundary.sh require_file tools/policy/check-sdk-mobile-extension-surface.sh require_file tools/policy/check-test-strategy.mjs require_file tools/policy/check-coverage.sh +require_file tools/policy/check-coverage-baseline.mjs require_file tools/policy/list-publishable-cargo-packages.mjs require_file tools/policy/sdk-check-lib.sh require_file tools/test/moon.yml @@ -614,6 +615,7 @@ require_text docs/maintainers/tooling.md 'src/bindings/wasix-rust/crates/oliphau require_text docs/maintainers/tooling.md 'src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/postgres_mod/stdio.rs' require_text docs/maintainers/tooling.md 'src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/postgres_mod/wasix_fs.rs' require_text docs/maintainers/tooling.md 'tools/policy/check-sdk-mobile-extension-surface.sh' +require_text tools/policy/check-coverage.sh 'bun tools/policy/check-coverage-baseline.mjs "$product"' require_text tools/policy/check-crate-package.sh 'bun tools/policy/list-publishable-cargo-packages.mjs' require_text src/bindings/wasix-rust/tools/check-examples.sh '--target-dir target/oliphaunt-wasix-rust/examples/tauri-sqlx-vanilla/src-tauri' require_text src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh 'oliphaunt_resolve_repo_root' From 89e3dce048b27ef24787061bda5e989af98f7b08 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 10:02:21 +0000 Subject: [PATCH 064/137] chore: validate wasix dependency invariants with bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 4 + src/sdks/js/ARCHITECTURE.md | 5 +- tools/policy/check-dependency-invariants.sh | 95 +------------ tools/policy/check-repo-structure.sh | 2 + ...ck-wasix-release-dependency-invariants.mjs | 128 ++++++++++++++++++ 5 files changed, 138 insertions(+), 96 deletions(-) create mode 100644 tools/policy/check-wasix-release-dependency-invariants.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 5bf1279d..22207559 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -213,6 +213,10 @@ review production pipelines, then normalize implementation details. `coverage/baseline.toml` validation to `bun tools/policy/check-coverage-baseline.mjs`, removing another inline Python TOML parser from policy checks. +- `tools/policy/check-dependency-invariants.sh` now validates WASIX release + artifact crate versions and path dependencies through + `bun tools/policy/check-wasix-release-dependency-invariants.mjs`; the shell + wrapper still owns the Cargo dependency-tree compiler/runtime exclusion gates. - CI/release producer-to-consumer audit found no P0/P1 mapping gaps across Cargo, npm, Maven, SwiftPM, or GitHub release assets. Existing `release.py check`, artifact-target, release-metadata, consumer-shape, and diff --git a/src/sdks/js/ARCHITECTURE.md b/src/sdks/js/ARCHITECTURE.md index 2f6cb2c0..b2a21bc3 100644 --- a/src/sdks/js/ARCHITECTURE.md +++ b/src/sdks/js/ARCHITECTURE.md @@ -127,8 +127,9 @@ When `engine` is omitted, the default is consistent: - `nativeDirect`: available when `liboliphaunt` loads and the runtime has a direct adapter. Bun and Deno use built-in FFI. Node resolves the verified - `@oliphaunt/node-direct-*` Node-API adapter optional package and loads it - without `postinstall`, node-gyp, Rust, Cargo, or third-party FFI packages; + `@oliphaunt/node-direct-*` Node-API adapter optional package, built from the + `oliphaunt-node-direct-*` release assets, and loads it without `postinstall`, + node-gyp, Rust, Cargo, or third-party FFI packages; - native direct extension package materialization is shared by Node and Bun. Deno direct mode may use extensions only with an explicit prepared `runtimeDirectory`; package-managed Deno extension materialization must remain diff --git a/tools/policy/check-dependency-invariants.sh b/tools/policy/check-dependency-invariants.sh index 8d70210f..2c003871 100755 --- a/tools/policy/check-dependency-invariants.sh +++ b/tools/policy/check-dependency-invariants.sh @@ -7,100 +7,7 @@ root="$(git rev-parse --show-toplevel 2>/dev/null)" || { } cd "$root" -python3 <<'PY' -import pathlib -import sys -import tomllib - -root = pathlib.Path.cwd() -product_manifest_path = root / "src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml" -product_manifest = tomllib.loads(product_manifest_path.read_text(encoding="utf-8")) -runtime_version = (root / "src/runtimes/liboliphaunt/wasix/VERSION").read_text(encoding="utf-8").strip() - - -def dependency_tables(manifest): - yield "dependencies", manifest.get("dependencies", {}) - for cfg, table in manifest.get("target", {}).items(): - yield f"target.{cfg}.dependencies", table.get("dependencies", {}) - - -def dependency_name(dep_key, spec): - if isinstance(spec, dict): - return spec.get("package", dep_key) - return dep_key - - -def dependency_version(spec): - if isinstance(spec, str): - return spec - if isinstance(spec, dict): - return spec.get("version") - return None - - -def dependency_path(spec): - if isinstance(spec, dict): - return spec.get("path") - return None - - -def is_wasix_artifact_crate(name): - return name == "liboliphaunt-wasix-portable" or name.startswith("liboliphaunt-wasix-aot-") - - -errors = [] -product_deps = {} -for table_name, deps in dependency_tables(product_manifest): - for dep_key, spec in deps.items(): - name = dependency_name(dep_key, spec) - if not is_wasix_artifact_crate(name): - continue - if name in product_deps: - errors.append(f"{name} is declared more than once in oliphaunt-wasix dependencies") - product_deps[name] = (table_name, spec) - -internal_manifest_paths = [root / "src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml"] -internal_manifest_paths.extend(sorted((root / "src/runtimes/liboliphaunt/wasix/crates/aot").glob("*/Cargo.toml"))) - -for manifest_path in internal_manifest_paths: - manifest = tomllib.loads(manifest_path.read_text(encoding="utf-8")) - package = manifest["package"] - name = package["name"] - version = package["version"] - if not is_wasix_artifact_crate(name): - errors.append(f"{manifest_path}: unexpected WASIX artifact crate name {name!r}") - continue - if version != runtime_version: - errors.append( - f"{manifest_path}: {name} version {version} does not match liboliphaunt-wasix runtime version {runtime_version}" - ) - if package.get("publish") is not False: - errors.append(f"{manifest_path}: source artifact crate template {name} must declare publish = false") - if name not in product_deps: - errors.append(f"oliphaunt-wasix must depend on WASIX artifact crate {name}") - -for name, (table_name, spec) in sorted(product_deps.items()): - version = dependency_version(spec) - path = dependency_path(spec) - if version != f"={runtime_version}": - errors.append( - "src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml " - f"{table_name}.{name} must use exact liboliphaunt-wasix version ={runtime_version}, got {version!r}" - ) - if not path: - errors.append( - "src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml " - f"{table_name}.{name} must keep a source-checkout path dependency" - ) - -if errors: - print("release version invariant violations:", file=sys.stderr) - for error in errors: - print(f" - {error}", file=sys.stderr) - sys.exit(1) - -print("release version invariants ok") -PY +bun tools/policy/check-wasix-release-dependency-invariants.mjs blocked='wasm''time|wasm''time-wasi|wasmer-compiler-(llvm|cranelift|singlepass)|llvm-sys|cranelift-|singlepass' diff --git a/tools/policy/check-repo-structure.sh b/tools/policy/check-repo-structure.sh index 65107558..edf395f3 100755 --- a/tools/policy/check-repo-structure.sh +++ b/tools/policy/check-repo-structure.sh @@ -238,6 +238,7 @@ require_file tools/policy/check-sdk-mobile-extension-surface.sh require_file tools/policy/check-test-strategy.mjs require_file tools/policy/check-coverage.sh require_file tools/policy/check-coverage-baseline.mjs +require_file tools/policy/check-wasix-release-dependency-invariants.mjs require_file tools/policy/list-publishable-cargo-packages.mjs require_file tools/policy/sdk-check-lib.sh require_file tools/test/moon.yml @@ -616,6 +617,7 @@ require_text docs/maintainers/tooling.md 'src/bindings/wasix-rust/crates/oliphau require_text docs/maintainers/tooling.md 'src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/postgres_mod/wasix_fs.rs' require_text docs/maintainers/tooling.md 'tools/policy/check-sdk-mobile-extension-surface.sh' require_text tools/policy/check-coverage.sh 'bun tools/policy/check-coverage-baseline.mjs "$product"' +require_text tools/policy/check-dependency-invariants.sh 'bun tools/policy/check-wasix-release-dependency-invariants.mjs' require_text tools/policy/check-crate-package.sh 'bun tools/policy/list-publishable-cargo-packages.mjs' require_text src/bindings/wasix-rust/tools/check-examples.sh '--target-dir target/oliphaunt-wasix-rust/examples/tauri-sqlx-vanilla/src-tauri' require_text src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh 'oliphaunt_resolve_repo_root' diff --git a/tools/policy/check-wasix-release-dependency-invariants.mjs b/tools/policy/check-wasix-release-dependency-invariants.mjs new file mode 100644 index 00000000..9eb2e68c --- /dev/null +++ b/tools/policy/check-wasix-release-dependency-invariants.mjs @@ -0,0 +1,128 @@ +#!/usr/bin/env bun +import { readdir } from 'node:fs/promises'; +import { join } from 'node:path'; + +const PRODUCT_MANIFEST_PATH = + 'src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml'; +const RUNTIME_VERSION_PATH = 'src/runtimes/liboliphaunt/wasix/VERSION'; +const INTERNAL_ASSETS_MANIFEST = + 'src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml'; +const INTERNAL_AOT_MANIFESTS_DIR = 'src/runtimes/liboliphaunt/wasix/crates/aot'; + +function fail(errors) { + console.error('release version invariant violations:'); + for (const error of errors) { + console.error(` - ${error}`); + } + process.exit(1); +} + +async function readToml(path) { + return Bun.TOML.parse(await Bun.file(path).text()); +} + +function* dependencyTables(manifest) { + yield ['dependencies', manifest.dependencies ?? {}]; + for (const [cfg, table] of Object.entries(manifest.target ?? {})) { + yield [`target.${cfg}.dependencies`, table.dependencies ?? {}]; + } +} + +function dependencyName(depKey, spec) { + if (spec !== null && typeof spec === 'object' && !Array.isArray(spec)) { + return spec.package ?? depKey; + } + return depKey; +} + +function dependencyVersion(spec) { + if (typeof spec === 'string') { + return spec; + } + if (spec !== null && typeof spec === 'object' && !Array.isArray(spec)) { + return spec.version; + } + return undefined; +} + +function dependencyPath(spec) { + if (spec !== null && typeof spec === 'object' && !Array.isArray(spec)) { + return spec.path; + } + return undefined; +} + +function isWasixArtifactCrate(name) { + return name === 'liboliphaunt-wasix-portable' || name.startsWith('liboliphaunt-wasix-aot-'); +} + +const productManifest = await readToml(PRODUCT_MANIFEST_PATH); +const runtimeVersion = (await Bun.file(RUNTIME_VERSION_PATH).text()).trim(); +const errors = []; +const productDeps = new Map(); + +for (const [tableName, deps] of dependencyTables(productManifest)) { + for (const [depKey, spec] of Object.entries(deps)) { + const name = dependencyName(depKey, spec); + if (!isWasixArtifactCrate(name)) { + continue; + } + if (productDeps.has(name)) { + errors.push(`${name} is declared more than once in oliphaunt-wasix dependencies`); + } + productDeps.set(name, { tableName, spec }); + } +} + +const internalManifestPaths = [INTERNAL_ASSETS_MANIFEST]; +for (const entry of (await readdir(INTERNAL_AOT_MANIFESTS_DIR, { withFileTypes: true })) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort()) { + internalManifestPaths.push(join(INTERNAL_AOT_MANIFESTS_DIR, entry, 'Cargo.toml')); +} + +for (const manifestPath of internalManifestPaths) { + const manifest = await readToml(manifestPath); + const packageConfig = manifest.package ?? {}; + const name = packageConfig.name; + const version = packageConfig.version; + if (typeof name !== 'string' || !isWasixArtifactCrate(name)) { + errors.push(`${manifestPath}: unexpected WASIX artifact crate name ${JSON.stringify(name)}`); + continue; + } + if (version !== runtimeVersion) { + errors.push( + `${manifestPath}: ${name} version ${version} does not match liboliphaunt-wasix runtime version ${runtimeVersion}`, + ); + } + if (packageConfig.publish !== false) { + errors.push(`${manifestPath}: source artifact crate template ${name} must declare publish = false`); + } + if (!productDeps.has(name)) { + errors.push(`oliphaunt-wasix must depend on WASIX artifact crate ${name}`); + } +} + +for (const [name, { tableName, spec }] of [...productDeps].sort(([left], [right]) => + left.localeCompare(right), +)) { + const version = dependencyVersion(spec); + const sourcePath = dependencyPath(spec); + if (version !== `=${runtimeVersion}`) { + errors.push( + `${PRODUCT_MANIFEST_PATH} ${tableName}.${name} must use exact liboliphaunt-wasix version =${runtimeVersion}, got ${JSON.stringify(version)}`, + ); + } + if (sourcePath === undefined || sourcePath === null || sourcePath === '') { + errors.push( + `${PRODUCT_MANIFEST_PATH} ${tableName}.${name} must keep a source-checkout path dependency`, + ); + } +} + +if (errors.length > 0) { + fail(errors); +} + +console.log('release version invariants ok'); From 4560a0c6a08c34dd66d3194bcc905df11ccd955a Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 10:15:02 +0000 Subject: [PATCH 065/137] fix: resolve split native tools in deno --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 11 +- src/sdks/js/ARCHITECTURE.md | 3 + src/sdks/js/README.md | 5 +- .../js/src/__tests__/native-bindings.test.ts | 1 + src/sdks/js/src/native/assets-deno.ts | 220 +++++++++++++++++- src/sdks/js/src/native/deno.ts | 6 +- src/sdks/js/tools/check-sdk.sh | 8 + tools/policy/check-sdk-parity.sh | 8 + 8 files changed, 255 insertions(+), 7 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 22207559..a4d307c6 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -292,7 +292,16 @@ review production pipelines, then normalize implementation details. `ghcr.io/catthehacker/ubuntu:act-latest`. Full Linux lane execution should run from a committed disposable worktree because `actions/checkout` validates committed HEAD rather than uncommitted local edits. -- JS Deno direct mode now resolves packaged ICU for explicit-library installs when running inside Deno, and rejects package-managed extension requests without an explicit prepared `runtimeDirectory`. Node and Bun remain the registry-managed extension materialization paths. +- JS Deno direct mode now resolves packaged ICU for explicit-library installs + when running inside Deno, and rejects package-managed extension requests + without an explicit prepared `runtimeDirectory`. Node and Bun remain the + registry-managed extension materialization paths. +- JS Deno package-managed native installs now mirror Node/Bun split runtime + tool resolution for the core tools package: the resolver validates + `@oliphaunt/tools-*`, requires `pg_dump` and `psql`, and materializes a + merged runtime tree from the installed `liboliphaunt` and tools packages. + Package-managed extension materialization remains explicitly unsupported for + Deno until it has a real extension resolver/cache path. - Release metadata checks now require the Deno package-managed extension rejection guard and its unit test, so the documented Deno limitation cannot silently drift from Node/Bun behavior. diff --git a/src/sdks/js/ARCHITECTURE.md b/src/sdks/js/ARCHITECTURE.md index b2a21bc3..2a007db4 100644 --- a/src/sdks/js/ARCHITECTURE.md +++ b/src/sdks/js/ARCHITECTURE.md @@ -130,6 +130,9 @@ When `engine` is omitted, the default is consistent: `@oliphaunt/node-direct-*` Node-API adapter optional package, built from the `oliphaunt-node-direct-*` release assets, and loads it without `postinstall`, node-gyp, Rust, Cargo, or third-party FFI packages; +- the split `@oliphaunt/tools-*` package is resolved for Node, Bun, and Deno + package-managed native installs and merged with the root `liboliphaunt` + runtime package before startup; - native direct extension package materialization is shared by Node and Bun. Deno direct mode may use extensions only with an explicit prepared `runtimeDirectory`; package-managed Deno extension materialization must remain diff --git a/src/sdks/js/README.md b/src/sdks/js/README.md index 9310075a..a4e92a38 100644 --- a/src/sdks/js/README.md +++ b/src/sdks/js/README.md @@ -37,8 +37,9 @@ On supported desktop targets, package managers install the matching `@oliphaunt/node-direct-*` packages. Each `@oliphaunt/liboliphaunt-*` package contains the matching native library plus the root PostgreSQL runtime (`postgres`, `initdb`, and `pg_ctl`), while `@oliphaunt/tools-*` carries -`pg_dump` and `psql`. Runtime startup uses those installed packages and never -downloads GitHub release assets. +`pg_dump` and `psql`. Node, Bun, and Deno package-managed native startup +validate the split tools package and use a merged runtime tree from the +installed packages; startup never downloads GitHub release assets. There is no `postinstall` native compilation step and no package-manager native addon approval in the normal path: Node, Bun, and Deno consumers do not install Rust, run Cargo, build PostgreSQL, or copy Oliphaunt native artifacts. The diff --git a/src/sdks/js/src/__tests__/native-bindings.test.ts b/src/sdks/js/src/__tests__/native-bindings.test.ts index 5bea696f..24f0f210 100644 --- a/src/sdks/js/src/__tests__/native-bindings.test.ts +++ b/src/sdks/js/src/__tests__/native-bindings.test.ts @@ -232,6 +232,7 @@ async function testDenoAssetResolverHonorsExplicitPaths(): Promise { libraryPath: '/tmp/liboliphaunt.dylib', runtimeDirectory: '/tmp/oliphaunt-deno-runtime', icuDataDirectory: undefined, + packageManaged: false, }); await assert.rejects(async () => resolveDenoNativeInstall(), /only be used inside Deno/); } finally { diff --git a/src/sdks/js/src/native/assets-deno.ts b/src/sdks/js/src/native/assets-deno.ts index ac257eb7..92ecda12 100644 --- a/src/sdks/js/src/native/assets-deno.ts +++ b/src/sdks/js/src/native/assets-deno.ts @@ -1,3 +1,7 @@ +import { createHash } from 'node:crypto'; +import { join } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + import { liboliphauntPackageTarget, type NativePackageTarget, @@ -9,13 +13,19 @@ export type ResolvedDenoNativeInstall = { libraryPath: string; runtimeDirectory?: string; icuDataDirectory?: string; + packageManaged: boolean; }; type DenoRuntime = { build: { os: string; arch: string }; + env?: { get(name: string): string | undefined }; readTextFile(path: string | URL): Promise; + writeTextFile(path: string | URL, data: string): Promise; readDir(path: string | URL): AsyncIterable<{ name: string; isFile?: boolean; isDirectory?: boolean }>; stat(path: string | URL): Promise<{ isFile?: boolean; isDirectory?: boolean }>; + mkdir(path: string | URL, options?: { recursive?: boolean }): Promise; + remove(path: string | URL, options?: { recursive?: boolean }): Promise; + copyFile(from: string | URL, to: string | URL): Promise; }; type PackageMetadata = { @@ -37,6 +47,17 @@ type LiboliphauntPackageMetadata = { }; }; +type NativeToolsPackageMetadata = { + name?: string; + version?: string; + oliphaunt?: { + product?: string; + kind?: string; + target?: string; + runtimeRelativePath?: string; + }; +}; + type IcuPackageMetadata = { name?: string; version?: string; @@ -67,6 +88,7 @@ export async function resolveDenoNativeInstall( libraryPath: explicit, runtimeDirectory: resolveExplicitRuntimeDirectory(), icuDataDirectory, + packageManaged: false, }; } @@ -140,13 +162,136 @@ async function resolvePackageNativeInstall( `${target.packageName} runtime directory metadata`, ); await requireDirectory(deno, runtimeUrl, `${target.packageName} runtime directory`); + for (const tool of nativeRuntimeToolsForTarget(target.id)) { + await requireFile( + deno, + new URL(`bin/${tool}`, directoryUrl(runtimeUrl)), + `${target.packageName} runtime tool bin/${tool}`, + ); + } + const tools = await resolveDenoNativeToolsPackage(deno, target, expectedVersion); + const libraryPath = fileURLToPath(libraryUrl); + const mergedRuntimeDirectory = await materializeDenoToolsRuntime(deno, { + target: target.id, + libraryPath, + runtimePackage: { + name: target.packageName, + version: packageJson.version, + runtimeDirectory: fileURLToPath(runtimeUrl), + runtimeUrl, + }, + toolsPackage: tools, + }); return { - libraryPath: decodeURIComponent(libraryUrl.pathname), - runtimeDirectory: decodeURIComponent(runtimeUrl.pathname.replace(/\/+$/, '')), + libraryPath, + runtimeDirectory: mergedRuntimeDirectory, icuDataDirectory, + packageManaged: true, + }; +} + +async function resolveDenoNativeToolsPackage( + deno: DenoRuntime, + target: NativePackageTarget, + expectedVersion: string, +): Promise<{ name: string; version: string; runtimeDirectory: string; runtimeUrl: URL }> { + const packageJsonUrl = resolvePackageJsonUrl(target.toolsPackageName); + const packageJson = JSON.parse( + await deno.readTextFile(packageJsonUrl), + ) as NativeToolsPackageMetadata; + if (packageJson.name !== target.toolsPackageName) { + throw new Error( + `${target.toolsPackageName} package metadata has name ${packageJson.name ?? ''}`, + ); + } + if (packageJson.version !== expectedVersion) { + throw new Error( + `${target.toolsPackageName} version ${packageJson.version ?? ''} does not match @oliphaunt/ts liboliphauntVersion ${expectedVersion}`, + ); + } + if (packageJson.oliphaunt?.product !== 'oliphaunt-tools') { + throw new Error(`${target.toolsPackageName} package metadata does not declare oliphaunt-tools`); + } + if (packageJson.oliphaunt?.kind !== 'native-tools') { + throw new Error(`${target.toolsPackageName} package metadata does not declare native tools`); + } + if (packageJson.oliphaunt?.target !== target.id) { + throw new Error(`${target.toolsPackageName} package metadata does not target ${target.id}`); + } + const runtimeUrl = resolvePackageRelativeUrl( + new URL('.', packageJsonUrl), + packageJson.oliphaunt?.runtimeRelativePath ?? target.toolsRuntimeRelativePath, + `${target.toolsPackageName} runtime directory metadata`, + ); + await requireDirectory(deno, runtimeUrl, `${target.toolsPackageName} runtime directory`); + for (const tool of nativeClientToolsForTarget(target.id)) { + await requireFile( + deno, + new URL(`bin/${tool}`, directoryUrl(runtimeUrl)), + `${target.toolsPackageName} native tool bin/${tool}`, + ); + } + return { + name: target.toolsPackageName, + version: packageJson.version, + runtimeDirectory: fileURLToPath(runtimeUrl), + runtimeUrl, }; } +async function materializeDenoToolsRuntime( + deno: DenoRuntime, + config: { + target: string; + libraryPath: string; + runtimePackage: { + name: string; + version?: string; + runtimeDirectory: string; + runtimeUrl: URL; + }; + toolsPackage: { + name: string; + version: string; + runtimeDirectory: string; + runtimeUrl: URL; + }; + }, +): Promise { + const cacheRoot = denoRuntimeCacheRoot(deno); + const root = pathToFileURL(join(cacheRoot, runtimeCacheKey(config))); + const runtimeUrl = pathToFileURL(join(fileURLToPath(root), 'runtime')); + const marker = pathToFileURL(join(fileURLToPath(root), 'manifest.json')); + const manifest = JSON.stringify( + { + target: config.target, + libraryPath: config.libraryPath, + runtimePackage: { + name: config.runtimePackage.name, + version: config.runtimePackage.version, + runtimeDirectory: config.runtimePackage.runtimeDirectory, + }, + toolsPackage: { + name: config.toolsPackage.name, + version: config.toolsPackage.version, + runtimeDirectory: config.toolsPackage.runtimeDirectory, + }, + }, + null, + 2, + ); + if ((await optionalReadText(deno, marker)) === manifest) { + return fileURLToPath(runtimeUrl); + } + + await removeTree(deno, root); + await deno.mkdir(root, { recursive: true }); + await copyDirectory(deno, config.runtimePackage.runtimeUrl, runtimeUrl); + await copyDirectory(deno, config.toolsPackage.runtimeUrl, runtimeUrl); + await deno.writeTextFile(marker, manifest); + return fileURLToPath(runtimeUrl); +} + async function resolveDenoIcuDataDirectory( deno: DenoRuntime, expectedVersion: string, @@ -180,7 +325,7 @@ async function resolveDenoIcuDataDirectory( `${packageName} ICU data directory metadata`, ); await requireIcuDataDirectory(deno, dataUrl, `${packageName} ICU data directory`); - return decodeURIComponent(dataUrl.pathname.replace(/\/+$/, '')); + return fileURLToPath(dataUrl); } export function resolvePackageRelativeUrl( @@ -221,6 +366,75 @@ function safePackageRelativePath(metadataPath: string, source: string): string { return normalized; } +async function copyDirectory(deno: DenoRuntime, source: URL, destination: URL): Promise { + await deno.mkdir(destination, { recursive: true }); + for await (const entry of deno.readDir(source)) { + const sourceChild = new URL(encodePathSegment(entry.name), directoryUrl(source)); + const destinationChild = new URL(encodePathSegment(entry.name), directoryUrl(destination)); + if (entry.isDirectory === true) { + await copyDirectory(deno, sourceChild, destinationChild); + } else if (entry.isFile === true) { + await deno.copyFile(sourceChild, destinationChild); + } + } +} + +async function optionalReadText( + deno: DenoRuntime, + path: string | URL, +): Promise { + try { + return await deno.readTextFile(path); + } catch { + return undefined; + } +} + +async function removeTree(deno: DenoRuntime, path: string | URL): Promise { + try { + await deno.remove(path, { recursive: true }); + } catch {} +} + +function denoRuntimeCacheRoot(deno: DenoRuntime): string { + const temp = + denoEnv(deno, 'TMPDIR') ?? + denoEnv(deno, 'TMP') ?? + denoEnv(deno, 'TEMP') ?? + (deno.build.os === 'windows' ? 'C:\\Temp' : '/tmp'); + return join(temp, 'oliphaunt-js-runtime-cache'); +} + +function denoEnv(deno: DenoRuntime, name: string): string | undefined { + try { + return deno.env?.get(name); + } catch { + return undefined; + } +} + +function nativeRuntimeToolsForTarget(target: string): string[] { + return target === 'windows-x64-msvc' + ? ['initdb.exe', 'pg_ctl.exe', 'postgres.exe'] + : ['initdb', 'pg_ctl', 'postgres']; +} + +function nativeClientToolsForTarget(target: string): string[] { + return target === 'windows-x64-msvc' ? ['pg_dump.exe', 'psql.exe'] : ['pg_dump', 'psql']; +} + +function runtimeCacheKey(value: unknown): string { + return createHash('sha256').update(JSON.stringify(value)).digest('hex').slice(0, 32); +} + +function directoryUrl(url: URL): URL { + return url.href.endsWith('/') ? url : new URL(`${url.href}/`); +} + +function encodePathSegment(value: string): string { + return encodeURIComponent(value).replaceAll('%2F', '/'); +} + function resolvePackageJsonUrl(packageName: string): URL { const resolver = (import.meta as ImportMeta & { resolve?: (specifier: string) => string }) .resolve; diff --git a/src/sdks/js/src/native/deno.ts b/src/sdks/js/src/native/deno.ts index 9c5f0cdb..48accf37 100644 --- a/src/sdks/js/src/native/deno.ts +++ b/src/sdks/js/src/native/deno.ts @@ -75,7 +75,11 @@ export async function createDenoNativeBinding( return BigInt(symbols.oliphaunt_capabilities() as bigint | number); }, open(config: NativeOpenConfig): NativeHandle { - if (config.extensions.length > 0 && config.runtimeDirectory === undefined) { + if ( + config.extensions.length > 0 && + (config.runtimeDirectory === undefined || + (install.packageManaged && config.runtimeDirectory === install.runtimeDirectory)) + ) { throw new Error( `Deno nativeDirect does not automatically materialize extension packages; pass runtimeDirectory with the selected extension assets or use Node/Bun nativeDirect. Selected extensions: ${config.extensions.join(', ')}`, ); diff --git a/src/sdks/js/tools/check-sdk.sh b/src/sdks/js/tools/check-sdk.sh index 15b95719..dd30f1dc 100755 --- a/src/sdks/js/tools/check-sdk.sh +++ b/src/sdks/js/tools/check-sdk.sh @@ -388,6 +388,14 @@ require_source_text "$root/tools/release/release.py" "node_direct_optional_npm_t "Node direct release dry-run must validate staged optional npm tarballs from builder jobs" require_source_text "$package_dir/src/native/assets-deno.ts" "runtimeRelativePath" \ "TypeScript Deno native binding must resolve runtime resources from the selected liboliphaunt package" +require_source_text "$package_dir/src/native/assets-deno.ts" "target.toolsPackageName" \ + "TypeScript Deno native binding must resolve the split oliphaunt-tools package" +require_source_text "$package_dir/src/native/assets-deno.ts" "materializeDenoToolsRuntime" \ + "TypeScript Deno native binding must merge liboliphaunt and oliphaunt-tools runtime trees" +require_source_text "$package_dir/src/native/assets-deno.ts" "nativeClientToolsForTarget" \ + "TypeScript Deno native binding must validate pg_dump and psql in the split tools package" +require_source_text "$package_dir/src/native/deno.ts" "install.packageManaged" \ + "TypeScript Deno nativeDirect must reject registry-managed extension materialization until it has a dedicated resolver" require_source_text "$package_dir/src/native/tar.ts" "extractTarArchive" \ "TypeScript SDK must extract verified liboliphaunt release assets without shelling out" require_source_text "$package_dir/src/client.ts" "supportedModes(options: SupportedModesOptions = {}): Promise" \ diff --git a/tools/policy/check-sdk-parity.sh b/tools/policy/check-sdk-parity.sh index b8a7720a..ec32d11d 100755 --- a/tools/policy/check-sdk-parity.sh +++ b/tools/policy/check-sdk-parity.sh @@ -180,6 +180,14 @@ require_manifest_text typescript 'tool_resolution = "split-oliphaunt-tools-npm-p "SDK manifest must declare TypeScript split oliphaunt-tools npm resolution" require_manifest_text typescript 'extension_resolution = "node-bun-exact-extension-npm-packages-deno-explicit-runtimeDirectory"' \ "SDK manifest must declare TypeScript Node/Bun registry extension resolution and Deno's explicit-runtimeDirectory gap" +require_text src/sdks/js/src/native/assets-deno.ts "target.toolsPackageName" \ + "TypeScript Deno native resolver must consume the split oliphaunt-tools package" +require_text src/sdks/js/src/native/assets-deno.ts "materializeDenoToolsRuntime" \ + "TypeScript Deno native resolver must merge liboliphaunt and oliphaunt-tools runtime trees" +require_text src/sdks/js/src/native/assets-deno.ts "nativeClientToolsForTarget" \ + "TypeScript Deno native resolver must validate pg_dump and psql in split tools packages" +require_text src/sdks/js/src/native/deno.ts "install.packageManaged" \ + "TypeScript Deno nativeDirect must keep registry-managed extension materialization explicitly unsupported" require_text docs/maintainers/sdk-products-policy.md "These are product SDKs, not auxiliary bindings." \ "SDK maintainer policy must frame Rust/Swift/Kotlin/RN as product SDKs" require_text docs/maintainers/sdk-products-policy.md '`tools/policy/sdk-manifest.toml` is the repo-level SDK registry kept for' \ From abb0b146b82cfe99f2c19c1c24704e9d82684335 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 10:21:49 +0000 Subject: [PATCH 066/137] fix: align deno server native package resolution --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 4 + src/sdks/js/ARCHITECTURE.md | 5 +- src/sdks/js/README.md | 6 +- .../js/src/__tests__/runtime-modes.test.ts | 38 ++++++++- src/sdks/js/src/native/assets-deno.ts | 7 ++ src/sdks/js/src/runtime/server.ts | 81 ++++++++++++++----- src/sdks/js/tools/check-sdk.sh | 4 + tools/policy/check-sdk-parity.sh | 4 + 8 files changed, 127 insertions(+), 22 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index a4d307c6..f8020358 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -302,6 +302,10 @@ review production pipelines, then normalize implementation details. merged runtime tree from the installed `liboliphaunt` and tools packages. Package-managed extension materialization remains explicitly unsupported for Deno until it has a real extension resolver/cache path. +- JS Deno nativeServer package-managed startup now uses the same Deno native + resolver, so server mode gets the merged split-tools runtime and packaged ICU + sidecar without falling through the Node resolver. Deno server extensions + keep the explicit prepared-`serverToolDirectory` requirement. - Release metadata checks now require the Deno package-managed extension rejection guard and its unit test, so the documented Deno limitation cannot silently drift from Node/Bun behavior. diff --git a/src/sdks/js/ARCHITECTURE.md b/src/sdks/js/ARCHITECTURE.md index 2a007db4..7099b7bc 100644 --- a/src/sdks/js/ARCHITECTURE.md +++ b/src/sdks/js/ARCHITECTURE.md @@ -136,7 +136,10 @@ When `engine` is omitted, the default is consistent: - native direct extension package materialization is shared by Node and Bun. Deno direct mode may use extensions only with an explicit prepared `runtimeDirectory`; package-managed Deno extension materialization must remain - a clear unsupported-feature error until it has a real resolver/cache path; + a clear unsupported-feature error until it has a real resolver/cache path. + Deno server mode follows the same explicit prepared-runtime rule for + extensions while still using the package-managed split tools resolver for the + base server toolchain; - `nativeBroker`: available when the broker helper resolves from an explicit override, package-adjacent executable, or verified Rust SDK release asset, the matching `liboliphaunt` install resolves, and the current runtime can spawn diff --git a/src/sdks/js/README.md b/src/sdks/js/README.md index a4e92a38..4c37edf3 100644 --- a/src/sdks/js/README.md +++ b/src/sdks/js/README.md @@ -80,8 +80,10 @@ validate that it was built for the same liboliphaunt version as extension SQL files and native modules. Deno nativeDirect does not yet materialize extension packages automatically; pass an explicit `runtimeDirectory` that already contains the selected extension assets, or use -Node/Bun for registry-managed extension resolution. Do not copy extension -release assets into the application bundle by hand. +Node/Bun for registry-managed extension resolution. Deno nativeServer has the +same limitation for package-managed extension resolution; pass a prepared +`serverToolDirectory` when server mode needs extension assets. Do not copy +extension release assets into the application bundle by hand. ## Compatibility diff --git a/src/sdks/js/src/__tests__/runtime-modes.test.ts b/src/sdks/js/src/__tests__/runtime-modes.test.ts index fe5e8827..f97df953 100644 --- a/src/sdks/js/src/__tests__/runtime-modes.test.ts +++ b/src/sdks/js/src/__tests__/runtime-modes.test.ts @@ -39,6 +39,7 @@ async function main(): Promise { await testServerSupportReportsMissingExecutable(); await testServerStartupTimeoutEnvIsValidatedBeforeProcessSetup(); await testServerRuntimeEnvIncludesPackagedLibraryDir(); + await testDenoServerModeRejectsPackageManagedExtensions(); testPgwireStartupCancelAndBackendKeyFrames(); await testNodeAdapterUtilities(); } @@ -200,6 +201,7 @@ async function testServerRuntimeEnvIncludesPackagedLibraryDir(): Promise { const runtime = join(root, 'runtime'); const toolDirectory = join(runtime, 'bin'); const libDirectory = join(runtime, 'lib'); + const icuDirectory = join(root, 'icu'); const envName = process.platform === 'darwin' ? 'DYLD_LIBRARY_PATH' @@ -211,12 +213,13 @@ async function testServerRuntimeEnvIncludesPackagedLibraryDir(): Promise { await mkdir(toolDirectory, { recursive: true }); await mkdir(libDirectory, { recursive: true }); process.env[envName] = 'existing-runtime-path'; - const env = await nativeServerRuntimeEnv(toolDirectory); + const env = await nativeServerRuntimeEnv(toolDirectory, icuDirectory); const expectedPrefix = process.platform === 'win32' ? [toolDirectory, libDirectory, 'existing-runtime-path'] : [libDirectory, 'existing-runtime-path']; assert.equal(env[envName], expectedPrefix.join(delimiter)); + assert.equal(env.ICU_DATA, icuDirectory); } finally { if (previous === undefined) { delete process.env[envName]; @@ -227,6 +230,39 @@ async function testServerRuntimeEnvIncludesPackagedLibraryDir(): Promise { } } +async function testDenoServerModeRejectsPackageManagedExtensions(): Promise { + const previousDeno = (globalThis as { Deno?: unknown }).Deno; + const previousPostgres = process.env.OLIPHAUNT_POSTGRES; + try { + delete process.env.OLIPHAUNT_POSTGRES; + (globalThis as { Deno?: unknown }).Deno = {}; + const binding = createServerRuntimeBinding(); + await assert.rejects( + () => + Promise.resolve( + binding.open( + normalizedTestConfig('/tmp/oliphaunt-js-deno-server-extension', { + engine: 'nativeServer', + extensions: ['hstore'], + }), + ), + ), + /Deno nativeServer does not automatically materialize extension packages/, + ); + } finally { + if (previousDeno === undefined) { + delete (globalThis as { Deno?: unknown }).Deno; + } else { + (globalThis as { Deno?: unknown }).Deno = previousDeno; + } + if (previousPostgres === undefined) { + delete process.env.OLIPHAUNT_POSTGRES; + } else { + process.env.OLIPHAUNT_POSTGRES = previousPostgres; + } + } +} + function normalizedTestConfig( root: string, overrides: Partial = {}, diff --git a/src/sdks/js/src/native/assets-deno.ts b/src/sdks/js/src/native/assets-deno.ts index 92ecda12..8216a0ae 100644 --- a/src/sdks/js/src/native/assets-deno.ts +++ b/src/sdks/js/src/native/assets-deno.ts @@ -375,6 +375,13 @@ async function copyDirectory(deno: DenoRuntime, source: URL, destination: URL): await copyDirectory(deno, sourceChild, destinationChild); } else if (entry.isFile === true) { await deno.copyFile(sourceChild, destinationChild); + } else { + const info = await deno.stat(sourceChild); + if (info.isDirectory === true) { + await copyDirectory(deno, sourceChild, destinationChild); + } else if (info.isFile === true) { + await deno.copyFile(sourceChild, destinationChild); + } } } } diff --git a/src/sdks/js/src/runtime/server.ts b/src/sdks/js/src/runtime/server.ts index a840e629..345648f7 100644 --- a/src/sdks/js/src/runtime/server.ts +++ b/src/sdks/js/src/runtime/server.ts @@ -7,6 +7,7 @@ import { createServer } from 'node:net'; import type { NormalizedOpenConfig } from '../config.js'; import { simpleQuery } from '../protocol.js'; import type { BackupFormat, EngineCapabilities, EngineModeSupport } from '../types.js'; +import { envVar } from '../native/common.js'; import { connectEndpoint, removeTree, @@ -28,6 +29,13 @@ const SERVER_STARTUP_TIMEOUT_MS_ENV = 'OLIPHAUNT_SERVER_STARTUP_TIMEOUT_MS'; const DEFAULT_STARTUP_TIMEOUT_MS = 60_000; const CONNECT_RETRY_MS = 50; const STOP_TIMEOUT_MS = 5_000; +const OLIPHAUNT_POSTGRES_ENV = 'OLIPHAUNT_POSTGRES'; + +type ServerTools = { + executable: string; + toolDirectory: string; + icuDataDirectory?: string; +}; export function createServerRuntimeBinding(): RuntimeBinding { return { @@ -208,11 +216,11 @@ async function openServer(config: NormalizedOpenConfig): Promise { const pgCtl = await optionalTool(toolDirectory, 'pg_ctl'); const pgDump = await optionalTool(toolDirectory, 'pg_dump'); const port = config.serverPort ?? (await pickPort()); - socketDir = process.platform === 'win32' ? undefined : await createSocketDir(); + socketDir = hostPlatform() === 'win32' ? undefined : await createSocketDir(); child = spawnManagedChild({ executable, args: postgresArgs(config, port, socketDir), - env: await nativeServerRuntimeEnv(toolDirectory), + env: await nativeServerRuntimeEnv(toolDirectory, tools.icuDataDirectory), }); const endpoint = sdkEndpoint(port, socketDir); const client = await waitForServer( @@ -357,7 +365,7 @@ function percentEncode(value: string): string { } function serverStartupTimeoutMs(): number { - const value = process.env[SERVER_STARTUP_TIMEOUT_MS_ENV]; + const value = envVar(SERVER_STARTUP_TIMEOUT_MS_ENV); if (value === undefined || value.length === 0) { return DEFAULT_STARTUP_TIMEOUT_MS; } @@ -374,10 +382,10 @@ async function resolveServerTools(options: { serverExecutable?: string; serverToolDirectory?: string; extensions?: readonly string[]; -}): Promise<{ executable: string; toolDirectory: string }> { +}): Promise { const candidates = [ options.serverExecutable, - process.env.OLIPHAUNT_POSTGRES, + envVar(OLIPHAUNT_POSTGRES_ENV), options.serverToolDirectory === undefined ? undefined : join(options.serverToolDirectory, executableName('postgres')), @@ -391,24 +399,42 @@ async function resolveServerTools(options: { } } if (options.serverExecutable !== undefined || options.serverToolDirectory !== undefined) { - throw new Error('set serverExecutable, serverToolDirectory, or OLIPHAUNT_POSTGRES'); + throw new Error(`set serverExecutable, serverToolDirectory, or ${OLIPHAUNT_POSTGRES_ENV}`); } - const install = await materializeNodeExtensionInstall( - await resolveNodeNativeInstall(), - options.extensions ?? [], - ); + const install = await resolvePackageManagedServerInstall(options.extensions ?? []); if (install.runtimeDirectory !== undefined) { const toolDirectory = join(install.runtimeDirectory, 'bin'); const executable = join(toolDirectory, executableName('postgres')); if (await isFile(executable)) { - return { executable, toolDirectory }; + return { executable, toolDirectory, icuDataDirectory: install.icuDataDirectory }; } } throw new Error( - 'set serverExecutable, serverToolDirectory, or OLIPHAUNT_POSTGRES, or install @oliphaunt/ts with optional native runtime packages enabled', + `set serverExecutable, serverToolDirectory, or ${OLIPHAUNT_POSTGRES_ENV}, or install @oliphaunt/ts with optional native runtime packages enabled`, ); } +async function resolvePackageManagedServerInstall( + extensions: readonly string[], +): Promise<{ runtimeDirectory?: string; icuDataDirectory?: string }> { + if (runtimeName() === 'deno') { + if (extensions.length > 0) { + throw new Error( + `Deno nativeServer does not automatically materialize extension packages; pass serverToolDirectory with the selected extension assets or use Node/Bun nativeServer. Selected extensions: ${extensions.join(', ')}`, + ); + } + const install = await import('../native/assets-deno.js').then((module) => + module.resolveDenoNativeInstall(), + ); + return { + runtimeDirectory: install.runtimeDirectory, + icuDataDirectory: install.icuDataDirectory, + }; + } + + return materializeNodeExtensionInstall(await resolveNodeNativeInstall(), extensions); +} + async function optionalTool( directory: string | undefined, name: string, @@ -421,7 +447,7 @@ async function optionalTool( } function executableName(name: string): string { - return process.platform === 'win32' ? `${name}.exe` : name; + return hostPlatform() === 'win32' ? `${name}.exe` : name; } async function isFile(path: string): Promise { @@ -440,14 +466,17 @@ async function isDirectory(path: string): Promise { } } -export async function nativeServerRuntimeEnv(toolDirectory: string): Promise> { +export async function nativeServerRuntimeEnv( + toolDirectory: string, + icuDataDirectory?: string, +): Promise> { const runtimeDirectory = dirname(toolDirectory); const env: Record = {}; const dynamicLibraryDirs = await nativeDynamicLibraryDirs(runtimeDirectory); const dynamicLibraryEnv = prependEnvPaths( nativeDynamicLibraryEnvName(), dynamicLibraryDirs, - process.env[nativeDynamicLibraryEnvName()], + envVar(nativeDynamicLibraryEnvName()), ); if (dynamicLibraryEnv !== undefined) { env[nativeDynamicLibraryEnvName()] = dynamicLibraryEnv; @@ -458,6 +487,13 @@ export async function nativeServerRuntimeEnv(toolDirectory: string): Promise { const dirs: string[] = []; - if (process.platform === 'win32') { + if (hostPlatform() === 'win32') { const bin = join(runtimeDirectory, 'bin'); if (await isDirectory(bin)) { dirs.push(bin); @@ -563,6 +600,14 @@ function sleep(ms: number): Promise { return new Promise((resolveSleep) => setTimeout(resolveSleep, ms)); } +function hostPlatform(): string { + const denoOs = (globalThis as { Deno?: { build?: { os?: string } } }).Deno?.build?.os; + if (denoOs === 'windows') { + return 'win32'; + } + return denoOs ?? process.platform; +} + function asServerHandle(handle: RuntimeHandle): ServerHandle { if (handle instanceof ServerHandle) { return handle; diff --git a/src/sdks/js/tools/check-sdk.sh b/src/sdks/js/tools/check-sdk.sh index dd30f1dc..c012afc5 100755 --- a/src/sdks/js/tools/check-sdk.sh +++ b/src/sdks/js/tools/check-sdk.sh @@ -396,6 +396,10 @@ require_source_text "$package_dir/src/native/assets-deno.ts" "nativeClientToolsF "TypeScript Deno native binding must validate pg_dump and psql in the split tools package" require_source_text "$package_dir/src/native/deno.ts" "install.packageManaged" \ "TypeScript Deno nativeDirect must reject registry-managed extension materialization until it has a dedicated resolver" +require_source_text "$package_dir/src/runtime/server.ts" "resolveDenoNativeInstall" \ + "TypeScript Deno nativeServer must resolve package-managed server tools through the Deno native resolver" +require_source_text "$package_dir/src/runtime/server.ts" "Deno nativeServer does not automatically materialize extension packages" \ + "TypeScript Deno nativeServer must fail clearly for registry-managed extension materialization" require_source_text "$package_dir/src/native/tar.ts" "extractTarArchive" \ "TypeScript SDK must extract verified liboliphaunt release assets without shelling out" require_source_text "$package_dir/src/client.ts" "supportedModes(options: SupportedModesOptions = {}): Promise" \ diff --git a/tools/policy/check-sdk-parity.sh b/tools/policy/check-sdk-parity.sh index ec32d11d..5cad804d 100755 --- a/tools/policy/check-sdk-parity.sh +++ b/tools/policy/check-sdk-parity.sh @@ -188,6 +188,10 @@ require_text src/sdks/js/src/native/assets-deno.ts "nativeClientToolsForTarget" "TypeScript Deno native resolver must validate pg_dump and psql in split tools packages" require_text src/sdks/js/src/native/deno.ts "install.packageManaged" \ "TypeScript Deno nativeDirect must keep registry-managed extension materialization explicitly unsupported" +require_text src/sdks/js/src/runtime/server.ts "resolveDenoNativeInstall" \ + "TypeScript Deno nativeServer must resolve package-managed server tools through the Deno native resolver" +require_text src/sdks/js/src/runtime/server.ts "Deno nativeServer does not automatically materialize extension packages" \ + "TypeScript Deno nativeServer must fail clearly for registry-managed extension materialization" require_text docs/maintainers/sdk-products-policy.md "These are product SDKs, not auxiliary bindings." \ "SDK maintainer policy must frame Rust/Swift/Kotlin/RN as product SDKs" require_text docs/maintainers/sdk-products-policy.md '`tools/policy/sdk-manifest.toml` is the repo-level SDK registry kept for' \ From 5bf85a77c062e18e784a0dd380a70366ea55e053 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 10:24:40 +0000 Subject: [PATCH 067/137] chore: remove python from tool launchers --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 4 +++ tools/dev/bun.sh | 31 ++++++------------- tools/dev/deno.sh | 15 ++------- tools/policy/check-tooling-stack.sh | 10 ++++++ 4 files changed, 27 insertions(+), 33 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index f8020358..fb85997f 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -217,6 +217,10 @@ review production pipelines, then normalize implementation details. artifact crate versions and path dependencies through `bun tools/policy/check-wasix-release-dependency-invariants.mjs`; the shell wrapper still owns the Cargo dependency-tree compiler/runtime exclusion gates. +- The pinned Bun and Deno developer launchers now use `unzip` for release + archive extraction instead of inline Python. `check-tooling-stack.sh` rejects + reintroducing Python in `tools/dev/bun.sh` or `tools/dev/deno.sh`, while the + launchers keep using official pinned release archives from `.prototools`. - CI/release producer-to-consumer audit found no P0/P1 mapping gaps across Cargo, npm, Maven, SwiftPM, or GitHub release assets. Existing `release.py check`, artifact-target, release-metadata, consumer-shape, and diff --git a/tools/dev/bun.sh b/tools/dev/bun.sh index 28a2f79c..9d05316f 100755 --- a/tools/dev/bun.sh +++ b/tools/dev/bun.sh @@ -17,7 +17,7 @@ proto_version() { awk -F '=' -v tool="$tool" ' $1 ~ "^[[:space:]]*" tool "[[:space:]]*$" { value=$2 - gsub(/^[[:space:]\"]+|[[:space:]\"]+$/, "", value) + gsub(/^[[:space:]"]+|[[:space:]"]+$/, "", value) print value found=1 } @@ -67,7 +67,7 @@ install_dir="$root/target/oliphaunt-tools/bun/v$version/$target" bun_bin="$install_dir/$exe_name" if [[ ! -x "$bun_bin" ]]; then command -v curl >/dev/null 2>&1 || fail "missing required command: curl" - command -v python3 >/dev/null 2>&1 || fail "missing required command: python3" + command -v unzip >/dev/null 2>&1 || fail "missing required command: unzip" mkdir -p "$install_dir" archive="$install_dir/bun.zip" url="https://github.com/oven-sh/bun/releases/download/bun-v$version/$asset" @@ -75,25 +75,14 @@ if [[ ! -x "$bun_bin" ]]; then rm -rf "$tmp_dir" mkdir -p "$tmp_dir" curl --fail --location --retry 3 --retry-delay 2 --output "$archive" "$url" - extracted_bin="$(python3 - "$archive" "$tmp_dir" "$exe_name" <<'PY' -import sys -import zipfile -from pathlib import Path - -archive = Path(sys.argv[1]) -target = Path(sys.argv[2]) -exe_name = sys.argv[3] -with zipfile.ZipFile(archive) as zf: - zf.extractall(target) -matches = [path for path in target.rglob(exe_name) if path.is_file()] -if len(matches) != 1: - print(f"Bun archive must contain exactly one {exe_name}, found {len(matches)}", file=sys.stderr) - for match in matches: - print(match, file=sys.stderr) - sys.exit(1) -print(matches[0]) -PY -)" + unzip -q "$archive" -d "$tmp_dir" + mapfile -t matches < <(find "$tmp_dir" -type f -name "$exe_name" | sort) + if [[ "${#matches[@]}" -ne 1 ]]; then + echo "Bun archive must contain exactly one $exe_name, found ${#matches[@]}" >&2 + printf '%s\n' "${matches[@]}" >&2 + exit 1 + fi + extracted_bin="${matches[0]}" mv "$extracted_bin" "$bun_bin" chmod +x "$bun_bin" rm -rf "$tmp_dir" "$archive" diff --git a/tools/dev/deno.sh b/tools/dev/deno.sh index 0e21c2e8..f425895d 100755 --- a/tools/dev/deno.sh +++ b/tools/dev/deno.sh @@ -17,7 +17,7 @@ proto_version() { awk -F '=' -v tool="$tool" ' $1 ~ "^[[:space:]]*" tool "[[:space:]]*$" { value=$2 - gsub(/^[[:space:]\"]+|[[:space:]\"]+$/, "", value) + gsub(/^[[:space:]"]+|[[:space:]"]+$/, "", value) print value found=1 } @@ -66,7 +66,7 @@ install_dir="$root/target/oliphaunt-tools/deno/v$version/$target" deno_bin="$install_dir/$exe_name" if [[ ! -x "$deno_bin" ]]; then command -v curl >/dev/null 2>&1 || fail "missing required command: curl" - command -v python3 >/dev/null 2>&1 || fail "missing required command: python3" + command -v unzip >/dev/null 2>&1 || fail "missing required command: unzip" mkdir -p "$install_dir" url="https://github.com/denoland/deno/releases/download/v$version/deno-$target.zip" tmp_dir="$install_dir.tmp.$$" @@ -82,16 +82,7 @@ if [[ ! -x "$deno_bin" ]]; then --connect-timeout 20 \ --output "$archive" \ "$url" - python3 - "$archive" "$tmp_dir" <<'PY' -import sys -import zipfile -from pathlib import Path - -archive = Path(sys.argv[1]) -target = Path(sys.argv[2]) -with zipfile.ZipFile(archive) as zf: - zf.extractall(target) -PY + unzip -q "$archive" -d "$tmp_dir" if [[ ! -f "$tmp_dir/$exe_name" ]]; then rm -rf "$tmp_dir" fail "Deno archive did not contain $exe_name: $url" diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 3d0e3bf7..b1ae1e13 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -198,12 +198,22 @@ grep -Fq 'tools/dev/bun.sh' tools/dev/doctor.sh || fail "pnpm doctor must report the pinned Bun launcher used by TypeScript SDK checks" grep -Fq 'https://github.com/oven-sh/bun/releases/download/bun-v$version/$asset' tools/dev/bun.sh || fail "repo Bun launcher must use official pinned Bun release binaries" +if grep -Fq 'python3' tools/dev/bun.sh; then + fail "repo Bun launcher must not use Python for archive extraction" +fi +grep -Fq 'unzip -q "$archive" -d "$tmp_dir"' tools/dev/bun.sh || + fail "repo Bun launcher must extract pinned release archives with unzip" grep -Fq 'tools/dev/bun.sh" "$package_dir/.oliphaunt-bun-smoke.ts"' src/sdks/js/tools/check-sdk.sh || fail "TypeScript SDK package checks must run Bun smoke through the pinned repo Bun launcher" grep -Fq 'missing optional deno' tools/dev/doctor.sh || fail "pnpm doctor must report the pinned Deno runtime needed by strict JSR consumer gates" grep -Fq 'https://github.com/denoland/deno/releases/download/v$version/deno-$target.zip' tools/dev/deno.sh || fail "repo Deno launcher must use official pinned Deno release binaries" +if grep -Fq 'python3' tools/dev/deno.sh; then + fail "repo Deno launcher must not use Python for archive extraction" +fi +grep -Fq 'unzip -q "$archive" -d "$tmp_dir"' tools/dev/deno.sh || + fail "repo Deno launcher must extract pinned release archives with unzip" grep -Fq 'tools/dev/deno.sh" run --allow-read --allow-env' src/sdks/js/tools/check-sdk.sh || fail "TypeScript SDK package checks must run Deno smoke through the pinned repo Deno launcher" grep -Fq 'RIPGREP_VERSION="${RIPGREP_VERSION:-15.1.0}"' tools/dev/bootstrap-tools.sh || From 787a37eb9a7abb812d52a5545e07982007663009 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 10:26:24 +0000 Subject: [PATCH 068/137] chore: remove python from tool bootstrap --- docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 3 +++ tools/dev/bootstrap-tools.sh | 12 +++++------- tools/policy/check-tooling-stack.sh | 5 +++++ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index fb85997f..8896f23e 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -221,6 +221,9 @@ review production pipelines, then normalize implementation details. archive extraction instead of inline Python. `check-tooling-stack.sh` rejects reintroducing Python in `tools/dev/bun.sh` or `tools/dev/deno.sh`, while the launchers keep using official pinned release archives from `.prototools`. +- The local maintainer tool bootstrap now also uses `unzip` instead of inline + Python for cargo-binstall zip archives, with `check-tooling-stack.sh` + rejecting Python reintroduction in `tools/dev/bootstrap-tools.sh`. - CI/release producer-to-consumer audit found no P0/P1 mapping gaps across Cargo, npm, Maven, SwiftPM, or GitHub release assets. Existing `release.py check`, artifact-target, release-metadata, consumer-shape, and diff --git a/tools/dev/bootstrap-tools.sh b/tools/dev/bootstrap-tools.sh index d4dcd73b..74eea3e6 100755 --- a/tools/dev/bootstrap-tools.sh +++ b/tools/dev/bootstrap-tools.sh @@ -132,13 +132,11 @@ install_cargo_binstall() { curl -L --fail --retry 3 --output "$archive" "$url" case "$extract" in zip) - python3 - "$archive" "$tmp" <<'PY' -import sys -import zipfile - -with zipfile.ZipFile(sys.argv[1]) as archive: - archive.extractall(sys.argv[2]) -PY + command -v unzip >/dev/null 2>&1 || { + echo "missing required command: unzip" >&2 + return 1 + } + unzip -q "$archive" -d "$tmp" ;; tgz) tar -xzf "$archive" -C "$tmp" diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index b1ae1e13..1b404cda 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -220,6 +220,11 @@ grep -Fq 'RIPGREP_VERSION="${RIPGREP_VERSION:-15.1.0}"' tools/dev/bootstrap-tool fail "local tool bootstrap must pin ripgrep" grep -Fq 'install_cargo_tool ripgrep rg "$RIPGREP_VERSION"' tools/dev/bootstrap-tools.sh || fail "local tool bootstrap must install the pinned ripgrep binary" +if grep -Fq 'python3' tools/dev/bootstrap-tools.sh; then + fail "local tool bootstrap must not use Python for archive extraction" +fi +grep -Fq 'unzip -q "$archive" -d "$tmp"' tools/dev/bootstrap-tools.sh || + fail "local tool bootstrap must extract cargo-binstall zip archives with unzip" grep -Fq 'cargo install ripgrep --version 15.1.0 --locked' .github/actions/setup-rust-tools/action.yml || fail "shared CI Rust setup must install pinned ripgrep for repo policy and native probes" grep -Fq '"$script_dir/install-actionlint.sh"' tools/dev/bootstrap-tools.sh || From 6a6f60411db803005b5e3a2d4a3b7d4aa14d94db Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 10:29:03 +0000 Subject: [PATCH 069/137] chore: remove inline python from node direct packaging --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 4 +++ .../node-direct/tools/build-node-addon.sh | 28 +++++-------------- .../node-direct/tools/check-package.sh | 4 +++ 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 8896f23e..801fd452 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -224,6 +224,10 @@ review production pipelines, then normalize implementation details. - The local maintainer tool bootstrap now also uses `unzip` instead of inline Python for cargo-binstall zip archives, with `check-tooling-stack.sh` rejecting Python reintroduction in `tools/dev/bootstrap-tools.sh`. +- Node direct addon packaging now uses the shared Bun + `tools/release/archive_dir.mjs` helper for release asset tar/zip creation and + shell `tar` for npm package membership checks, removing inline Python from + that packaging script while keeping the existing release validators intact. - CI/release producer-to-consumer audit found no P0/P1 mapping gaps across Cargo, npm, Maven, SwiftPM, or GitHub release assets. Existing `release.py check`, artifact-target, release-metadata, consumer-shape, and diff --git a/src/runtimes/node-direct/tools/build-node-addon.sh b/src/runtimes/node-direct/tools/build-node-addon.sh index c4ab4f80..9dba06cc 100755 --- a/src/runtimes/node-direct/tools/build-node-addon.sh +++ b/src/runtimes/node-direct/tools/build-node-addon.sh @@ -195,20 +195,14 @@ JS if [ "$platform" = "windows" ]; then asset="oliphaunt-node-direct-$version-$target.zip" - python3 - "$out_dir" "$asset_dir/$asset" <<'PY' -import pathlib -import sys -import zipfile - -out_dir = pathlib.Path(sys.argv[1]) -asset = pathlib.Path(sys.argv[2]) -with zipfile.ZipFile(asset, "w", compression=zipfile.ZIP_DEFLATED) as archive: - archive.write(out_dir / "oliphaunt_node.node", "oliphaunt_node.node") -PY else asset="oliphaunt-node-direct-$version-$target.tar.gz" - tar -C "$out_dir" -czf "$asset_dir/$asset" oliphaunt_node.node fi +asset_stage="$root/target/oliphaunt-node-direct/release-stage/$target" +rm -rf "$asset_stage" +mkdir -p "$asset_stage" +cp "$addon_file" "$asset_stage/oliphaunt_node.node" +tools/release/archive_dir.mjs "$asset_stage" "$asset_dir/$asset" input_dirs="${OLIPHAUNT_NODE_ADDON_ASSET_INPUT_DIRS:-${OLIPHAUNT_RELEASE_ASSET_INPUT_DIRS:-}}" if [ -n "$input_dirs" ]; then @@ -272,17 +266,9 @@ JS echo "npm pack did not create $tarball" >&2 exit 1 } -python3 - "$tarball" <<'PY' || { -import sys -import tarfile - -expected = "package/prebuilds/oliphaunt_node.node" -with tarfile.open(sys.argv[1], "r:gz") as archive: - if expected not in archive.getnames(): - raise SystemExit(1) -PY +if ! tar -tzf "$tarball" | grep -Fxq "package/prebuilds/oliphaunt_node.node"; then echo "Node direct optional npm package is missing prebuilds/oliphaunt_node.node: $tarball" >&2 exit 1 -} +fi printf 'Node direct optional npm package staged: %s\n' "$tarball" printf '%s\n' "$asset_dir/$asset" diff --git a/src/runtimes/node-direct/tools/check-package.sh b/src/runtimes/node-direct/tools/check-package.sh index 98d5b341..80484c5d 100755 --- a/src/runtimes/node-direct/tools/check-package.sh +++ b/src/runtimes/node-direct/tools/check-package.sh @@ -50,8 +50,12 @@ check_static() { "Node direct build must compile product-owned addon source" require_text "$package_dir/tools/build-node-addon.sh" "oliphaunt-node-direct-\$version-\$target.tar.gz" \ "Node direct build must emit product-scoped release assets" + require_text "$package_dir/tools/build-node-addon.sh" "tools/release/archive_dir.mjs" \ + "Node direct build must create release assets with the shared deterministic archive helper" require_text "$package_dir/tools/build-node-addon.sh" "Node direct addon smoke passed" \ "Node direct build must load-smoke the compiled addon before publishing an artifact" + reject_text "$package_dir/tools/build-node-addon.sh" "python3 -" \ + "Node direct build must not use inline Python for archive creation or package validation" reject_text "$package_dir/tools/build-node-addon.sh" "oliphaunt-js-node-direct" \ "Node direct runtime must not emit TypeScript-owned addon assets" require_text "$package_dir/native/node-addon/oliphaunt_node.cc" "NAPI_MODULE" \ From 6aa36b3361a10ebeb6cb23aa37d36c6597c83f33 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 10:38:28 +0000 Subject: [PATCH 070/137] fix: harden wasix tools artifact split --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 12 +++--- .../aot/aarch64-apple-darwin/Cargo.toml | 2 +- .../crates/aot/aarch64-apple-darwin/README.md | 4 +- .../aot/aarch64-unknown-linux-gnu/Cargo.toml | 2 +- .../aot/aarch64-unknown-linux-gnu/README.md | 4 +- .../aot/x86_64-pc-windows-msvc/Cargo.toml | 2 +- .../aot/x86_64-pc-windows-msvc/README.md | 4 +- .../aot/x86_64-unknown-linux-gnu/Cargo.toml | 2 +- .../aot/x86_64-unknown-linux-gnu/README.md | 4 +- .../wasix/crates/assets/Cargo.toml | 2 +- .../tools-aot/aarch64-apple-darwin/Cargo.toml | 2 +- .../tools-aot/aarch64-apple-darwin/README.md | 4 +- .../aarch64-unknown-linux-gnu/Cargo.toml | 2 +- .../aarch64-unknown-linux-gnu/README.md | 4 +- .../x86_64-pc-windows-msvc/Cargo.toml | 2 +- .../x86_64-pc-windows-msvc/README.md | 4 +- .../x86_64-unknown-linux-gnu/Cargo.toml | 2 +- .../x86_64-unknown-linux-gnu/README.md | 4 +- .../wasix/crates/tools/Cargo.toml | 2 +- tools/policy/check-native-boundaries.sh | 4 ++ ...ck-wasix-release-dependency-invariants.mjs | 25 +++++++---- tools/release/check_consumer_shape.py | 42 +++++++++++++++++++ tools/release/check_release_metadata.py | 7 ++++ ...kage_liboliphaunt_wasix_cargo_artifacts.py | 2 +- 24 files changed, 105 insertions(+), 39 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 801fd452..30bc5fda 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -151,11 +151,13 @@ review production pipelines, then normalize implementation details. The same packager helper also drives the WASIX AOT target-cfg dependency maps and `tools` feature dependency expectations used by release metadata, consumer-shape, and release publication checks. -- WASIX runtime and tools source crates keep `publish = false` as a - source-tree guard, but the release Cargo artifact packager removes it from - staged manifests before publishing. Release metadata now checks that behavior, - so `oliphaunt-wasix-tools` and tools-AOT crates remain registry-publishable - while `oliphaunt-wasix` installs them through optional dependencies. +- WASIX runtime, tools, root-AOT, and tools-AOT source crates keep + `publish = false` as a source-tree guard, but their descriptions now match the + public registry artifact role and the release Cargo artifact packager removes + `publish = false` from staged manifests before publishing. Release metadata + and dependency-invariant checks cover the full root/tools package family, so + `oliphaunt-wasix-tools` and tools-AOT crates remain registry-publishable while + `oliphaunt-wasix` installs them through optional dependencies. - SDK CI package artifact names now derive from release products marked `kind = "sdk"`. The release workflow and local registry publisher use `release.py ci-artifacts --family sdk-package` instead of repeating diff --git a/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/Cargo.toml b/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/Cargo.toml index 9f70bee5..77f96f1a 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/Cargo.toml +++ b/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/Cargo.toml @@ -3,7 +3,7 @@ name = "liboliphaunt-wasix-aot-aarch64-apple-darwin" version = "0.1.0" edition = "2024" rust-version = "1.93" -description = "Internal Wasmer AOT artifacts for oliphaunt-wasix on aarch64-apple-darwin" +description = "Wasmer AOT runtime artifacts for oliphaunt-wasix on aarch64-apple-darwin" repository = "https://github.com/f0rr0/oliphaunt" license = "MIT AND Apache-2.0 AND PostgreSQL" publish = false diff --git a/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/README.md b/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/README.md index f668a911..4c64eb0a 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/README.md +++ b/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/README.md @@ -1,4 +1,4 @@ # liboliphaunt-wasix-aot-aarch64-apple-darwin -Internal target-specific Wasmer AOT artifact crate for `oliphaunt-wasix`. -Do not depend on this crate directly. +Target-specific Wasmer AOT runtime artifact crate for `oliphaunt-wasix`. +Applications use it through `oliphaunt-wasix`; direct dependencies are not required. diff --git a/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/Cargo.toml b/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/Cargo.toml index 64f16d8a..fbb57cb5 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/Cargo.toml +++ b/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/Cargo.toml @@ -3,7 +3,7 @@ name = "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu" version = "0.1.0" edition = "2024" rust-version = "1.93" -description = "Internal Wasmer AOT artifacts for oliphaunt-wasix on aarch64-unknown-linux-gnu" +description = "Wasmer AOT runtime artifacts for oliphaunt-wasix on aarch64-unknown-linux-gnu" repository = "https://github.com/f0rr0/oliphaunt" license = "MIT AND Apache-2.0 AND PostgreSQL" publish = false diff --git a/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/README.md b/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/README.md index b875d8c3..16e7406b 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/README.md +++ b/src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/README.md @@ -1,4 +1,4 @@ # liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu -Internal target-specific Wasmer AOT artifact crate for `oliphaunt-wasix`. -Do not depend on this crate directly. +Target-specific Wasmer AOT runtime artifact crate for `oliphaunt-wasix`. +Applications use it through `oliphaunt-wasix`; direct dependencies are not required. diff --git a/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/Cargo.toml b/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/Cargo.toml index d8534a75..a6571e1b 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/Cargo.toml +++ b/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/Cargo.toml @@ -3,7 +3,7 @@ name = "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc" version = "0.1.0" edition = "2024" rust-version = "1.93" -description = "Internal Wasmer AOT artifacts for oliphaunt-wasix on x86_64-pc-windows-msvc" +description = "Wasmer AOT runtime artifacts for oliphaunt-wasix on x86_64-pc-windows-msvc" repository = "https://github.com/f0rr0/oliphaunt" license = "MIT AND Apache-2.0 AND PostgreSQL" publish = false diff --git a/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/README.md b/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/README.md index 5a34efd9..b99bafcc 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/README.md +++ b/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/README.md @@ -1,4 +1,4 @@ # liboliphaunt-wasix-aot-x86_64-pc-windows-msvc -Internal target-specific Wasmer AOT artifact crate for `oliphaunt-wasix`. -Do not depend on this crate directly. +Target-specific Wasmer AOT runtime artifact crate for `oliphaunt-wasix`. +Applications use it through `oliphaunt-wasix`; direct dependencies are not required. diff --git a/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/Cargo.toml b/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/Cargo.toml index fde81f39..c344fa5b 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/Cargo.toml +++ b/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/Cargo.toml @@ -3,7 +3,7 @@ name = "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu" version = "0.1.0" edition = "2024" rust-version = "1.93" -description = "Internal Wasmer AOT artifacts for oliphaunt-wasix on x86_64-unknown-linux-gnu" +description = "Wasmer AOT runtime artifacts for oliphaunt-wasix on x86_64-unknown-linux-gnu" repository = "https://github.com/f0rr0/oliphaunt" license = "MIT AND Apache-2.0 AND PostgreSQL" publish = false diff --git a/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/README.md b/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/README.md index 1838f842..8513c4ce 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/README.md +++ b/src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/README.md @@ -1,4 +1,4 @@ # liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu -Internal target-specific Wasmer AOT artifact crate for `oliphaunt-wasix`. -Do not depend on this crate directly. +Target-specific Wasmer AOT runtime artifact crate for `oliphaunt-wasix`. +Applications use it through `oliphaunt-wasix`; direct dependencies are not required. diff --git a/src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml b/src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml index c4a540bf..872f3e67 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml +++ b/src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml @@ -3,7 +3,7 @@ name = "liboliphaunt-wasix-portable" version = "0.1.0" edition = "2024" rust-version = "1.93" -description = "Internal Oliphaunt runtime and extension assets for oliphaunt-wasix" +description = "Portable WASIX runtime assets for oliphaunt-wasix" repository = "https://github.com/f0rr0/oliphaunt" homepage = "https://oliphaunt.dev" documentation = "https://docs.rs/liboliphaunt-wasix-portable" diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/Cargo.toml b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/Cargo.toml index c8e02eb4..441abcc2 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/Cargo.toml +++ b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/Cargo.toml @@ -3,7 +3,7 @@ name = "oliphaunt-wasix-tools-aot-aarch64-apple-darwin" version = "0.1.0" edition = "2024" rust-version = "1.93" -description = "Internal Wasmer AOT artifacts for oliphaunt-wasix tools on aarch64-apple-darwin" +description = "Wasmer AOT pg_dump and psql artifacts for oliphaunt-wasix on aarch64-apple-darwin" repository = "https://github.com/f0rr0/oliphaunt" license = "MIT AND Apache-2.0 AND PostgreSQL" publish = false diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/README.md b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/README.md index 23102d82..15038541 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/README.md +++ b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/README.md @@ -1,4 +1,4 @@ # oliphaunt-wasix-tools-aot-aarch64-apple-darwin -Internal target-specific Wasmer AOT artifact crate for `oliphaunt-wasix`. -Do not depend on this crate directly. +Target-specific Wasmer AOT artifact crate for `oliphaunt-wasix` pg_dump and psql. +Applications use it through the `oliphaunt-wasix` `tools` feature. diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/Cargo.toml b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/Cargo.toml index e9015723..5b8975ec 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/Cargo.toml +++ b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/Cargo.toml @@ -3,7 +3,7 @@ name = "oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu" version = "0.1.0" edition = "2024" rust-version = "1.93" -description = "Internal Wasmer AOT artifacts for oliphaunt-wasix tools on aarch64-unknown-linux-gnu" +description = "Wasmer AOT pg_dump and psql artifacts for oliphaunt-wasix on aarch64-unknown-linux-gnu" repository = "https://github.com/f0rr0/oliphaunt" license = "MIT AND Apache-2.0 AND PostgreSQL" publish = false diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/README.md b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/README.md index a209c192..b0950ddb 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/README.md +++ b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/README.md @@ -1,4 +1,4 @@ # oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu -Internal target-specific Wasmer AOT artifact crate for `oliphaunt-wasix`. -Do not depend on this crate directly. +Target-specific Wasmer AOT artifact crate for `oliphaunt-wasix` pg_dump and psql. +Applications use it through the `oliphaunt-wasix` `tools` feature. diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/Cargo.toml b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/Cargo.toml index 2d2a7815..7ecee15e 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/Cargo.toml +++ b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/Cargo.toml @@ -3,7 +3,7 @@ name = "oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc" version = "0.1.0" edition = "2024" rust-version = "1.93" -description = "Internal Wasmer AOT artifacts for oliphaunt-wasix tools on x86_64-pc-windows-msvc" +description = "Wasmer AOT pg_dump and psql artifacts for oliphaunt-wasix on x86_64-pc-windows-msvc" repository = "https://github.com/f0rr0/oliphaunt" license = "MIT AND Apache-2.0 AND PostgreSQL" publish = false diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/README.md b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/README.md index 85a746d5..fadefde4 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/README.md +++ b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/README.md @@ -1,4 +1,4 @@ # oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc -Internal target-specific Wasmer AOT artifact crate for `oliphaunt-wasix`. -Do not depend on this crate directly. +Target-specific Wasmer AOT artifact crate for `oliphaunt-wasix` pg_dump and psql. +Applications use it through the `oliphaunt-wasix` `tools` feature. diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/Cargo.toml b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/Cargo.toml index 7a9c55fd..8a07516c 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/Cargo.toml +++ b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/Cargo.toml @@ -3,7 +3,7 @@ name = "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu" version = "0.1.0" edition = "2024" rust-version = "1.93" -description = "Internal Wasmer AOT artifacts for oliphaunt-wasix tools on x86_64-unknown-linux-gnu" +description = "Wasmer AOT pg_dump and psql artifacts for oliphaunt-wasix on x86_64-unknown-linux-gnu" repository = "https://github.com/f0rr0/oliphaunt" license = "MIT AND Apache-2.0 AND PostgreSQL" publish = false diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/README.md b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/README.md index e7b3bf74..f0cac781 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/README.md +++ b/src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/README.md @@ -1,4 +1,4 @@ # oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu -Internal target-specific Wasmer AOT artifact crate for `oliphaunt-wasix`. -Do not depend on this crate directly. +Target-specific Wasmer AOT artifact crate for `oliphaunt-wasix` pg_dump and psql. +Applications use it through the `oliphaunt-wasix` `tools` feature. diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml b/src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml index f49f92b6..828c20d1 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml +++ b/src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml @@ -3,7 +3,7 @@ name = "oliphaunt-wasix-tools" version = "0.1.0" edition = "2024" rust-version = "1.93" -description = "Internal Oliphaunt WASIX PostgreSQL tool assets" +description = "WASIX pg_dump and psql assets for oliphaunt-wasix" repository = "https://github.com/f0rr0/oliphaunt" homepage = "https://oliphaunt.dev" documentation = "https://docs.rs/oliphaunt-wasix-tools" diff --git a/tools/policy/check-native-boundaries.sh b/tools/policy/check-native-boundaries.sh index f4f5fe67..e2d8ad82 100755 --- a/tools/policy/check-native-boundaries.sh +++ b/tools/policy/check-native-boundaries.sh @@ -20,9 +20,11 @@ errors: list[str] = [] legacy_package_names = { "oliphaunt-wasix", "liboliphaunt-wasix-portable", + "oliphaunt-wasix-tools", } legacy_name_prefixes = ( "liboliphaunt-wasix-aot-", + "oliphaunt-wasix-tools-aot-", ) legacy_runtime_names = { "wasmer", @@ -35,6 +37,8 @@ legacy_path_fragments = ( "src/bindings/wasix-rust/crates/oliphaunt-wasix", "src/runtimes/liboliphaunt/wasix/crates/assets", "src/runtimes/liboliphaunt/wasix/crates/aot", + "src/runtimes/liboliphaunt/wasix/crates/tools", + "src/runtimes/liboliphaunt/wasix/crates/tools-aot", ) diff --git a/tools/policy/check-wasix-release-dependency-invariants.mjs b/tools/policy/check-wasix-release-dependency-invariants.mjs index 9eb2e68c..230f6b93 100644 --- a/tools/policy/check-wasix-release-dependency-invariants.mjs +++ b/tools/policy/check-wasix-release-dependency-invariants.mjs @@ -7,7 +7,11 @@ const PRODUCT_MANIFEST_PATH = const RUNTIME_VERSION_PATH = 'src/runtimes/liboliphaunt/wasix/VERSION'; const INTERNAL_ASSETS_MANIFEST = 'src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml'; +const INTERNAL_TOOLS_MANIFEST = + 'src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml'; const INTERNAL_AOT_MANIFESTS_DIR = 'src/runtimes/liboliphaunt/wasix/crates/aot'; +const INTERNAL_TOOLS_AOT_MANIFESTS_DIR = + 'src/runtimes/liboliphaunt/wasix/crates/tools-aot'; function fail(errors) { console.error('release version invariant violations:'); @@ -53,7 +57,12 @@ function dependencyPath(spec) { } function isWasixArtifactCrate(name) { - return name === 'liboliphaunt-wasix-portable' || name.startsWith('liboliphaunt-wasix-aot-'); + return ( + name === 'liboliphaunt-wasix-portable' || + name === 'oliphaunt-wasix-tools' || + name.startsWith('liboliphaunt-wasix-aot-') || + name.startsWith('oliphaunt-wasix-tools-aot-') + ); } const productManifest = await readToml(PRODUCT_MANIFEST_PATH); @@ -74,12 +83,14 @@ for (const [tableName, deps] of dependencyTables(productManifest)) { } } -const internalManifestPaths = [INTERNAL_ASSETS_MANIFEST]; -for (const entry of (await readdir(INTERNAL_AOT_MANIFESTS_DIR, { withFileTypes: true })) - .filter((entry) => entry.isDirectory()) - .map((entry) => entry.name) - .sort()) { - internalManifestPaths.push(join(INTERNAL_AOT_MANIFESTS_DIR, entry, 'Cargo.toml')); +const internalManifestPaths = [INTERNAL_ASSETS_MANIFEST, INTERNAL_TOOLS_MANIFEST]; +for (const manifestsDir of [INTERNAL_AOT_MANIFESTS_DIR, INTERNAL_TOOLS_AOT_MANIFESTS_DIR]) { + for (const entry of (await readdir(manifestsDir, { withFileTypes: true })) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort()) { + internalManifestPaths.push(join(manifestsDir, entry, 'Cargo.toml')); + } } for (const manifestPath of internalManifestPaths) { diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index f1dd3648..4f5cf463 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -1698,6 +1698,26 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: asset_package = asset_manifest.get("package", {}) tools_manifest = read_toml("src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml") tools_package = tools_manifest.get("package", {}) + wasix_artifact_manifest_paths = [ + "src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml", + "src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml", + *[ + relative(path) + for path in sorted( + (ROOT / "src/runtimes/liboliphaunt/wasix/crates/aot").glob("*/Cargo.toml") + ) + ], + *[ + relative(path) + for path in sorted( + (ROOT / "src/runtimes/liboliphaunt/wasix/crates/tools-aot").glob("*/Cargo.toml") + ) + ], + ] + wasix_artifact_descriptions = [ + str(read_toml(path).get("package", {}).get("description", "")) + for path in wasix_artifact_manifest_paths + ] assets_build_source = read_text("src/runtimes/liboliphaunt/wasix/crates/assets/build.rs") release_workspace_source = read_text("tools/xtask/src/release_workspace.rs") tools_build_source = read_text("src/runtimes/liboliphaunt/wasix/crates/tools/build.rs") @@ -1721,6 +1741,15 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: f"src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml package={tools_package!r}", severity="P0", ) + require( + findings, + product, + "wasix-public-artifact-descriptions", + all(description and "Internal" not in description for description in wasix_artifact_descriptions), + "WASIX runtime, tools, root AOT, and tools-AOT artifact crate templates must describe the public registry artifact packages instead of calling them internal.", + wasix_artifact_manifest_paths, + severity="P0", + ) require( findings, product, @@ -1777,6 +1806,7 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: ) release_source = read_text("tools/release/release.py") wasix_packager_source = read_text("tools/release/package_liboliphaunt_wasix_cargo_artifacts.py") + wasix_dependency_invariant_source = read_text("tools/policy/check-wasix-release-dependency-invariants.mjs") workflow_source = read_text(".github/workflows/release.yml") require( findings, @@ -1814,6 +1844,18 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: ], severity="P0", ) + require( + findings, + product, + "wasix-tools-dependency-invariant", + "INTERNAL_TOOLS_MANIFEST" in wasix_dependency_invariant_source + and "INTERNAL_TOOLS_AOT_MANIFESTS_DIR" in wasix_dependency_invariant_source + and "oliphaunt-wasix-tools" in wasix_dependency_invariant_source + and "oliphaunt-wasix-tools-aot-" in wasix_dependency_invariant_source, + "WASIX release dependency invariants must cover the registry-installed tools and tools-AOT artifact crates, not only the root runtime/AOT crates.", + "tools/policy/check-wasix-release-dependency-invariants.mjs", + severity="P0", + ) require( findings, product, diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index c256d33f..467bcc20 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1334,6 +1334,13 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None or "text = re.sub(r'(?m)^publish = false\\n?', \"\", text)" not in wasix_packager_source ): fail("WASIX Cargo artifact packager must split pg_dump/psql into publishable tools crates while keeping only postgres/initdb in root runtime crates") + wasix_dependency_invariant_source = read_text("tools/policy/check-wasix-release-dependency-invariants.mjs") + if ( + "INTERNAL_TOOLS_MANIFEST" not in wasix_dependency_invariant_source + or "INTERNAL_TOOLS_AOT_MANIFESTS_DIR" not in wasix_dependency_invariant_source + or "oliphaunt-wasix-tools-aot-" not in wasix_dependency_invariant_source + ): + fail("WASIX release dependency invariants must cover oliphaunt-wasix-tools and tools-AOT artifact crates") native_packager_source = read_text("tools/release/package_liboliphaunt_cargo_artifacts.py") if ( optimize_native_runtime_payload.NATIVE_RUNTIME_TOOL_STEMS != ("initdb", "pg_ctl", "postgres") diff --git a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py index e7d7779c..2142cc7a 100644 --- a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py +++ b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py @@ -541,7 +541,7 @@ def patch_tools_aot_template(crate_dir: Path, target: str) -> None: text = re.sub(r'(?m)^links = "[^"]+"$', f'links = "{links}"', text, count=1) text = re.sub( r'(?m)^description = "[^"]+"$', - f'description = "Internal Wasmer AOT artifacts for oliphaunt-wasix tools on {target}"', + f'description = "Wasmer AOT pg_dump and psql artifacts for oliphaunt-wasix on {target}"', text, count=1, ) From e47be6f7d7866017b9f55522b4327360a16464bf Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 10:55:52 +0000 Subject: [PATCH 071/137] fix: tighten split tools sdk validation --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 17 +- .../js/src/__tests__/runtime-modes.test.ts | 96 +++++- src/sdks/js/src/client.ts | 2 + src/sdks/js/src/runtime/broker.ts | 41 ++- src/sdks/js/src/runtime/server.ts | 18 +- src/sdks/js/tools/check-sdk.sh | 10 +- .../rust/crates/oliphaunt-build/src/lib.rs | 313 +++++++++++++++++- tools/policy/check-sdk-parity.sh | 30 ++ 8 files changed, 507 insertions(+), 20 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 30bc5fda..96391117 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -346,5 +346,18 @@ review production pipelines, then normalize implementation details. tool, exact-extension, and explicit local override path. The parity policy documents the cross-SDK artifact-resolution matrix, and `tools/policy/check-sdk-parity.sh` fails if Rust/TypeScript split tools, - mobile direct-mode no-tools behavior, React Native delegation, or the Deno - explicit-`runtimeDirectory` extension deviation drift from that matrix. + mobile direct-mode no-tools behavior, React Native delegation, explicit local + override paths, or the Deno explicit-`runtimeDirectory` extension deviation + drift from that matrix. +- TypeScript broker/server parity is now tighter: Deno `nativeBroker` rejects + package-managed extensions without an explicit prepared `runtimeDirectory`, + broker restore passes the resolved native install environment, and + `nativeServer` preflights both split client tools (`pg_dump` and `psql`) for + explicit and package-managed tool directories. The JS SDK release-check uses + pnpm's trusted-lockfile mode for its scratch workspace so local unpublished + `@oliphaunt/*` packages do not fail npm age checks before package validation. +- `oliphaunt-build` now validates artifact manifest kind/product boundaries and + required split-tool payloads before staging Cargo-resolved artifacts. Native + tool artifacts must contain both `pg_dump` and `psql`; WASIX tool artifacts + must contain `pg_dump` and `psql` payloads and reject `pg_ctl`; WASIX + tools-AOT similarly requires `pg_dump`/`psql` AOT payloads. diff --git a/src/sdks/js/src/__tests__/runtime-modes.test.ts b/src/sdks/js/src/__tests__/runtime-modes.test.ts index f97df953..34b3fe0d 100644 --- a/src/sdks/js/src/__tests__/runtime-modes.test.ts +++ b/src/sdks/js/src/__tests__/runtime-modes.test.ts @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import { test } from 'vitest'; -import { chmod, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { delimiter, join } from 'node:path'; import { tmpdir } from 'node:os'; @@ -34,9 +34,12 @@ import { async function main(): Promise { testBrokerCapabilities(); await testBrokerSupportAndRestoreFailureAreActionable(); + await testBrokerRestorePassesNativeInstallEnv(); await testBrokerStartupTimeoutEnvIsValidatedBeforeNativeInstall(); + await testDenoBrokerModeRejectsPackageManagedExtensions(); testServerCapabilitiesAndConnectionString(); await testServerSupportReportsMissingExecutable(); + await testServerSupportRequiresSplitClientTools(); await testServerStartupTimeoutEnvIsValidatedBeforeProcessSetup(); await testServerRuntimeEnvIncludesPackagedLibraryDir(); await testDenoServerModeRejectsPackageManagedExtensions(); @@ -101,6 +104,46 @@ async function testBrokerSupportAndRestoreFailureAreActionable(): Promise } } +async function testBrokerRestorePassesNativeInstallEnv(): Promise { + const root = await mkdtemp(join(tmpdir(), 'oliphaunt-js-broker-restore-env-')); + const broker = join(root, process.platform === 'win32' ? 'broker.cmd' : 'broker'); + const capture = join(root, 'env.txt'); + const libraryPath = join(root, 'liboliphaunt.so'); + const runtimeDirectory = join(root, 'runtime'); + try { + await mkdir(runtimeDirectory, { recursive: true }); + await writeFile(libraryPath, ''); + if (process.platform === 'win32') { + await writeFile( + broker, + `@echo off\r\n> "${capture}" echo %LIBOLIPHAUNT_PATH%\r\n>> "${capture}" echo %OLIPHAUNT_INSTALL_DIR%\r\n>> "${capture}" echo %OLIPHAUNT_RUNTIME_DIR%\r\n`, + ); + } else { + await writeFile( + broker, + `#!/bin/sh\nprintf '%s\\n%s\\n%s\\n' "$LIBOLIPHAUNT_PATH" "$OLIPHAUNT_INSTALL_DIR" "$OLIPHAUNT_RUNTIME_DIR" > "${capture}"\n`, + ); + } + await chmod(broker, 0o700); + + await restorePhysicalArchiveWithBroker({ + brokerExecutable: broker, + root: join(root, 'db'), + bytes: new Uint8Array([1, 2, 3]), + libraryPath, + runtimeDirectory, + }); + + assert.deepEqual((await readFile(capture, 'utf8')).trim().split(/\r?\n/), [ + libraryPath, + runtimeDirectory, + runtimeDirectory, + ]); + } finally { + await rm(root, { recursive: true, force: true }); + } +} + async function testBrokerStartupTimeoutEnvIsValidatedBeforeNativeInstall(): Promise { const root = await mkdtemp(join(tmpdir(), 'oliphaunt-js-broker-timeout-')); const executable = join(root, process.platform === 'win32' ? 'broker.cmd' : 'broker'); @@ -131,6 +174,37 @@ async function testBrokerStartupTimeoutEnvIsValidatedBeforeNativeInstall(): Prom } } +async function testDenoBrokerModeRejectsPackageManagedExtensions(): Promise { + const root = await mkdtemp(join(tmpdir(), 'oliphaunt-js-deno-broker-extension-')); + const executable = join(root, process.platform === 'win32' ? 'broker.cmd' : 'broker'); + const previousDeno = (globalThis as { Deno?: unknown }).Deno; + try { + await writeFile(executable, process.platform === 'win32' ? '@echo off\r\n' : '#!/bin/sh\n'); + await chmod(executable, 0o700); + (globalThis as { Deno?: unknown }).Deno = {}; + const binding = createBrokerRuntimeBinding({ executable }); + await assert.rejects( + () => + Promise.resolve( + binding.open( + normalizedTestConfig(join(root, 'db'), { + engine: 'nativeBroker', + extensions: ['hstore'], + }), + ), + ), + /Deno nativeBroker does not automatically materialize extension packages/, + ); + } finally { + if (previousDeno === undefined) { + delete (globalThis as { Deno?: unknown }).Deno; + } else { + (globalThis as { Deno?: unknown }).Deno = previousDeno; + } + await rm(root, { recursive: true, force: true }); + } +} + function testServerCapabilitiesAndConnectionString(): void { const binding = createServerRuntimeBinding(); assert.equal(binding.runtime, 'node'); @@ -171,6 +245,26 @@ async function testServerSupportReportsMissingExecutable(): Promise { assert.match(support.unavailableReason ?? '', /set serverExecutable|OLIPHAUNT_POSTGRES/); } +async function testServerSupportRequiresSplitClientTools(): Promise { + const root = await mkdtemp(join(tmpdir(), 'oliphaunt-js-server-tools-')); + const bin = join(root, 'bin'); + const postgres = join(bin, process.platform === 'win32' ? 'postgres.exe' : 'postgres'); + try { + await mkdir(bin, { recursive: true }); + await writeFile(postgres, ''); + const missingPgDump = await serverModeSupport({ serverExecutable: postgres }); + assert.equal(missingPgDump.available, false); + assert.match(missingPgDump.unavailableReason ?? '', /missing pg_dump/); + + await writeFile(join(bin, process.platform === 'win32' ? 'pg_dump.exe' : 'pg_dump'), ''); + const missingPsql = await serverModeSupport({ serverExecutable: postgres }); + assert.equal(missingPsql.available, false); + assert.match(missingPsql.unavailableReason ?? '', /missing psql/); + } finally { + await rm(root, { recursive: true, force: true }); + } +} + async function testServerStartupTimeoutEnvIsValidatedBeforeProcessSetup(): Promise { const previous = process.env.OLIPHAUNT_SERVER_STARTUP_TIMEOUT_MS; try { diff --git a/src/sdks/js/src/client.ts b/src/sdks/js/src/client.ts index 37841baa..78c5a220 100644 --- a/src/sdks/js/src/client.ts +++ b/src/sdks/js/src/client.ts @@ -449,11 +449,13 @@ export function createOliphauntClient( options.brokerExecutable, 'brokerExecutable', ); + const libraryPath = validateOptionalPathOverride(options.libraryPath, 'libraryPath'); return restorePhysicalArchiveWithBroker({ root: options.root, bytes: toUint8Array(artifact.bytes), replaceExisting: options.replaceExisting, brokerExecutable, + libraryPath, }); } throw new Error('nativeServer restore is not supported by the TypeScript SDK'); diff --git a/src/sdks/js/src/runtime/broker.ts b/src/sdks/js/src/runtime/broker.ts index 2f26d31d..a6fddf76 100644 --- a/src/sdks/js/src/runtime/broker.ts +++ b/src/sdks/js/src/runtime/broker.ts @@ -54,6 +54,8 @@ export type BrokerRestoreOptions = { bytes: Uint8Array; replaceExisting?: boolean; brokerExecutable?: string; + libraryPath?: string; + runtimeDirectory?: string; }; export function createBrokerRuntimeBinding( @@ -136,6 +138,10 @@ export async function restorePhysicalArchiveWithBroker( options: BrokerRestoreOptions, ): Promise { const executable = await resolveBrokerExecutable(options.brokerExecutable); + const nativeInstall = await resolveBrokerNativeInstall({ + libraryPath: options.libraryPath, + runtimeDirectory: options.runtimeDirectory, + }); const tempDir = await createTempDir('lpgr-'); const artifactPath = join(tempDir, 'physical-archive.tar'); try { @@ -144,7 +150,13 @@ export async function restorePhysicalArchiveWithBroker( if (options.replaceExisting === true) { args.push('--replace-existing'); } - await runBrokerTool(executable, args, RESTORE_TIMEOUT_MS, 'native broker restore'); + await runBrokerTool( + executable, + args, + RESTORE_TIMEOUT_MS, + 'native broker restore', + brokerNativeInstallEnv(nativeInstall), + ); return options.root; } finally { await removeTree(tempDir); @@ -395,10 +407,25 @@ async function resolveBrokerNativeInstall(config: { runtimeDirectory?: string; extensions?: readonly string[]; }): Promise { + const extensions = config.extensions ?? []; if (runtimeName() === 'deno') { + if (extensions.length > 0 && config.runtimeDirectory === undefined) { + throw new Error( + `Deno nativeBroker does not automatically materialize extension packages; pass runtimeDirectory with the selected extension assets or use Node/Bun nativeBroker. Selected extensions: ${extensions.join(', ')}`, + ); + } const install = await import('../native/assets-deno.js').then((module) => module.resolveDenoNativeInstall(config.libraryPath), ); + if ( + extensions.length > 0 && + install.packageManaged && + config.runtimeDirectory === install.runtimeDirectory + ) { + throw new Error( + `Deno nativeBroker does not automatically materialize extension packages; pass runtimeDirectory with the selected extension assets or use Node/Bun nativeBroker. Selected extensions: ${extensions.join(', ')}`, + ); + } return { libraryPath: install.libraryPath, runtimeDirectory: config.runtimeDirectory ?? install.runtimeDirectory, @@ -413,15 +440,21 @@ async function resolveBrokerNativeInstall(config: { runtimeDirectory: config.runtimeDirectory ?? install.runtimeDirectory, icuDataDirectory: install.icuDataDirectory, }; - return assets.materializeNodeExtensionInstall(resolved, config.extensions ?? []); + return assets.materializeNodeExtensionInstall(resolved, extensions); } function brokerSpawnEnv( authToken: string, nativeInstall: BrokerNativeInstall, ): Record { - const env: Record = { + return { OLIPHAUNT_BROKER_AUTH_TOKEN: authToken, + ...brokerNativeInstallEnv(nativeInstall), + }; +} + +function brokerNativeInstallEnv(nativeInstall: BrokerNativeInstall): Record { + const env: Record = { [LIBOLIPHAUNT_PATH_ENV]: nativeInstall.libraryPath, }; if (nativeInstall.runtimeDirectory !== undefined) { @@ -549,9 +582,11 @@ async function runBrokerTool( args: string[], timeoutMs: number, label: string, + env: Record = {}, ): Promise { await new Promise((resolve, reject) => { const child = spawn(executable, args, { + env: { ...process.env, ...env }, stdio: ['ignore', 'pipe', 'pipe'], }); const stdout: Buffer[] = []; diff --git a/src/sdks/js/src/runtime/server.ts b/src/sdks/js/src/runtime/server.ts index 345648f7..7a8fd53b 100644 --- a/src/sdks/js/src/runtime/server.ts +++ b/src/sdks/js/src/runtime/server.ts @@ -392,9 +392,11 @@ async function resolveServerTools(options: { ].filter((value): value is string => value !== undefined && value.length > 0); for (const candidate of candidates) { if (await isFile(candidate)) { + const toolDirectory = options.serverToolDirectory ?? dirname(candidate); + await requireServerClientTools(toolDirectory); return { executable: candidate, - toolDirectory: options.serverToolDirectory ?? dirname(candidate), + toolDirectory, }; } } @@ -406,6 +408,7 @@ async function resolveServerTools(options: { const toolDirectory = join(install.runtimeDirectory, 'bin'); const executable = join(toolDirectory, executableName('postgres')); if (await isFile(executable)) { + await requireServerClientTools(toolDirectory); return { executable, toolDirectory, icuDataDirectory: install.icuDataDirectory }; } } @@ -446,6 +449,19 @@ async function optionalTool( return (await isFile(path)) ? path : undefined; } +async function requireServerClientTools(toolDirectory: string): Promise { + await requireTool(toolDirectory, 'pg_dump'); + await requireTool(toolDirectory, 'psql'); +} + +async function requireTool(toolDirectory: string, name: string): Promise { + const path = join(toolDirectory, executableName(name)); + if (!(await isFile(path))) { + throw new Error(`native server tool directory is missing ${executableName(name)} at ${path}`); + } + return path; +} + function executableName(name: string): string { return hostPlatform() === 'win32' ? `${name}.exe` : name; } diff --git a/src/sdks/js/tools/check-sdk.sh b/src/sdks/js/tools/check-sdk.sh index c012afc5..b45f86a4 100755 --- a/src/sdks/js/tools/check-sdk.sh +++ b/src/sdks/js/tools/check-sdk.sh @@ -112,7 +112,7 @@ YAML --exclude lib \ "$source_package_dir/" "$package_dir/" rm -rf "$scratch_root/node_modules" "$package_dir/node_modules" - run pnpm --dir "$scratch_root" install --frozen-lockfile + run pnpm --dir "$scratch_root" install --frozen-lockfile --trust-lockfile if [ ! -e "$package_dir/node_modules" ]; then ln -s "$scratch_root/node_modules" "$package_dir/node_modules" fi @@ -400,6 +400,14 @@ require_source_text "$package_dir/src/runtime/server.ts" "resolveDenoNativeInsta "TypeScript Deno nativeServer must resolve package-managed server tools through the Deno native resolver" require_source_text "$package_dir/src/runtime/server.ts" "Deno nativeServer does not automatically materialize extension packages" \ "TypeScript Deno nativeServer must fail clearly for registry-managed extension materialization" +require_source_text "$package_dir/src/runtime/broker.ts" "Deno nativeBroker does not automatically materialize extension packages" \ + "TypeScript Deno nativeBroker must fail clearly for registry-managed extension materialization" +require_source_text "$package_dir/src/runtime/broker.ts" "brokerNativeInstallEnv(nativeInstall)" \ + "TypeScript nativeBroker restore must pass the resolved native install environment" +require_source_text "$package_dir/src/runtime/server.ts" "requireServerClientTools" \ + "TypeScript nativeServer must preflight split client tools" +require_source_text "$package_dir/src/runtime/server.ts" "requireTool(toolDirectory, 'psql')" \ + "TypeScript nativeServer must validate psql alongside pg_dump" require_source_text "$package_dir/src/native/tar.ts" "extractTarArchive" \ "TypeScript SDK must extract verified liboliphaunt release assets without shelling out" require_source_text "$package_dir/src/client.ts" "supportedModes(options: SupportedModesOptions = {}): Promise" \ diff --git a/src/sdks/rust/crates/oliphaunt-build/src/lib.rs b/src/sdks/rust/crates/oliphaunt-build/src/lib.rs index 8065fea3..5dedfb6f 100644 --- a/src/sdks/rust/crates/oliphaunt-build/src/lib.rs +++ b/src/sdks/rust/crates/oliphaunt-build/src/lib.rs @@ -594,6 +594,8 @@ impl ArtifactManifest { self.label() ))); } + self.validate_product_kind()?; + self.validate_payload()?; Ok(()) } @@ -603,6 +605,165 @@ impl ArtifactManifest { .map(|path| path.display().to_string()) .unwrap_or_else(|| format!("{} {} {}", self.product, self.kind.as_str(), self.target)) } + + fn validate_product_kind(&self) -> Result<()> { + let expected = match self.kind { + ArtifactKind::NativeRuntime => Some("liboliphaunt-native"), + ArtifactKind::NativeTools => Some("oliphaunt-tools"), + ArtifactKind::WasixRuntime | ArtifactKind::WasixAot => Some("liboliphaunt-wasix"), + ArtifactKind::WasixTools | ArtifactKind::WasixToolsAot => Some("oliphaunt-wasix-tools"), + ArtifactKind::BrokerHelper => Some("oliphaunt-broker"), + ArtifactKind::IcuData => Some("oliphaunt-icu"), + ArtifactKind::Extension => None, + }; + if let Some(expected) = expected { + if self.product != expected { + return Err(Error::new(format!( + "{} kind {} must use product {expected:?}", + self.label(), + self.kind.as_str() + ))); + } + } else if !self.product.starts_with("oliphaunt-extension-") { + return Err(Error::new(format!( + "{} extension artifact product must start with \"oliphaunt-extension-\"", + self.label() + ))); + } + Ok(()) + } + + fn validate_payload(&self) -> Result<()> { + let relatives: BTreeSet<&str> = self + .files + .iter() + .map(|file| file.relative.as_str()) + .collect(); + match self.kind { + ArtifactKind::NativeRuntime => { + self.require_files( + &relatives, + &[ + "runtime/bin/postgres", + "runtime/bin/initdb", + "runtime/bin/pg_ctl", + ], + )?; + self.reject_files( + &relatives, + &[ + "runtime/bin/pg_dump", + "runtime/bin/psql", + "runtime/bin/pg_dump.exe", + "runtime/bin/psql.exe", + ], + )?; + } + ArtifactKind::NativeTools => { + self.require_files(&relatives, &["runtime/bin/pg_dump", "runtime/bin/psql"])?; + self.reject_files( + &relatives, + &[ + "runtime/bin/postgres", + "runtime/bin/initdb", + "runtime/bin/pg_ctl", + "runtime/bin/postgres.exe", + "runtime/bin/initdb.exe", + "runtime/bin/pg_ctl.exe", + ], + )?; + } + ArtifactKind::WasixRuntime => { + self.require_files( + &relatives, + &["oliphaunt.wasix.tar.zst", "bin/initdb.wasix.wasm"], + )?; + self.reject_files( + &relatives, + &[ + "bin/pg_ctl.wasix.wasm", + "bin/pg_dump.wasix.wasm", + "bin/psql.wasix.wasm", + ], + )?; + } + ArtifactKind::WasixTools => { + self.require_files( + &relatives, + &["bin/pg_dump.wasix.wasm", "bin/psql.wasix.wasm"], + )?; + self.reject_files( + &relatives, + &[ + "bin/postgres.wasix.wasm", + "bin/initdb.wasix.wasm", + "bin/pg_ctl.wasix.wasm", + ], + )?; + } + ArtifactKind::WasixToolsAot => { + self.require_files( + &relatives, + &["pg_dump-llvm-opta.bin.zst", "psql-llvm-opta.bin.zst"], + )?; + self.reject_files( + &relatives, + &[ + "postgres-llvm-opta.bin.zst", + "initdb-llvm-opta.bin.zst", + "pg_ctl-llvm-opta.bin.zst", + ], + )?; + } + ArtifactKind::WasixAot => { + self.require_files(&relatives, &["manifest.json"])?; + self.reject_files( + &relatives, + &[ + "pg_ctl-llvm-opta.bin.zst", + "pg_dump-llvm-opta.bin.zst", + "psql-llvm-opta.bin.zst", + ], + )?; + } + ArtifactKind::BrokerHelper | ArtifactKind::IcuData | ArtifactKind::Extension => {} + } + Ok(()) + } + + fn require_files(&self, relatives: &BTreeSet<&str>, required: &[&str]) -> Result<()> { + for relative in required { + if !relatives.contains(relative) && !windows_tool_variant_present(relatives, relative) { + return Err(Error::new(format!( + "{} {} artifact is missing required payload {relative:?}", + self.label(), + self.kind.as_str() + ))); + } + } + Ok(()) + } + + fn reject_files(&self, relatives: &BTreeSet<&str>, rejected: &[&str]) -> Result<()> { + for relative in rejected { + if relatives.contains(relative) { + return Err(Error::new(format!( + "{} {} artifact must not contain payload {relative:?}", + self.label(), + self.kind.as_str() + ))); + } + } + Ok(()) + } +} + +fn windows_tool_variant_present(relatives: &BTreeSet<&str>, relative: &str) -> bool { + if !relative.starts_with("runtime/bin/") || relative.ends_with(".exe") { + return false; + } + let windows_relative = format!("{relative}.exe"); + relatives.contains(windows_relative.as_str()) } #[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)] @@ -1173,6 +1334,66 @@ runtime-version = "0.1.0" ); } + #[test] + fn artifact_manifest_rejects_incomplete_native_tools_payload() { + let temp = app_with_metadata(""); + let tools_manifest = write_artifact_manifest_with_relatives( + &temp, + "tools.toml", + "oliphaunt-tools", + "0.1.0", + "native-tools", + "x86_64-unknown-linux-gnu", + None, + &["runtime/bin/pg_dump"], + ); + let context = BuildContext { + manifest_dir: temp.path().to_path_buf(), + out_dir: temp.path().join("out"), + target: "x86_64-unknown-linux-gnu".to_owned(), + artifact_manifest_paths: vec![tools_manifest], + }; + + let error = context + .read_artifact_manifests() + .expect_err("native tools without psql must fail validation"); + + assert!(error.to_string().contains("missing required payload")); + assert!(error.to_string().contains("runtime/bin/psql")); + } + + #[test] + fn artifact_manifest_rejects_wasix_pg_ctl_tool_payload() { + let temp = app_with_metadata(""); + let tools_manifest = write_artifact_manifest_with_relatives( + &temp, + "wasix-tools.toml", + "oliphaunt-wasix-tools", + "0.1.0", + "wasix-tools", + "portable", + None, + &[ + "bin/pg_dump.wasix.wasm", + "bin/psql.wasix.wasm", + "bin/pg_ctl.wasix.wasm", + ], + ); + let context = BuildContext { + manifest_dir: temp.path().to_path_buf(), + out_dir: temp.path().join("out"), + target: "wasm32-wasip1".to_owned(), + artifact_manifest_paths: vec![tools_manifest], + }; + + let error = context + .read_artifact_manifests() + .expect_err("WASIX tools must not contain pg_ctl"); + + assert!(error.to_string().contains("must not contain payload")); + assert!(error.to_string().contains("bin/pg_ctl.wasix.wasm")); + } + fn app_with_metadata(metadata: &str) -> TempDir { let temp = TempDir::new().unwrap(); let manifest = format!( @@ -1197,35 +1418,103 @@ edition = "2024" extension: Option<&str>, relative: &str, ) -> PathBuf { - let source = temp - .path() - .join("artifacts") - .join(manifest_name.replace(".toml", ".bin")); - fs::create_dir_all(source.parent().unwrap()).unwrap(); - let mut file = fs::File::create(&source).unwrap(); - write!(file, "{product}:{kind}:{target}").unwrap(); - let bytes = fs::read(&source).unwrap(); - let sha256 = sha256_hex(&bytes); + let relatives = test_artifact_relatives(kind, relative); + let relative_refs: Vec<&str> = relatives.iter().map(String::as_str).collect(); + write_artifact_manifest_with_relatives( + temp, + manifest_name, + product, + version, + kind, + target, + extension, + &relative_refs, + ) + } + + fn write_artifact_manifest_with_relatives( + temp: &TempDir, + manifest_name: &str, + product: &str, + version: &str, + kind: &str, + target: &str, + extension: Option<&str>, + relatives: &[&str], + ) -> PathBuf { let extension_line = extension .map(|value| format!("extension = {value:?}\n")) .unwrap_or_default(); - let manifest = format!( + let mut manifest = format!( r#"schema = "oliphaunt-artifact-manifest-v1" product = {product:?} version = {version:?} kind = {kind:?} target = {target:?} {extension_line} +"#, + ); + let source_root = temp.path().join("artifacts").join(manifest_name); + for relative in relatives { + let source = source_root.join(relative.replace(['/', '\\'], "_")); + fs::create_dir_all(source.parent().unwrap()).unwrap(); + let mut file = fs::File::create(&source).unwrap(); + write!(file, "{product}:{kind}:{target}:{relative}").unwrap(); + let bytes = fs::read(&source).unwrap(); + let sha256 = sha256_hex(&bytes); + manifest.push_str(&format!( + r#" [[files]] source = "{}" relative = {relative:?} sha256 = {sha256:?} executable = true "#, - source.display(), - ); + source.display(), + )); + } let path = temp.path().join(manifest_name); fs::write(&path, manifest).unwrap(); path } + + fn test_artifact_relatives(kind: &str, primary: &str) -> Vec { + let mut relatives = match kind { + "native-runtime" => vec![ + "runtime/bin/postgres".to_owned(), + "runtime/bin/initdb".to_owned(), + "runtime/bin/pg_ctl".to_owned(), + ], + "native-tools" => vec![ + "runtime/bin/pg_dump".to_owned(), + "runtime/bin/psql".to_owned(), + ], + "wasix-runtime" => vec![ + "manifest.json".to_owned(), + "oliphaunt.wasix.tar.zst".to_owned(), + "prepopulated/pgdata-template.tar.zst".to_owned(), + "prepopulated/pgdata-template.json".to_owned(), + "bin/initdb.wasix.wasm".to_owned(), + ], + "wasix-tools" => vec![ + "bin/pg_dump.wasix.wasm".to_owned(), + "bin/psql.wasix.wasm".to_owned(), + ], + "wasix-aot" => vec![ + "manifest.json".to_owned(), + "oliphaunt-llvm-opta.bin.zst".to_owned(), + "initdb-llvm-opta.bin.zst".to_owned(), + ], + "wasix-tools-aot" => vec![ + "manifest.json".to_owned(), + "pg_dump-llvm-opta.bin.zst".to_owned(), + "psql-llvm-opta.bin.zst".to_owned(), + ], + _ => vec![primary.to_owned()], + }; + if !relatives.iter().any(|relative| relative == primary) { + relatives.push(primary.to_owned()); + } + relatives + } } diff --git a/tools/policy/check-sdk-parity.sh b/tools/policy/check-sdk-parity.sh index 5cad804d..05a66589 100755 --- a/tools/policy/check-sdk-parity.sh +++ b/tools/policy/check-sdk-parity.sh @@ -114,6 +114,12 @@ require_manifest_text rust 'tool_resolution = "split-oliphaunt-tools-cargo-crate "SDK manifest must declare Rust split oliphaunt-tools Cargo resolution" require_manifest_text rust 'extension_resolution = "exact-extension-cargo-crates"' \ "SDK manifest must declare Rust exact-extension Cargo resolution" +require_manifest_text rust 'resource_override = "OLIPHAUNT_RESOURCES_DIR"' \ + "SDK manifest must declare Rust's explicit local runtime-resource override" +require_text src/sdks/rust/crates/oliphaunt-build/src/lib.rs "runtime/bin/psql" \ + "Rust oliphaunt-build must validate psql in split native-tools artifact manifests" +require_text src/sdks/rust/crates/oliphaunt-build/src/lib.rs "bin/pg_ctl.wasix.wasm" \ + "Rust oliphaunt-build must reject pg_ctl from split WASIX tools artifact manifests" require_manifest_text swift 'classification = "sdk"' \ "SDK manifest must classify Swift as a product SDK" require_manifest_text swift 'primary_targets = ["ios", "macos"]' \ @@ -130,6 +136,8 @@ require_manifest_text swift 'tool_resolution = "not-applicable-mobile-native-dir "SDK manifest must declare that Swift mobile native-direct does not expose standalone PostgreSQL tools" require_manifest_text swift 'extension_resolution = "exact-extension-xcframework-artifacts"' \ "SDK manifest must declare Swift exact-extension XCFramework resolution" +require_manifest_text swift 'resource_override = "runtimeDirectory-resourceRoot"' \ + "SDK manifest must declare Swift's explicit local runtime-resource overrides" require_manifest_text kotlin 'classification = "sdk"' \ "SDK manifest must classify Kotlin as a product SDK" require_manifest_text kotlin 'primary_targets = ["android"]' \ @@ -146,6 +154,8 @@ require_manifest_text kotlin 'tool_resolution = "not-applicable-mobile-native-di "SDK manifest must declare that Kotlin Android native-direct does not expose standalone PostgreSQL tools" require_manifest_text kotlin 'extension_resolution = "exact-extension-maven-artifacts"' \ "SDK manifest must declare Kotlin exact-extension Maven resolution" +require_manifest_text kotlin 'resource_override = "runtimeDirectory-resourceRoot"' \ + "SDK manifest must declare Kotlin's explicit local runtime-resource overrides" require_manifest_text react-native 'classification = "sdk"' \ "SDK manifest must classify React Native as an SDK" require_manifest_text react-native 'runtime_owner = false' \ @@ -164,6 +174,8 @@ require_manifest_text react-native 'tool_resolution = "delegated-platform-sdk"' "SDK manifest must declare React Native delegated tool behavior" require_manifest_text react-native 'extension_resolution = "delegated-exact-extension-artifacts"' \ "SDK manifest must declare React Native delegated exact-extension resolution" +require_manifest_text react-native 'resource_override = "runtimeDirectory-resourceRoot"' \ + "SDK manifest must declare React Native's delegated local runtime-resource overrides" require_manifest_text typescript 'classification = "sdk"' \ "SDK manifest must classify TypeScript as an SDK" require_manifest_text typescript 'package_name = "@oliphaunt/ts"' \ @@ -180,6 +192,8 @@ require_manifest_text typescript 'tool_resolution = "split-oliphaunt-tools-npm-p "SDK manifest must declare TypeScript split oliphaunt-tools npm resolution" require_manifest_text typescript 'extension_resolution = "node-bun-exact-extension-npm-packages-deno-explicit-runtimeDirectory"' \ "SDK manifest must declare TypeScript Node/Bun registry extension resolution and Deno's explicit-runtimeDirectory gap" +require_manifest_text typescript 'resource_override = "libraryPath-runtimeDirectory"' \ + "SDK manifest must declare TypeScript's explicit local native override paths" require_text src/sdks/js/src/native/assets-deno.ts "target.toolsPackageName" \ "TypeScript Deno native resolver must consume the split oliphaunt-tools package" require_text src/sdks/js/src/native/assets-deno.ts "materializeDenoToolsRuntime" \ @@ -192,6 +206,14 @@ require_text src/sdks/js/src/runtime/server.ts "resolveDenoNativeInstall" \ "TypeScript Deno nativeServer must resolve package-managed server tools through the Deno native resolver" require_text src/sdks/js/src/runtime/server.ts "Deno nativeServer does not automatically materialize extension packages" \ "TypeScript Deno nativeServer must fail clearly for registry-managed extension materialization" +require_text src/sdks/js/src/runtime/broker.ts "Deno nativeBroker does not automatically materialize extension packages" \ + "TypeScript Deno nativeBroker must fail clearly for registry-managed extension materialization" +require_text src/sdks/js/src/runtime/broker.ts "brokerNativeInstallEnv(nativeInstall)" \ + "TypeScript nativeBroker restore must pass the same resolved native install environment used by broker open" +require_text src/sdks/js/src/runtime/server.ts "requireServerClientTools" \ + "TypeScript nativeServer startup must preflight split client tools for explicit and package-managed installs" +require_text src/sdks/js/src/runtime/server.ts "requireTool(toolDirectory, 'psql')" \ + "TypeScript nativeServer startup must validate psql alongside pg_dump" require_text docs/maintainers/sdk-products-policy.md "These are product SDKs, not auxiliary bindings." \ "SDK maintainer policy must frame Rust/Swift/Kotlin/RN as product SDKs" require_text docs/maintainers/sdk-products-policy.md '`tools/policy/sdk-manifest.toml` is the repo-level SDK registry kept for' \ @@ -280,12 +302,20 @@ require_text docs/maintainers/sdk-parity-policy.md "React Native is not a fifth "SDK parity docs must forbid an independent React Native runtime" require_text docs/maintainers/sdk-parity-policy.md "## Artifact Resolution" \ "SDK parity docs must include the artifact-resolution contract" +require_text docs/maintainers/sdk-parity-policy.md "Explicit local override" \ + "SDK parity docs must include explicit local override paths in the artifact-resolution matrix" require_text docs/maintainers/sdk-parity-policy.md "split \`oliphaunt-tools-*\` Cargo artifact crates copied into the runtime cache" \ "SDK parity docs must describe Rust split tools Cargo artifact resolution" +require_text docs/maintainers/sdk-parity-policy.md "\`OLIPHAUNT_RESOURCES_DIR\`" \ + "SDK parity docs must document Rust's explicit local runtime-resource override" require_text docs/maintainers/sdk-parity-policy.md "split \`@oliphaunt/tools-*\` npm packages" \ "SDK parity docs must describe TypeScript split tools npm resolution" +require_text docs/maintainers/sdk-parity-policy.md "\`libraryPath\` and \`runtimeDirectory\`" \ + "SDK parity docs must document TypeScript's explicit local native override paths" require_text docs/maintainers/sdk-parity-policy.md "Deno requires an explicit prepared \`runtimeDirectory\` for extension materialization" \ "SDK parity docs must document the Deno extension-resolution deviation" +require_text docs/maintainers/sdk-parity-policy.md "\`runtimeDirectory\` or \`resourceRoot\`" \ + "SDK parity docs must document mobile SDK explicit local runtime-resource overrides" require_text docs/maintainers/sdk-parity-policy.md "### Desktop TypeScript Deltas" \ "SDK parity docs must describe desktop TypeScript deltas explicitly" require_text docs/maintainers/sdk-parity-policy.md "The default open profile is \`runtimeFootprint: 'throughput'\` with" \ From 674b6829b8857b335fbd4ef3265b4d4999e4b369 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 11:00:06 +0000 Subject: [PATCH 072/137] docs: record package surface verification --- docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 96391117..66185128 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -12,7 +12,7 @@ review production pipelines, then normalize implementation details. - [x] Confirm native and WASIX examples resolve local published runtime, tools, and extension crates with locked installs. - [x] Add direct `psql` execution coverage when the WASIX SDK exposes a public tool runner for it. - [x] Run GUI-level e2e for Electron and Tauri examples, or document the exact missing host capabilities if a full GUI run is blocked. -- [ ] Fix the CI/release metadata gaps found by the package-surface audit, then verify CI and release workflows produce exactly the package surfaces expected for each registry. +- [x] Fix the CI/release metadata gaps found by the package-surface audit, then verify CI and release workflows produce exactly the package surfaces expected for each registry. ## Priority 1: Example App Validation @@ -361,3 +361,11 @@ review production pipelines, then normalize implementation details. tool artifacts must contain both `pg_dump` and `psql`; WASIX tool artifacts must contain `pg_dump` and `psql` payloads and reject `pg_ctl`; WASIX tools-AOT similarly requires `pg_dump`/`psql` AOT payloads. +- On 2026-06-26, the current branch passed the package-surface verification + gates for the P0 CI/release metadata item: `check_release_metadata.py`, + `check_consumer_shape.py`, `check_artifact_targets.py`, + `check-release-policy.py`, `check-workflows.sh`, and + `check-wasix-release-dependency-invariants.mjs`. Together these prove the + release metadata, consumer package shapes, workflow wiring, artifact target + derivation, and WASIX registry dependency graph are aligned with the intended + Cargo, npm, Maven, SwiftPM, and GitHub release surfaces. From d06c90958fc667d4fb6d6432c8fce3891bbccb8d Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 11:09:39 +0000 Subject: [PATCH 073/137] fix: align react native open mode surface --- .../content/sdk/react-native/api-reference.md | 2 +- .../content/sdk/react-native/architecture.mdx | 9 ++- src/docs/content/sdk/react-native/guide.mdx | 3 +- src/docs/content/sdk/react-native/index.mdx | 5 +- src/sdks/react-native/README.md | 11 ++- .../react-native/src/__tests__/client.test.ts | 75 +++++++++++++------ src/sdks/react-native/src/client.ts | 16 +++- tools/policy/check-sdk-parity.sh | 10 +++ 8 files changed, 91 insertions(+), 40 deletions(-) diff --git a/src/docs/content/sdk/react-native/api-reference.md b/src/docs/content/sdk/react-native/api-reference.md index ae89f79a..718447e8 100644 --- a/src/docs/content/sdk/react-native/api-reference.md +++ b/src/docs/content/sdk/react-native/api-reference.md @@ -10,7 +10,7 @@ SDK by task. | Area | Public surface | Use it for | | --- | --- | --- | -| Opening | `Oliphaunt.open`, `OpenConfig` | Open a database from TypeScript with root, mode, durability, and selected extensions | +| Opening | `Oliphaunt.open`, `OpenConfig` | Open a `nativeDirect` database from TypeScript with root, durability, and selected extensions | | Config plugin | Expo plugin options | Include the selected native runtime and exact extension artifacts in iOS and Android builds | | Platform support | `supportedModes()`, `capabilities()` | Read what the installed Swift or Kotlin runtime can actually do | | Database handle | `OliphauntDatabase` | Keep the opened database in app state and route calls through one native handle | diff --git a/src/docs/content/sdk/react-native/architecture.mdx b/src/docs/content/sdk/react-native/architecture.mdx index 16f84cab..37049a99 100644 --- a/src/docs/content/sdk/react-native/architecture.mdx +++ b/src/docs/content/sdk/react-native/architecture.mdx @@ -91,7 +91,8 @@ An app that selects only `vector` ships `vector` and its declared dependencies. Mobile direct mode uses one resident backend per app process and one physical session. It is same-root logically reopenable inside that process. Broker and -server modes add a process boundary on targets that advertise those modes. +server entries can appear in `supportedModes()` on targets that advertise those +capabilities, but `OpenConfig.engine` currently accepts `nativeDirect` only. Use the React Native lifecycle helpers around background and foreground transitions. They delegate to Swift or Kotlin so platform storage and lifecycle @@ -114,9 +115,9 @@ Capabilities report: - process and root behavior; - whether broker or server mode is available. -Mode requests outside advertised capabilities fail with clear errors. Direct -mode remains one physical session; use a server-capable platform runtime when an -app needs independent PostgreSQL client sessions. +Mode requests outside the React Native bridge's open surface fail with clear +errors. Direct mode remains one physical session; use a server-capable platform +runtime when an app needs independent PostgreSQL client sessions. `Oliphaunt.restore({ libraryPath, ... })` forwards the same native library override that the platform SDKs use, so restore follows the selected native diff --git a/src/docs/content/sdk/react-native/guide.mdx b/src/docs/content/sdk/react-native/guide.mdx index 7f52f4af..a333adef 100644 --- a/src/docs/content/sdk/react-native/guide.mdx +++ b/src/docs/content/sdk/react-native/guide.mdx @@ -141,7 +141,8 @@ mode, durability, and extension activation for that app run. React Native starts with `nativeDirect` on mobile. The database work is delegated to Swift on Apple platforms and Kotlin on Android, so -`capabilities()` is the source of truth for additional broker or server modes. +`capabilities()` is the source of truth for additional broker or server mode +reports. `OpenConfig.engine` currently accepts `nativeDirect` only. diff --git a/src/docs/content/sdk/react-native/index.mdx b/src/docs/content/sdk/react-native/index.mdx index 2158f6dc..3539d801 100644 --- a/src/docs/content/sdk/react-native/index.mdx +++ b/src/docs/content/sdk/react-native/index.mdx @@ -76,8 +76,9 @@ Apple calls flow through Swift; Android calls flow through Kotlin. Direct mobile mode owns one resident backend per app process and one serialized physical PostgreSQL session. Multiple JavaScript calls can share a handle and -are queued through the platform SDK. Broker and server mode become available -when the platform SDK advertises them through `capabilities()`. +are queued through the platform SDK. `OpenConfig.engine` currently accepts +`nativeDirect` only; broker and server mode entries in capability reports are +discovery signals until the React Native bridge exposes those open paths. ## App Responsibilities diff --git a/src/sdks/react-native/README.md b/src/sdks/react-native/README.md index f628c39c..0b3137b2 100644 --- a/src/sdks/react-native/README.md +++ b/src/sdks/react-native/README.md @@ -135,17 +135,16 @@ handle until commit or rollback. `OliphauntDatabase.checkpoint()` requests a PostgreSQL checkpoint through the same delegated platform SDK session and is rejected while a transaction is active. Call `Oliphaunt.supportedModes()` before opening to discover the platform adapter's -actual direct/broker/server availability. React Native reports the same +actual direct/broker/server capability report. React Native reports the same canonical capability shape as Swift/Kotlin and carries explicit reasons for -unavailable modes instead of attempting direct-mode aliases. +unavailable modes instead of attempting direct-mode aliases. `OpenConfig.engine` +currently accepts `nativeDirect` only; broker/server entries are discovery +signals until the React Native bridge exposes those open paths. Lifecycle capability fields are forwarded from the platform SDK: `sameRootLogicalReopen`, `rootSwitchable`, and `crashRestartable` distinguish direct's same-root resident reopen from broker/server process-managed behavior. Native direct is not root-switchable or crash-restartable. Mobile direct mode -has one resident backend per app process and one physical session. Use server -mode only where the SDK reports true server support; it is not a -crash-isolated server and it does not provide independent concurrent client -sessions. +has one resident backend per app process and one physical session. `Oliphaunt.open({ username, database })` forwards startup identity to the Swift or Kotlin SDK and rejects empty or NUL-containing values before the TurboModule call. diff --git a/src/sdks/react-native/src/__tests__/client.test.ts b/src/sdks/react-native/src/__tests__/client.test.ts index 2374dd42..712251a6 100644 --- a/src/sdks/react-native/src/__tests__/client.test.ts +++ b/src/sdks/react-native/src/__tests__/client.test.ts @@ -7,6 +7,7 @@ import { createOliphauntClient, supportsBackupFormat, supportsRestoreFormat, + type OpenConfig, type OliphauntTransaction, } from '../client'; import { simpleQuery } from '../protocol'; @@ -18,6 +19,7 @@ import type { NativeCapabilities, Spec } from '../specs/NativeOliphaunt'; async function main(): Promise { await testPackageEntrypointWiresDefaultTurboModuleClient(); await testSupportedModesExposePlatformRuntimeContract(); + testOpenConfigTypeSurface(); await testPackageSizeReportDelegatesToNativeSdk(); await testPackageSizeReportRejectsBlankResourceRootBeforeNativeCall(); await testProcessMemoryReportDelegatesToNativeSdk(); @@ -28,6 +30,7 @@ async function main(): Promise { await testJsiStreamTransportRejectsNonBinaryChunks(); await testJsiStreamTransportPropagatesChunkCallbackErrors(); await testOpenRequiresJsiTransportBeforeNativeCall(); + await testOpenRejectsBrokerServerBeforeNativeCall(); await testJsiArrayBufferTransportRejectsNonBinaryResponses(); await testReusableReactNativeSmokeRunnerExercisesInstalledTransportShape(); await testReusableReactNativeBenchmarkRunnerExercisesInstalledTransportShape(); @@ -134,6 +137,19 @@ async function testSupportedModesExposePlatformRuntimeContract(): Promise assert.match(support[2]?.unavailableReason ?? '', /server/); } +function testOpenConfigTypeSurface(): void { + const direct = { engine: 'nativeDirect' } satisfies OpenConfig; + assert.equal(direct.engine, 'nativeDirect'); + + // @ts-expect-error React Native open currently supports nativeDirect only. + const broker = { engine: 'nativeBroker' } satisfies OpenConfig; + void broker; + + // @ts-expect-error React Native open currently supports nativeDirect only. + const server = { engine: 'nativeServer' } satisfies OpenConfig; + void server; +} + async function testPackageSizeReportDelegatesToNativeSdk(): Promise { const native = new MockNative(); const client = createOliphauntClient(native); @@ -226,10 +242,10 @@ function sharedFixturePath(relativePath: string): string | undefined { } async function testOpenExecCapabilitiesAndClose(): Promise { - const native = new MockNative(); + const native = new DirectCapabilitiesNative(); const client = createOliphauntClient(native); const db = await client.open({ - engine: 'nativeServer', + engine: 'nativeDirect', temporary: true, durability: 'balanced', extensions: ['hstore'], @@ -237,7 +253,7 @@ async function testOpenExecCapabilitiesAndClose(): Promise { assert.equal(db.handle, 1); assert.deepEqual(native.openCalls[0], { - engine: 'nativeServer', + engine: 'nativeDirect', root: undefined, temporary: true, durability: 'balanced', @@ -251,22 +267,22 @@ async function testOpenExecCapabilitiesAndClose(): Promise { resourceRoot: undefined, }); const capabilities = await db.capabilities(); - assert.equal(capabilities.engine, 'nativeServer'); + assert.equal(capabilities.engine, 'nativeDirect'); assert.equal(capabilities.rawProtocolTransport, 'jsi-array-buffer'); assert.equal(capabilities.multiRoot, false); assert.equal(capabilities.queryCancel, true); assert.equal(capabilities.backupRestore, true); - assert.deepEqual(capabilities.backupFormats, ['sql', 'physicalArchive']); + assert.deepEqual(capabilities.backupFormats, ['physicalArchive']); assert.deepEqual(capabilities.restoreFormats, ['physicalArchive']); - assert.equal(supportsBackupFormat(capabilities, 'sql'), true); + assert.equal(supportsBackupFormat(capabilities, 'sql'), false); assert.equal(supportsBackupFormat(capabilities, 'physicalArchive'), true); assert.equal(supportsBackupFormat(capabilities, 'oliphauntArchive'), false); assert.equal(supportsRestoreFormat(capabilities, 'physicalArchive'), true); assert.equal(supportsRestoreFormat(capabilities, 'sql'), false); - assert.equal(await db.supportsBackupFormat('sql'), true); + assert.equal(await db.supportsBackupFormat('sql'), false); assert.equal(await db.supportsRestoreFormat('sql'), false); assert.equal(capabilities.simpleQuery, true); - assert.equal(capabilities.connectionString, 'postgres://postgres@127.0.0.1:55432/template1'); + assert.equal(capabilities.connectionString, undefined); const response = await db.execProtocolRaw(Uint8Array.from([0x51])); assert.deepEqual(Array.from(response), [1, 0x51]); @@ -276,9 +292,9 @@ async function testOpenExecCapabilitiesAndClose(): Promise { assert.ok(query.includes(0x44), 'missing DataRow'); assert.ok(query.includes(0x5a), 'missing ReadyForQuery'); - const backup = await db.backup('sql'); - assert.equal(backup.format, 'sql'); - assert.equal(new TextDecoder().decode(backup.bytes), 'sql-backup'); + const backup = await db.backup('physicalArchive'); + assert.equal(backup.format, 'physicalArchive'); + assert.equal(new TextDecoder().decode(backup.bytes), 'physicalArchive-backup'); await db.close(); await db.close(); @@ -445,6 +461,19 @@ async function testOpenRequiresJsiTransportBeforeNativeCall(): Promise { } } +async function testOpenRejectsBrokerServerBeforeNativeCall(): Promise { + for (const engine of ['nativeBroker', 'nativeServer'] as const) { + const native = new MockNative(); + const client = createOliphauntClient(native); + + await assert.rejects( + () => client.open({ engine } as unknown as OpenConfig), + new RegExp(`React Native open currently supports nativeDirect, got ${engine}`), + ); + assert.deepEqual(native.openCalls, []); + } +} + async function testJsiArrayBufferTransportRejectsNonBinaryResponses(): Promise { const native = new MockNative(); const globalWithJsi = globalThis as GlobalWithJsiTransport; @@ -474,7 +503,7 @@ async function testJsiArrayBufferTransportRejectsNonBinaryResponses(): Promise { - const native = new MockNative(); + const native = new DirectCapabilitiesNative(); let afterSmokeValue = ''; // liboliphaunt-doc-example:react-native-smoke-runner const report = await runOliphauntReactNativeSmoke(createOliphauntClient(native), { @@ -483,7 +512,6 @@ async function testReusableReactNativeSmokeRunnerExercisesInstalledTransportShap extensions: ['vector'], resourceRoot: '/tmp/oliphaunt-rn-smoke-resources', }, - expectedEngine: 'nativeServer', requirePackageSizeReport: true, afterSmoke: async (database) => { assert.deepEqual(native.closedHandles, []); @@ -492,7 +520,7 @@ async function testReusableReactNativeSmokeRunnerExercisesInstalledTransportShap }, }); - assert.equal(report.engine, 'nativeServer'); + assert.equal(report.engine, 'nativeDirect'); assert.equal(report.rawProtocolTransport, 'jsi-array-buffer'); assert.equal(report.selectOne, '1'); assert.equal(report.parameterRoundTrip, 'hello'); @@ -842,16 +870,14 @@ async function testConnectionStringIsOnlyPresentForServerCapabilities(): Promise assert.equal((await direct.capabilities()).crashRestartable, false); await direct.close(); - const server = await createOliphauntClient(new MockNative()).open({ - engine: 'nativeServer', - }); - assert.equal(await server.connectionString(), 'postgres://postgres@127.0.0.1:55432/template1'); - assert.equal((await server.capabilities()).independentSessions, true); - assert.equal((await server.capabilities()).reopenable, true); - assert.equal((await server.capabilities()).sameRootLogicalReopen, false); - assert.equal((await server.capabilities()).rootSwitchable, true); - assert.equal((await server.capabilities()).crashRestartable, false); - await server.close(); + const support = await createOliphauntClient(new MockNative()).supportedModes(); + const server = support.find((entry) => entry.engine === 'nativeServer'); + assert.equal(server?.capabilities.connectionString, 'postgres://postgres@127.0.0.1:55432/template1'); + assert.equal(server?.capabilities.independentSessions, true); + assert.equal(server?.capabilities.reopenable, true); + assert.equal(server?.capabilities.sameRootLogicalReopen, false); + assert.equal(server?.capabilities.rootSwitchable, true); + assert.equal(server?.capabilities.crashRestartable, false); } async function testTransactionCommitsAndRejectsUnpinnedInterleaving(): Promise { @@ -1396,6 +1422,7 @@ class MockNative implements Spec { restoreFormats: ['physicalArchive'], simpleQuery: true, extensions: true, + connectionString: 'postgres://postgres@127.0.0.1:55432/template1', rawProtocolTransport: 'jsi-array-buffer', }, unavailableReason: 'server adapter is unavailable', diff --git a/src/sdks/react-native/src/client.ts b/src/sdks/react-native/src/client.ts index 23fc2f71..8606aae2 100644 --- a/src/sdks/react-native/src/client.ts +++ b/src/sdks/react-native/src/client.ts @@ -41,7 +41,7 @@ export type PostgresStartupGUC = export type BinaryInput = ArrayBuffer | ArrayBufferView | Uint8Array | ReadonlyArray; export type OpenConfig = { - engine?: EngineMode; + engine?: 'nativeDirect'; root?: string; temporary?: boolean; durability?: DurabilityProfile; @@ -508,7 +508,7 @@ function normalizeOpenConfig(config: OpenConfig): NativeOpenConfig { ); const resourceRoot = validateOptionalPathOverride(config.resourceRoot, 'resourceRoot'); return { - engine: config.engine ?? 'nativeDirect', + engine: normalizeOpenEngine(config.engine), root: config.root, temporary: config.temporary, durability: config.durability ?? 'balanced', @@ -523,6 +523,18 @@ function normalizeOpenConfig(config: OpenConfig): NativeOpenConfig { }; } +function normalizeOpenEngine(engine: unknown): 'nativeDirect' { + if (engine === undefined || engine === null || engine === 'nativeDirect') { + return 'nativeDirect'; + } + if (engine === 'nativeBroker' || engine === 'nativeServer') { + throw new Error( + `React Native open currently supports nativeDirect, got ${engine}; use supportedModes() to inspect broker/server availability`, + ); + } + throw new Error(`unsupported engine mode ${String(engine)}`); +} + function normalizeResourceConfig(options: PackageSizeReportOptions): NativeResourceConfig { return { resourceRoot: validateOptionalPathOverride(options.resourceRoot, 'resourceRoot'), diff --git a/tools/policy/check-sdk-parity.sh b/tools/policy/check-sdk-parity.sh index 05a66589..673c9bd3 100755 --- a/tools/policy/check-sdk-parity.sh +++ b/tools/policy/check-sdk-parity.sh @@ -410,10 +410,18 @@ require_text src/sdks/kotlin/oliphaunt/src/commonTest/kotlin/dev/oliphaunt/Oliph "Kotlin tests must lock the mobile PG18 startup GUC contract" require_text src/sdks/react-native/src/client.ts "export type RuntimeFootprintProfile" \ "React Native SDK must expose runtime footprint profiles" +require_text src/sdks/react-native/src/client.ts "engine?: 'nativeDirect'" \ + "React Native OpenConfig must only expose nativeDirect until the RN bridge supports broker/server open paths" require_text src/sdks/react-native/src/client.ts "runtimeFootprint?: RuntimeFootprintProfile" \ "React Native OpenConfig must expose runtime footprint selection" require_text src/sdks/react-native/src/client.ts "startupGUCs?: ReadonlyArray" \ "React Native OpenConfig must expose startup GUC overrides" +require_text src/sdks/react-native/src/client.ts "React Native open currently supports nativeDirect" \ + "React Native SDK must reject broker/server open requests before crossing the native bridge" +require_text src/sdks/react-native/src/__tests__/client.test.ts "testOpenRejectsBrokerServerBeforeNativeCall" \ + "React Native tests must lock broker/server open rejection before native calls" +require_text src/sdks/react-native/src/__tests__/client.test.ts "@ts-expect-error React Native open currently supports nativeDirect only." \ + "React Native tests must lock the direct-only OpenConfig type surface" require_text src/sdks/react-native/src/client.ts "function normalizeRuntimeFootprint" \ "React Native SDK must validate runtime footprint profiles before native calls" require_text src/sdks/react-native/src/client.ts "function validateStartupGUCs" \ @@ -905,6 +913,8 @@ require_text src/sdks/react-native/README.md "\`OliphauntDatabase.checkpoint()\` "React Native README must document checkpoint DX" require_text src/sdks/react-native/README.md "\`Oliphaunt.supportedModes()\`" \ "React Native README must document mode support discovery" +require_text src/sdks/react-native/README.md "currently accepts \`nativeDirect\` only" \ + "React Native README must document that mode discovery is broader than the current open surface" require_text src/sdks/react-native/README.md "\`backupFormats\` and \`restoreFormats\`" \ "React Native README must document backup/restore format support discovery" require_text src/sdks/react-native/README.md "\`OliphauntDatabase.supportsBackupFormat\` and" \ From 4e342a70234efbcb77701cb3b5fad1747fcbffdf Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 11:14:00 +0000 Subject: [PATCH 074/137] fix: validate wasix tools aot manifests --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 4 + .../oliphaunt-wasix/src/oliphaunt/aot.rs | 90 ++++++++++++++++++- tools/policy/check-sdk-parity.sh | 8 ++ 3 files changed, 101 insertions(+), 1 deletion(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 66185128..9a9d1ab1 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -361,6 +361,10 @@ review production pipelines, then normalize implementation details. tool artifacts must contain both `pg_dump` and `psql`; WASIX tool artifacts must contain `pg_dump` and `psql` payloads and reject `pg_ctl`; WASIX tools-AOT similarly requires `pg_dump`/`psql` AOT payloads. +- `oliphaunt-wasix` now validates the package-manager-resolved tools AOT + manifest again at SDK load time: it must contain exactly `tool:pg_dump` and + `tool:psql`, with no missing, duplicate, or non-tool artifacts before the + tools manifest is merged into the runtime AOT namespace. - On 2026-06-26, the current branch passed the package-surface verification gates for the P0 CI/release metadata item: `check_release_metadata.py`, `check_consumer_shape.py`, `check_artifact_targets.py`, diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs index 10daf8f6..4481d7aa 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; use std::fs; use std::io::{Cursor, Read}; use std::path::{Path, PathBuf}; @@ -32,6 +32,7 @@ const AOT_ENGINE_ID: &str = concat!( ); const ZSTD_MAGIC: &[u8] = &[0x28, 0xb5, 0x2f, 0xfd]; const CACHE_RECEIPT_FORMAT_VERSION: u32 = 1; +const TOOL_AOT_ARTIFACTS: &[&str] = &["tool:pg_dump", "tool:psql"]; static AOT_INSTALL_LOCK: OnceLock> = OnceLock::new(); static HEADLESS_ENGINE: OnceLock = OnceLock::new(); static INSTALLED_ARTIFACTS: OnceLock>> = OnceLock::new(); @@ -506,10 +507,33 @@ fn merge_tools_aot_manifest(manifest: &mut AotManifest) -> Result<()> { tools_manifest.postgres_version == manifest.postgres_version, "tools AOT manifest postgres version mismatch" ); + validate_tools_aot_manifest_artifacts(&tools_manifest.artifacts)?; manifest.artifacts.extend(tools_manifest.artifacts); Ok(()) } +fn validate_tools_aot_manifest_artifacts(artifacts: &[AotManifestArtifact]) -> Result<()> { + let mut seen = BTreeSet::new(); + for artifact in artifacts { + let name = artifact.name.as_str(); + ensure!( + TOOL_AOT_ARTIFACTS.contains(&name), + "tools AOT manifest contains unexpected artifact '{name}'; expected only tool:pg_dump and tool:psql" + ); + ensure!( + seen.insert(name), + "tools AOT manifest contains duplicate artifact '{name}'" + ); + } + for &required in TOOL_AOT_ARTIFACTS { + ensure!( + seen.contains(required), + "tools AOT manifest is missing required artifact '{required}'" + ); + } + Ok(()) +} + fn merge_extension_aot_manifests(_manifest: &mut AotManifest) -> Result<()> { #[cfg(feature = "extensions")] { @@ -1023,6 +1047,70 @@ mod tests { ); } + #[test] + fn tools_aot_manifest_artifacts_must_be_exact_tool_pair() { + validate_tools_aot_manifest_artifacts(&[ + test_manifest_artifact("tool:pg_dump"), + test_manifest_artifact("tool:psql"), + ]) + .expect("pg_dump and psql tool pair should be accepted"); + } + + #[test] + fn tools_aot_manifest_rejects_missing_tool_artifacts() { + let error = + validate_tools_aot_manifest_artifacts(&[test_manifest_artifact("tool:pg_dump")]) + .expect_err("missing psql should be rejected"); + assert!( + error + .to_string() + .contains("missing required artifact 'tool:psql'"), + "unexpected error: {error:#}" + ); + } + + #[test] + fn tools_aot_manifest_rejects_duplicate_tool_artifacts() { + let error = validate_tools_aot_manifest_artifacts(&[ + test_manifest_artifact("tool:pg_dump"), + test_manifest_artifact("tool:pg_dump"), + test_manifest_artifact("tool:psql"), + ]) + .expect_err("duplicate tool should be rejected"); + assert!( + error + .to_string() + .contains("duplicate artifact 'tool:pg_dump'"), + "unexpected error: {error:#}" + ); + } + + #[test] + fn tools_aot_manifest_rejects_non_tool_artifacts() { + let error = validate_tools_aot_manifest_artifacts(&[ + test_manifest_artifact("tool:pg_dump"), + test_manifest_artifact("tool:psql"), + test_manifest_artifact("runtime:oliphaunt"), + ]) + .expect_err("non-tool artifact should be rejected"); + assert!( + error + .to_string() + .contains("unexpected artifact 'runtime:oliphaunt'"), + "unexpected error: {error:#}" + ); + } + + fn test_manifest_artifact(name: &str) -> AotManifestArtifact { + AotManifestArtifact { + name: name.to_owned(), + sha256: "compressed-sha256".to_owned(), + module_sha256: "module-sha256".to_owned(), + raw_sha256: Some("raw-sha256".to_owned()), + raw_size: Some(1), + } + } + fn toolchain_value(key: &str) -> &str { let rest = WASIX_TOOLCHAIN .split_once("[toolchain]") diff --git a/tools/policy/check-sdk-parity.sh b/tools/policy/check-sdk-parity.sh index 673c9bd3..65d564f5 100755 --- a/tools/policy/check-sdk-parity.sh +++ b/tools/policy/check-sdk-parity.sh @@ -120,6 +120,14 @@ require_text src/sdks/rust/crates/oliphaunt-build/src/lib.rs "runtime/bin/psql" "Rust oliphaunt-build must validate psql in split native-tools artifact manifests" require_text src/sdks/rust/crates/oliphaunt-build/src/lib.rs "bin/pg_ctl.wasix.wasm" \ "Rust oliphaunt-build must reject pg_ctl from split WASIX tools artifact manifests" +require_text src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs 'TOOL_AOT_ARTIFACTS: &[&str] = &["tool:pg_dump", "tool:psql"]' \ + "WASIX SDK must define the exact split tools AOT artifact set" +require_text src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs "validate_tools_aot_manifest_artifacts(&tools_manifest.artifacts)" \ + "WASIX SDK must validate split tools AOT manifests before merging them into the runtime AOT namespace" +require_text src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs "tools AOT manifest contains unexpected artifact" \ + "WASIX SDK must reject non-tool artifacts from split tools AOT manifests" +require_text src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs "tools AOT manifest is missing required artifact" \ + "WASIX SDK must reject split tools AOT manifests that omit pg_dump or psql" require_manifest_text swift 'classification = "sdk"' \ "SDK manifest must classify Swift as a product SDK" require_manifest_text swift 'primary_targets = ["ios", "macos"]' \ From edcdc0edd08951795ccca57fa13e4ea605fdb148 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 11:27:10 +0000 Subject: [PATCH 075/137] fix: exercise wasix example tool smoke --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 13 +++++++++-- .../examples-ci-release-validation.md | 8 ++++++- examples/tools/check-examples.sh | 13 ++++++++--- .../examples/tauri-sqlx-vanilla/README.md | 17 +++++++++----- .../tauri-sqlx-vanilla/src-tauri/src/bench.rs | 22 ++++++++----------- 5 files changed, 49 insertions(+), 24 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 9a9d1ab1..91d3ea6d 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -8,7 +8,7 @@ review production pipelines, then normalize implementation details. ## Priority 0: Current Acceptance Gates - [x] Confirm generated Cargo crates stay under the crates.io 10 MiB limit. -- [x] Confirm WASIX example smoke tests install `oliphaunt-wasix-tools` from the local registry and exercise the split tools path with `pg_dump`. +- [x] Confirm WASIX example smoke tests install `oliphaunt-wasix-tools` from the local registry and exercise the split tools path with `pg_dump` and `psql`. - [x] Confirm native and WASIX examples resolve local published runtime, tools, and extension crates with locked installs. - [x] Add direct `psql` execution coverage when the WASIX SDK exposes a public tool runner for it. - [x] Run GUI-level e2e for Electron and Tauri examples, or document the exact missing host capabilities if a full GUI run is blocked. @@ -64,7 +64,10 @@ review production pipelines, then normalize implementation details. ## Current Notes - The active branch contains the split native/WASIX tools package work and the example GUI smoke coverage. -- Local-registry WASIX smoke coverage proves `pg_dump` through the SDK `dump_sql` path and `psql` through `PsqlOptions::command("SELECT 1")`. +- Local-registry WASIX smoke coverage proves `pg_dump` through the SDK + `dump_sql` path and `psql` through `PsqlOptions::command("SELECT 1")`. + Example policy now requires `preflight_tools()`, `dump_sql`, and `psql` calls + in every WASIX example that validates the split tools package. - Local-registry Cargo payload inspection confirmed `liboliphaunt-native-linux-x64-gnu-part-*` contains `initdb`, `pg_ctl`, and `postgres` only under `runtime/bin`, while `oliphaunt-tools-linux-x64-gnu-part-*` contains only `pg_dump` and `psql` there. - The small liboliphaunt release fixture now includes all five native desktop PostgreSQL binaries so fixture Cargo packaging exercises the split: @@ -106,6 +109,12 @@ review production pipelines, then normalize implementation details. extension crates from `oliphaunt-local`; WASIX Tauri exercised the split WASIX runtime/tools/AOT and selected extension package graph through WebDriver. +- On 2026-06-26, the nested WASIX SQLx Tauri profiler was switched to TCP + startup so its headless local-registry run executes the split WASIX tools + smoke (`preflight_tools`, `pg_dump --schema-only`, and noninteractive + `psql SELECT 1`) on Linux instead of returning early on the Unix-socket path. + The local-registry profiler command passed with `--fresh --rows 10`, and the + generated report included a `validate split WASIX tools` startup phase. - On 2026-06-26 after the Bun lockfile-sync conversion, the four GUI smoke commands passed again against the staged local Cargo and Verdaccio registries: `examples/tools/run-electron-driver-smoke.sh examples/electron`, diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index 8f191257..b4bbdb9f 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -23,7 +23,7 @@ the release/tooling surface after the runtime tool crate split. - [x] Exercise tool paths in example code, not only in dependency manifests: - native example should execute a flow that requires packaged `pg_dump` - WASIX example should execute a flow that requires packaged `pg_dump` - - WASIX example should compile with `psql` available from `oliphaunt-wasix-tools` + - WASIX example should execute noninteractive `psql SELECT 1` from `oliphaunt-wasix-tools` - [x] Run `examples/tools/with-local-registries.sh` installs/builds for each root example. - [x] Run native and WASIX app smoke flows where available. @@ -132,6 +132,12 @@ the release/tooling surface after the runtime tool crate split. `examples/tools/run-electron-driver-smoke.sh examples/electron-wasix`, `examples/tools/run-tauri-webdriver-smoke.sh examples/tauri`, and `examples/tools/run-tauri-webdriver-smoke.sh examples/tauri-wasix`. +- On 2026-06-26, the nested WASIX SQLx Tauri profiler was switched to the + default TCP `OliphauntServer` path so its local-registry smoke executes + `preflight_tools`, `pg_dump --schema-only`, and noninteractive `psql SELECT 1` + instead of skipping tool execution on Unix socket runs. +- The validating command passed: + `examples/tools/with-local-registries.sh cargo run --manifest-path src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml --bin profile_queries -- --fresh --rows 10 --json-out target/oliphaunt-wasix-rust/examples/tauri-sqlx-vanilla/profile-smoke.json`. - The nested WASIX SQLx Tauri example check now keeps normal CI on `pnpm install --frozen-lockfile` but switches to `--no-frozen-lockfile` when `examples/tools/with-local-registries.sh` has disabled pnpm lockfile reads to diff --git a/examples/tools/check-examples.sh b/examples/tools/check-examples.sh index 0414be3f..8252a3a0 100755 --- a/examples/tools/check-examples.sh +++ b/examples/tools/check-examples.sh @@ -50,6 +50,13 @@ require_text() { fi } +require_wasix_tools_smoke() { + local path="$1" + require_text "$path" 'preflight_tools\(\)' + require_text "$path" 'dump_sql' + require_text "$path" 'psql\(|PsqlOptions::new\(\)' +} + reject_text() { local path="$1" local pattern="$2" @@ -115,7 +122,7 @@ require_text "examples/tauri-wasix/src-tauri/Cargo.toml" 'oliphaunt-wasix-tools- require_text "examples/tauri-wasix/src-tauri/Cargo.lock" 'oliphaunt-extension-hstore-wasix-aot-x86_64-unknown-linux-gnu' require_text "examples/tauri-wasix/src-tauri/Cargo.lock" 'oliphaunt-extension-pg-trgm-wasix-aot-x86_64-unknown-linux-gnu' require_text "examples/tauri-wasix/src-tauri/Cargo.lock" 'oliphaunt-extension-unaccent-wasix-aot-x86_64-unknown-linux-gnu' -require_text "examples/tauri-wasix/src-tauri/src/lib.rs" 'preflight_tools\(\)' +require_wasix_tools_smoke "examples/tauri-wasix/src-tauri/src/lib.rs" require_text "examples/electron-wasix/src-wasix/Cargo.toml" 'registry = "oliphaunt-local"' require_text "examples/electron-wasix/src-wasix/Cargo.toml" '"tools"' require_text "examples/electron-wasix/src-wasix/Cargo.toml" 'oliphaunt-wasix-tools' @@ -124,12 +131,12 @@ require_text "examples/electron-wasix/src-wasix/Cargo.toml" 'oliphaunt-wasix-too require_text "examples/electron-wasix/src-wasix/Cargo.lock" 'oliphaunt-extension-hstore-wasix-aot-x86_64-unknown-linux-gnu' require_text "examples/electron-wasix/src-wasix/Cargo.lock" 'oliphaunt-extension-pg-trgm-wasix-aot-x86_64-unknown-linux-gnu' require_text "examples/electron-wasix/src-wasix/Cargo.lock" 'oliphaunt-extension-unaccent-wasix-aot-x86_64-unknown-linux-gnu' -require_text "examples/electron-wasix/src-wasix/src/main.rs" 'preflight_tools\(\)' +require_wasix_tools_smoke "examples/electron-wasix/src-wasix/src/main.rs" require_text "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml" 'registry = "oliphaunt-local"' require_text "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml" '"tools"' require_text "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml" 'oliphaunt-wasix-tools' require_text "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml" 'oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu' -require_text "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs" 'preflight_tools\(\)' +require_wasix_tools_smoke "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs" reject_text "examples/electron/package.json" '"@oliphaunt/ts": "workspace:\*"' reject_text "examples/tauri/src-tauri/Cargo.toml" 'path = "../../../src/sdks/rust' reject_text "examples/tauri-wasix/src-tauri/Cargo.toml" 'path = "../../../src/bindings/wasix-rust' diff --git a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/README.md b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/README.md index ae7fbd91..b8ab92b5 100644 --- a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/README.md +++ b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/README.md @@ -6,8 +6,8 @@ it through a real one-connection `sqlx::PgPool`. ## Run the desktop app ```sh -pnpm install -pnpm run tauri dev +examples/tools/with-local-registries.sh pnpm --dir src/bindings/wasix-rust/examples/tauri-sqlx-vanilla install +examples/tools/with-local-registries.sh pnpm --dir src/bindings/wasix-rust/examples/tauri-sqlx-vanilla tauri dev ``` The app opens first and runs the database profile only when the profile command @@ -16,8 +16,11 @@ is invoked from the UI. ## Run the headless profiler ```sh -cd src-tauri -cargo run --release --bin profile_queries -- --fresh --rows 10000 --json-out /tmp/oliphaunt-profile-release.json +examples/tools/with-local-registries.sh cargo run \ + --manifest-path src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml \ + --release \ + --bin profile_queries \ + -- --fresh --rows 10000 --json-out /tmp/oliphaunt-profile-release.json ``` Use `--fresh` to remove the profile data directory before the run. Omit it to @@ -28,4 +31,8 @@ measure a warm start with an existing cluster. - storing the database in managed Rust state; - using `OliphauntServer` to hand SQLx a PostgreSQL URI; - configuring the SQLx pool with `max_connections(1)`; -- creating schema, seeding rows, and profiling real SQL queries. +- creating schema, seeding rows, and profiling real SQL queries; +- resolving `oliphaunt-wasix-tools` and tools-AOT crates from the configured + Cargo registry; +- preflighting the split WASIX tools, running `pg_dump --schema-only`, and + running noninteractive `psql` with `SELECT 1`. diff --git a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs index 1f00ed73..b191c312 100644 --- a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs +++ b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs @@ -3,10 +3,10 @@ use std::future::Future; use std::path::PathBuf; use std::time::{Duration, Instant}; -use anyhow::{Context, Result, anyhow, bail}; +use anyhow::{anyhow, bail, Context, Result}; use oliphaunt_wasix::{ - OliphauntPaths, OliphauntServer, PgDumpOptions, PsqlOptions, install_into, - preload_runtime_module, + install_into, preload_runtime_module, OliphauntPaths, OliphauntServer, PgDumpOptions, + PsqlOptions, }; use serde::Serialize; use sqlx::postgres::{PgConnectOptions, PgPoolOptions, PgSslMode}; @@ -123,7 +123,11 @@ impl DatabaseHarness { preferred_server(server_root) }) .await?; - validate_wasix_tools(&server)?; + let server = time_blocking(&mut startup, "validate split WASIX tools", move || { + validate_wasix_tools(&server)?; + Ok(server) + }) + .await?; let database_url = server.connection_uri(); let pool = time_async(&mut startup, "sqlx pool connect", async { @@ -354,15 +358,7 @@ fn validate_wasix_tools(server: &OliphauntServer) -> Result<()> { } fn preferred_server(root: PathBuf) -> Result { - let builder = OliphauntServer::builder().path(&root); - #[cfg(unix)] - { - builder.unix(root.join(".s.PGSQL.5432")).start() - } - #[cfg(not(unix))] - { - builder.start() - } + OliphauntServer::builder().path(&root).start() } fn pg_connect_options(server: &OliphauntServer) -> Result { From 6912d9cfab43e194d69064efce264704aef3a6f8 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 11:41:23 +0000 Subject: [PATCH 076/137] fix: align release checksum tooling --- .github/workflows/release.yml | 2 +- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 10 +++ ...2026-06-07-transitional-catalog-smoke.json | 2 +- .../generated/docs/extension-evidence.json | 80 +++++++++---------- .../assets/generated/asset-inputs.sha256 | 2 +- tools/policy/check-tooling-stack.sh | 9 +++ tools/release/check_release_metadata.py | 6 ++ .../package-liboliphaunt-aggregate-assets.sh | 37 ++------- tools/release/write_checksum_manifest.mjs | 8 +- 9 files changed, 81 insertions(+), 75 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3ebf66c8..4379f492 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -275,7 +275,7 @@ jobs: ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.MAVEN_GPG_KEY_ID }} ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.MAVEN_GPG_PASSPHRASE }} - run: tools/release/check_publish_environment.py --products-json "${PRODUCTS_JSON}" + run: tools/release/check_publish_environment.mjs --products-json "${PRODUCTS_JSON}" - name: Require release-commit CI build gate id: ci_build_gate diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 91d3ea6d..b4b0153e 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -184,10 +184,20 @@ review production pipelines, then normalize implementation details. broker and node-direct release asset paths. The helper preserves deterministic basename-sorted SHA-256 output, streams large archive hashing, and is called directly from `release.py`, broker packaging, and node-direct packaging. +- The same Bun checksum helper now emits strict `./asset` manifest paths, fails + closed when no payload assets match, and is reused by the aggregate + liboliphaunt release asset packager instead of an inline Python checksum + heredoc. `check-tooling-stack.sh` rejects drift back to the inline Python + checksum path. A direct aggregate packager run reached release asset + validation but could not pass with the local cached Android asset because that + generated artifact is stale and still contains unstripped ELF debug sections. - Release publish-environment validation now uses Bun instead of Python. The helper scans product `release.toml` metadata directly, validates selected product ids, and preserves the trusted-publishing, GitHub, Maven, and forbidden-token checks. +- The Release workflow now calls the Bun publish-environment helper directly; + release metadata checks reject the retired Python helper path in the workflow + and require `release.py publish` dry-runs to use the same Bun helper. - Product release-tag verification now uses Bun instead of Python. The helper reads release-please product config, resolves the product's current version, and verifies the product-scoped tag points at the release commit. diff --git a/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json b/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json index 639fe860..cade5524 100644 --- a/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json +++ b/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json @@ -514,7 +514,7 @@ } ], "schema": "oliphaunt-extension-evidence-v1", - "sourceDigest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f", + "sourceDigest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8", "sourceDigestInputs": [ "src/postgres/versions/18/source.toml", "src/extensions/catalog/extensions.promoted.toml", diff --git a/src/extensions/generated/docs/extension-evidence.json b/src/extensions/generated/docs/extension-evidence.json index ee585d5d..3f655541 100644 --- a/src/extensions/generated/docs/extension-evidence.json +++ b/src/extensions/generated/docs/extension-evidence.json @@ -20,7 +20,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -56,7 +56,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -92,7 +92,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -128,7 +128,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -164,7 +164,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -200,7 +200,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -236,7 +236,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -272,7 +272,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -308,7 +308,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -344,7 +344,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -380,7 +380,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -416,7 +416,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -452,7 +452,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -488,7 +488,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -524,7 +524,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -560,7 +560,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -596,7 +596,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -632,7 +632,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -668,7 +668,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -704,7 +704,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -740,7 +740,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -776,7 +776,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -812,7 +812,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -848,7 +848,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -884,7 +884,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -920,7 +920,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -956,7 +956,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -992,7 +992,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -1028,7 +1028,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -1064,7 +1064,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -1100,7 +1100,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -1136,7 +1136,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -1172,7 +1172,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -1208,7 +1208,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -1244,7 +1244,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -1280,7 +1280,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -1316,7 +1316,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -1352,7 +1352,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -1388,7 +1388,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f" + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" } ], "platform-targets": [ @@ -1420,7 +1420,7 @@ "path": "src/extensions/evidence/runs" } ], - "source-digest": "sha256:58d4cf16ab1bd172689152be0c2a611e0711a9761abc2d6408398278b00a0a2f", + "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8", "source-digest-inputs": [ "src/postgres/versions/18/source.toml", "src/extensions/catalog/extensions.promoted.toml", diff --git a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 index 91847a85..01901ecc 100644 --- a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 +++ b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 @@ -1 +1 @@ -d0fc9d49b00d356052ed91846b9f2f8e495fca79411a85a3ca118ed7f5fb478b +21885820e26443b452e9ebb46ea5bfdb9f904b1f0a4b26fc552667603be07ee5 diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 1b404cda..9d67c018 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -290,6 +290,15 @@ grep -Fq 'missing package-shape output' tools/release/build-sdk-ci-artifacts.sh if grep -Fq 'OLIPHAUNT_SDK_CHECK_SCRATCH="$work_root/check"' tools/release/build-sdk-ci-artifacts.sh; then fail "SDK artifact builder must not rerun package-shape inside the artifact staging script" fi +grep -Fq 'tools/release/write_checksum_manifest.mjs \' tools/release/package-liboliphaunt-aggregate-assets.sh || + fail "aggregate liboliphaunt asset packager must use the shared Bun checksum manifest writer" +if grep -Fq 'python3 - "$asset_dir" "$checksum_file"' tools/release/package-liboliphaunt-aggregate-assets.sh; then + fail "aggregate liboliphaunt asset packager must not embed inline Python for checksum manifests" +fi +grep -Fq ' ./${path.basename(asset)}' tools/release/write_checksum_manifest.mjs || + fail "shared release checksum writer must emit strict './asset' paths" +grep -Fq 'no release assets found' tools/release/write_checksum_manifest.mjs || + fail "shared release checksum writer must fail when no payload assets match" grep -Fq 'upstream="${OLIPHAUNT_MOON_UPSTREAM:-deep}"' .github/scripts/run-affected-moon-task.sh || fail "affected quality Moon helper must preserve Moon upstream task inheritance by default" grep -Fq 'exec .github/scripts/run-moon-targets.sh --upstream "$upstream"' .github/scripts/run-affected-moon-task.sh || diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 467bcc20..cad73afb 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -275,6 +275,12 @@ def validate_exact_extension_registry_shape(graph: dict) -> None: def validate_publish_target_coverage(graph: dict) -> None: workflow = read_text(".github/workflows/release.yml") release_source = read_text("tools/release/release.py") + if "tools/release/check_publish_environment.mjs --products-json" not in workflow: + fail("Release workflow must validate publish credentials through the Bun publish-environment helper") + if "tools/release/check_publish_environment.py" in workflow: + fail("Release workflow must not call the retired Python publish-environment helper") + if 'run(["tools/release/check_publish_environment.mjs", *products_args])' not in release_source: + fail("release.py publish dry-run must validate publish credentials through the Bun helper") saw_extension = False for product, config in product_metadata.graph_products(graph).items(): declared = set(product_metadata.string_list(config, "publish_targets", product)) diff --git a/tools/release/package-liboliphaunt-aggregate-assets.sh b/tools/release/package-liboliphaunt-aggregate-assets.sh index 7169d048..318444b0 100755 --- a/tools/release/package-liboliphaunt-aggregate-assets.sh +++ b/tools/release/package-liboliphaunt-aggregate-assets.sh @@ -18,35 +18,12 @@ asset_dir="${OLIPHAUNT_LIBOLIPHAUNT_RELEASE_ASSETS:-target/liboliphaunt/release- version="$(python3 tools/release/product_metadata.py version liboliphaunt-native)" checksum_file="$asset_dir/liboliphaunt-${version}-release-assets.sha256" -python3 - "$asset_dir" "$checksum_file" <<'PY' -from __future__ import annotations - -import hashlib -import sys -from pathlib import Path - -asset_dir = Path(sys.argv[1]) -checksum_file = Path(sys.argv[2]) -payloads = sorted( - path - for path in asset_dir.iterdir() - if path.is_file() - and path != checksum_file - and ( - path.name.endswith(".tar.gz") - or path.name.endswith(".tar.zst") - or path.name.endswith(".zip") - or path.name.endswith(".tsv") - ) -) -if not payloads: - raise SystemExit(f"no liboliphaunt release payload assets found in {asset_dir}") - -lines = [] -for path in payloads: - digest = hashlib.sha256(path.read_bytes()).hexdigest() - lines.append(f"{digest} ./{path.name}\n") -checksum_file.write_text("".join(lines), encoding="utf-8") -PY +tools/release/write_checksum_manifest.mjs \ + --asset-dir "$asset_dir" \ + --output "$(basename "$checksum_file")" \ + --pattern '*.tar.gz' \ + --pattern '*.tar.zst' \ + --pattern '*.zip' \ + --pattern '*.tsv' tools/release/check_liboliphaunt_release_assets.py --asset-dir "$asset_dir" diff --git a/tools/release/write_checksum_manifest.mjs b/tools/release/write_checksum_manifest.mjs index 546641b9..846680cb 100755 --- a/tools/release/write_checksum_manifest.mjs +++ b/tools/release/write_checksum_manifest.mjs @@ -70,10 +70,14 @@ async function matchingAssets(assetDir, patterns) { const args = parseArgs(Bun.argv.slice(2)); const outputPath = path.join(args.assetDir, args.output); const lines = []; -for (const asset of await matchingAssets(args.assetDir, args.patterns)) { +const assets = await matchingAssets(args.assetDir, args.patterns); +if (assets.length === 0) { + fail(`no release assets found in ${args.assetDir} matching ${args.patterns.join(', ')}`); +} +for (const asset of assets) { if (path.resolve(asset) === path.resolve(outputPath)) { continue; } - lines.push(`${await sha256(asset)} ${path.basename(asset)}\n`); + lines.push(`${await sha256(asset)} ./${path.basename(asset)}\n`); } await fs.writeFile(outputPath, lines.join('')); From 241373b1b88c89c807526b438e032b43ee6b0c22 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 11:52:13 +0000 Subject: [PATCH 077/137] chore: port extension contract check to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 4 ++ .../extension-runtime-contract/moon.yml | 4 +- .../tools/check-contract.mjs | 59 +++++++++++++++++++ .../tools/check-contract.py | 48 --------------- tools/policy/check-tooling-stack.sh | 7 +++ 5 files changed, 72 insertions(+), 50 deletions(-) create mode 100755 src/shared/extension-runtime-contract/tools/check-contract.mjs delete mode 100644 src/shared/extension-runtime-contract/tools/check-contract.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index b4b0153e..d89d8051 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -214,6 +214,10 @@ review production pipelines, then normalize implementation details. - The CI affected-plan wrapper `.github/scripts/plan-affected.py` was removed; the workflow now invokes `python3 tools/graph/ci_plan.py` directly, keeping the shared planner as the single Python entrypoint for CI job selection. +- The extension runtime contract checker now uses Bun instead of Python. The + Moon project is modeled as JavaScript tooling, and `check-tooling-stack.sh` + rejects reintroducing `check-contract.py` or rewiring the task away from the + Bun checker. - The Moon cache witness helper now uses Bun instead of Python. The converted `tools/graph/cache-witness.mjs` preserves the two-step output-cache assertion and resolves `MOON_BIN` or the local proto Moon shim for reliable diff --git a/src/shared/extension-runtime-contract/moon.yml b/src/shared/extension-runtime-contract/moon.yml index a632aa33..0c48c3a6 100644 --- a/src/shared/extension-runtime-contract/moon.yml +++ b/src/shared/extension-runtime-contract/moon.yml @@ -1,7 +1,7 @@ $schema: "https://moonrepo.dev/schemas/project.json" id: "extension-runtime-contract" -language: "python" +language: "javascript" layer: "configuration" stack: "systems" tags: ["extensions", "contract", "runtime"] @@ -19,7 +19,7 @@ owners: tasks: check: tags: ["quality", "static"] - command: "python3 src/shared/extension-runtime-contract/tools/check-contract.py" + command: "bun src/shared/extension-runtime-contract/tools/check-contract.mjs" inputs: - "/src/shared/extension-runtime-contract/**/*" options: diff --git a/src/shared/extension-runtime-contract/tools/check-contract.mjs b/src/shared/extension-runtime-contract/tools/check-contract.mjs new file mode 100755 index 00000000..9c9a6374 --- /dev/null +++ b/src/shared/extension-runtime-contract/tools/check-contract.mjs @@ -0,0 +1,59 @@ +#!/usr/bin/env bun +import { readFile } from 'node:fs/promises'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const CONTRACT = resolve(ROOT, 'contract.toml'); + +function fail(message) { + console.error(`extension-runtime-contract: ${message}`); + process.exit(1); +} + +function isRecord(value) { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +let data; +try { + data = Bun.TOML.parse(await readFile(CONTRACT, 'utf8')); +} catch (error) { + const detail = error instanceof Error ? error.message : String(error); + fail(`cannot parse ${CONTRACT}: ${detail}`); +} + +if (data.schema !== 'oliphaunt-extension-runtime-contract-v1') { + fail('contract.toml must use schema oliphaunt-extension-runtime-contract-v1'); +} + +const runtime = data.runtime; +const selection = data.selection; +const artifacts = data.artifacts; +if (!isRecord(runtime) || !isRecord(selection) || !isRecord(artifacts)) { + fail('contract.toml must define runtime, selection, and artifacts tables'); +} + +if (runtime.resource_layout !== 'share/postgresql/extension') { + fail('runtime.resource_layout must match PostgreSQL extension resources'); +} +if (runtime.dynamic_loader !== 'postgres-compatible') { + fail('runtime.dynamic_loader must stay PostgreSQL-compatible'); +} +if (runtime.static_registry_abi !== 1) { + fail('runtime.static_registry_abi must be 1 until the C ABI changes'); +} +if (selection.unit !== 'sql-extension-name') { + fail('selection.unit must be exact SQL extension name'); +} +for (const key of ['implicit_extensions', 'implicit_extension_groups']) { + if (selection[key] !== false) { + fail(`selection.${key} must be false`); + } +} +if (artifacts.base_runtime_contains_optional_extensions !== false) { + fail('base runtime must not contain optional extension artifacts'); +} +if (artifacts.extension_artifacts_are_exact !== true) { + fail('extension artifacts must be exact-selected'); +} diff --git a/src/shared/extension-runtime-contract/tools/check-contract.py b/src/shared/extension-runtime-contract/tools/check-contract.py deleted file mode 100644 index 97c256aa..00000000 --- a/src/shared/extension-runtime-contract/tools/check-contract.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import pathlib -import sys -import tomllib - - -ROOT = pathlib.Path(__file__).resolve().parents[1] -CONTRACT = ROOT / "contract.toml" - - -def fail(message: str) -> None: - raise SystemExit(f"extension-runtime-contract: {message}") - - -def main() -> None: - try: - data = tomllib.loads(CONTRACT.read_text(encoding="utf-8")) - except Exception as error: - fail(f"cannot parse {CONTRACT}: {error}") - - if data.get("schema") != "oliphaunt-extension-runtime-contract-v1": - fail("contract.toml must use schema oliphaunt-extension-runtime-contract-v1") - runtime = data.get("runtime") - selection = data.get("selection") - artifacts = data.get("artifacts") - if not isinstance(runtime, dict) or not isinstance(selection, dict) or not isinstance(artifacts, dict): - fail("contract.toml must define runtime, selection, and artifacts tables") - if runtime.get("resource_layout") != "share/postgresql/extension": - fail("runtime.resource_layout must match PostgreSQL extension resources") - if runtime.get("dynamic_loader") != "postgres-compatible": - fail("runtime.dynamic_loader must stay PostgreSQL-compatible") - if runtime.get("static_registry_abi") != 1: - fail("runtime.static_registry_abi must be 1 until the C ABI changes") - if selection.get("unit") != "sql-extension-name": - fail("selection.unit must be exact SQL extension name") - for key in ("implicit_extensions", "implicit_extension_groups"): - if selection.get(key) is not False: - fail(f"selection.{key} must be false") - if artifacts.get("base_runtime_contains_optional_extensions") is not False: - fail("base runtime must not contain optional extension artifacts") - if artifacts.get("extension_artifacts_are_exact") is not True: - fail("extension artifacts must be exact-selected") - - -if __name__ == "__main__": - main() diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 9d67c018..a556545e 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -178,6 +178,13 @@ grep -Fq "bun tools/policy/fetch-sources.mjs" src/sources/moon.yml || fail "source fetch task must use cross-platform Bun" grep -Fq "bun tools/policy/assertions/assert-source-inputs.mjs toolchains" src/sources/toolchains/moon.yml || fail "toolchain source checks must use the Bun source-input assertion task" +grep -Fq 'language: "javascript"' src/shared/extension-runtime-contract/moon.yml || + fail "extension runtime contract checks must be modeled as JavaScript/Bun tooling" +grep -Fq 'bun src/shared/extension-runtime-contract/tools/check-contract.mjs' src/shared/extension-runtime-contract/moon.yml || + fail "extension runtime contract check must use the Bun checker" +if [ -e src/shared/extension-runtime-contract/tools/check-contract.py ]; then + fail "extension runtime contract checker must not use the retired Python implementation" +fi for retired_source_input_checker in tools/policy/check-source-inputs.sh tools/policy/check-source-inputs.mjs; do if git ls-files --error-unmatch "$retired_source_input_checker" >/dev/null 2>&1; then fail "source-input policy parsers must live under tools/policy/assertions/assert-*.mjs" From 23b27979e31a6b43db35b933b5edfbb36664413b Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 12:05:14 +0000 Subject: [PATCH 078/137] chore: port extension tree checker to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 3 + src/extensions/contrib/amcheck/moon.yml | 4 +- src/extensions/contrib/auto_explain/moon.yml | 4 +- src/extensions/contrib/bloom/moon.yml | 4 +- src/extensions/contrib/btree_gin/moon.yml | 4 +- src/extensions/contrib/btree_gist/moon.yml | 4 +- src/extensions/contrib/citext/moon.yml | 4 +- src/extensions/contrib/cube/moon.yml | 4 +- src/extensions/contrib/dict_int/moon.yml | 4 +- src/extensions/contrib/dict_xsyn/moon.yml | 4 +- src/extensions/contrib/earthdistance/moon.yml | 4 +- src/extensions/contrib/file_fdw/moon.yml | 4 +- src/extensions/contrib/fuzzystrmatch/moon.yml | 4 +- src/extensions/contrib/hstore/moon.yml | 4 +- src/extensions/contrib/intarray/moon.yml | 4 +- src/extensions/contrib/isn/moon.yml | 4 +- src/extensions/contrib/lo/moon.yml | 4 +- src/extensions/contrib/ltree/moon.yml | 4 +- src/extensions/contrib/moon.yml | 4 +- src/extensions/contrib/pageinspect/moon.yml | 4 +- .../contrib/pg_buffercache/moon.yml | 4 +- .../contrib/pg_freespacemap/moon.yml | 4 +- src/extensions/contrib/pg_surgery/moon.yml | 4 +- src/extensions/contrib/pg_trgm/moon.yml | 4 +- src/extensions/contrib/pg_visibility/moon.yml | 4 +- src/extensions/contrib/pg_walinspect/moon.yml | 4 +- src/extensions/contrib/pgcrypto/moon.yml | 4 +- src/extensions/contrib/seg/moon.yml | 4 +- src/extensions/contrib/tablefunc/moon.yml | 4 +- src/extensions/contrib/tcn/moon.yml | 4 +- .../contrib/tsm_system_rows/moon.yml | 4 +- .../contrib/tsm_system_time/moon.yml | 4 +- src/extensions/contrib/unaccent/moon.yml | 4 +- src/extensions/contrib/uuid_ossp/moon.yml | 4 +- ...2026-06-07-transitional-catalog-smoke.json | 2 +- src/extensions/external/age/moon.yml | 4 +- src/extensions/external/pg_hashids/moon.yml | 4 +- src/extensions/external/pg_ivm/moon.yml | 4 +- .../external/pg_textsearch/moon.yml | 4 +- src/extensions/external/pg_uuidv7/moon.yml | 4 +- src/extensions/external/pgtap/moon.yml | 4 +- src/extensions/external/postgis/moon.yml | 4 +- src/extensions/external/vector/moon.yml | 4 +- .../generated/docs/extension-evidence.json | 80 ++++---- src/extensions/tools/check-extension-tree.mjs | 193 ++++++++++++++++++ src/extensions/tools/check-extension-tree.py | 140 ------------- .../assets/generated/asset-inputs.sha256 | 2 +- .../tools/check-contract.mjs | 0 tools/policy/check-tooling-stack.sh | 11 + 49 files changed, 331 insertions(+), 264 deletions(-) create mode 100755 src/extensions/tools/check-extension-tree.mjs delete mode 100644 src/extensions/tools/check-extension-tree.py mode change 100755 => 100644 src/shared/extension-runtime-contract/tools/check-contract.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index d89d8051..c2fe8748 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -218,6 +218,9 @@ review production pipelines, then normalize implementation details. Moon project is modeled as JavaScript tooling, and `check-tooling-stack.sh` rejects reintroducing `check-contract.py` or rewiring the task away from the Bun checker. +- The extension tree checker now uses Bun instead of Python. Extension Moon + checks reference `check-extension-tree.mjs`, and `check-tooling-stack.sh` + rejects the retired Python checker or task references to it. - The Moon cache witness helper now uses Bun instead of Python. The converted `tools/graph/cache-witness.mjs` preserves the two-step output-cache assertion and resolves `MOON_BIN` or the local proto Moon shim for reliable diff --git a/src/extensions/contrib/amcheck/moon.yml b/src/extensions/contrib/amcheck/moon.yml index 35d756df..ef0686ed 100644 --- a/src/extensions/contrib/amcheck/moon.yml +++ b/src/extensions/contrib/amcheck/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/amcheck" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/amcheck" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/amcheck/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/auto_explain/moon.yml b/src/extensions/contrib/auto_explain/moon.yml index 5ea64e6d..2b8406fd 100644 --- a/src/extensions/contrib/auto_explain/moon.yml +++ b/src/extensions/contrib/auto_explain/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/auto_explain" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/auto_explain" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/auto_explain/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/bloom/moon.yml b/src/extensions/contrib/bloom/moon.yml index 3cd60cee..7a0434d3 100644 --- a/src/extensions/contrib/bloom/moon.yml +++ b/src/extensions/contrib/bloom/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/bloom" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/bloom" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/bloom/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/btree_gin/moon.yml b/src/extensions/contrib/btree_gin/moon.yml index b9bd68f2..0c3979e3 100644 --- a/src/extensions/contrib/btree_gin/moon.yml +++ b/src/extensions/contrib/btree_gin/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/btree_gin" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/btree_gin" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/btree_gin/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/btree_gist/moon.yml b/src/extensions/contrib/btree_gist/moon.yml index 30af94a7..9cb10d33 100644 --- a/src/extensions/contrib/btree_gist/moon.yml +++ b/src/extensions/contrib/btree_gist/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/btree_gist" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/btree_gist" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/btree_gist/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/citext/moon.yml b/src/extensions/contrib/citext/moon.yml index d1aa6321..3fe5ebd0 100644 --- a/src/extensions/contrib/citext/moon.yml +++ b/src/extensions/contrib/citext/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/citext" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/citext" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/citext/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/cube/moon.yml b/src/extensions/contrib/cube/moon.yml index 5572389b..f980e98e 100644 --- a/src/extensions/contrib/cube/moon.yml +++ b/src/extensions/contrib/cube/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/cube" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/cube" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/cube/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/dict_int/moon.yml b/src/extensions/contrib/dict_int/moon.yml index 83866344..cf4d35c0 100644 --- a/src/extensions/contrib/dict_int/moon.yml +++ b/src/extensions/contrib/dict_int/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/dict_int" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/dict_int" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/dict_int/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/dict_xsyn/moon.yml b/src/extensions/contrib/dict_xsyn/moon.yml index 148d22e5..f0bd2e52 100644 --- a/src/extensions/contrib/dict_xsyn/moon.yml +++ b/src/extensions/contrib/dict_xsyn/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/dict_xsyn" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/dict_xsyn" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/dict_xsyn/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/earthdistance/moon.yml b/src/extensions/contrib/earthdistance/moon.yml index 1db9cf3b..bc940002 100644 --- a/src/extensions/contrib/earthdistance/moon.yml +++ b/src/extensions/contrib/earthdistance/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/earthdistance" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/earthdistance" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/earthdistance/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/file_fdw/moon.yml b/src/extensions/contrib/file_fdw/moon.yml index c7bb7e81..ce821c8f 100644 --- a/src/extensions/contrib/file_fdw/moon.yml +++ b/src/extensions/contrib/file_fdw/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/file_fdw" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/file_fdw" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/file_fdw/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/fuzzystrmatch/moon.yml b/src/extensions/contrib/fuzzystrmatch/moon.yml index 02dcc5d0..ad2c4d9b 100644 --- a/src/extensions/contrib/fuzzystrmatch/moon.yml +++ b/src/extensions/contrib/fuzzystrmatch/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/fuzzystrmatch" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/fuzzystrmatch" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/fuzzystrmatch/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/hstore/moon.yml b/src/extensions/contrib/hstore/moon.yml index c48bddc8..8ab1cb14 100644 --- a/src/extensions/contrib/hstore/moon.yml +++ b/src/extensions/contrib/hstore/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/hstore" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/hstore" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/hstore/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/intarray/moon.yml b/src/extensions/contrib/intarray/moon.yml index 08720fed..aa9fcd01 100644 --- a/src/extensions/contrib/intarray/moon.yml +++ b/src/extensions/contrib/intarray/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/intarray" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/intarray" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/intarray/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/isn/moon.yml b/src/extensions/contrib/isn/moon.yml index df8574d8..630fe7f4 100644 --- a/src/extensions/contrib/isn/moon.yml +++ b/src/extensions/contrib/isn/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/isn" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/isn" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/isn/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/lo/moon.yml b/src/extensions/contrib/lo/moon.yml index 90917d71..552ba3a7 100644 --- a/src/extensions/contrib/lo/moon.yml +++ b/src/extensions/contrib/lo/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/lo" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/lo" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/lo/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/ltree/moon.yml b/src/extensions/contrib/ltree/moon.yml index 1fa9e376..15901950 100644 --- a/src/extensions/contrib/ltree/moon.yml +++ b/src/extensions/contrib/ltree/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/ltree" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/ltree" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/ltree/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/moon.yml b/src/extensions/contrib/moon.yml index 0d6943a3..a24240f0 100644 --- a/src/extensions/contrib/moon.yml +++ b/src/extensions/contrib/moon.yml @@ -17,13 +17,13 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib" deps: - "postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/**/*" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/postgres/versions/18/**/*" - "/src/shared/extension-runtime-contract/**/*" options: diff --git a/src/extensions/contrib/pageinspect/moon.yml b/src/extensions/contrib/pageinspect/moon.yml index c31796d5..3ed2117a 100644 --- a/src/extensions/contrib/pageinspect/moon.yml +++ b/src/extensions/contrib/pageinspect/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/pageinspect" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/pageinspect" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/pageinspect/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/pg_buffercache/moon.yml b/src/extensions/contrib/pg_buffercache/moon.yml index b494170d..a361e0aa 100644 --- a/src/extensions/contrib/pg_buffercache/moon.yml +++ b/src/extensions/contrib/pg_buffercache/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/pg_buffercache" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/pg_buffercache" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/pg_buffercache/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/pg_freespacemap/moon.yml b/src/extensions/contrib/pg_freespacemap/moon.yml index 092f6a4d..071f7456 100644 --- a/src/extensions/contrib/pg_freespacemap/moon.yml +++ b/src/extensions/contrib/pg_freespacemap/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/pg_freespacemap" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/pg_freespacemap" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/pg_freespacemap/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/pg_surgery/moon.yml b/src/extensions/contrib/pg_surgery/moon.yml index 74505d1d..ecda81cd 100644 --- a/src/extensions/contrib/pg_surgery/moon.yml +++ b/src/extensions/contrib/pg_surgery/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/pg_surgery" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/pg_surgery" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/pg_surgery/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/pg_trgm/moon.yml b/src/extensions/contrib/pg_trgm/moon.yml index acb3651d..74f09d15 100644 --- a/src/extensions/contrib/pg_trgm/moon.yml +++ b/src/extensions/contrib/pg_trgm/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/pg_trgm" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/pg_trgm" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/pg_trgm/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/pg_visibility/moon.yml b/src/extensions/contrib/pg_visibility/moon.yml index 83bb6fb3..4e89ee28 100644 --- a/src/extensions/contrib/pg_visibility/moon.yml +++ b/src/extensions/contrib/pg_visibility/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/pg_visibility" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/pg_visibility" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/pg_visibility/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/pg_walinspect/moon.yml b/src/extensions/contrib/pg_walinspect/moon.yml index ea6079e0..06cd002c 100644 --- a/src/extensions/contrib/pg_walinspect/moon.yml +++ b/src/extensions/contrib/pg_walinspect/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/pg_walinspect" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/pg_walinspect" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/pg_walinspect/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/pgcrypto/moon.yml b/src/extensions/contrib/pgcrypto/moon.yml index b35247ac..75cc03e2 100644 --- a/src/extensions/contrib/pgcrypto/moon.yml +++ b/src/extensions/contrib/pgcrypto/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/pgcrypto" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/pgcrypto" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/pgcrypto/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/seg/moon.yml b/src/extensions/contrib/seg/moon.yml index 1ebbfb73..d9b6eb98 100644 --- a/src/extensions/contrib/seg/moon.yml +++ b/src/extensions/contrib/seg/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/seg" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/seg" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/seg/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/tablefunc/moon.yml b/src/extensions/contrib/tablefunc/moon.yml index 7b1f5ebb..4f3ce7e1 100644 --- a/src/extensions/contrib/tablefunc/moon.yml +++ b/src/extensions/contrib/tablefunc/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/tablefunc" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/tablefunc" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/tablefunc/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/tcn/moon.yml b/src/extensions/contrib/tcn/moon.yml index 35af01a9..fa13a6a1 100644 --- a/src/extensions/contrib/tcn/moon.yml +++ b/src/extensions/contrib/tcn/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/tcn" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/tcn" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/tcn/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/tsm_system_rows/moon.yml b/src/extensions/contrib/tsm_system_rows/moon.yml index 5a767bd2..53a5cc38 100644 --- a/src/extensions/contrib/tsm_system_rows/moon.yml +++ b/src/extensions/contrib/tsm_system_rows/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/tsm_system_rows" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/tsm_system_rows" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/tsm_system_rows/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/tsm_system_time/moon.yml b/src/extensions/contrib/tsm_system_time/moon.yml index c2610822..eb9a8a56 100644 --- a/src/extensions/contrib/tsm_system_time/moon.yml +++ b/src/extensions/contrib/tsm_system_time/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/tsm_system_time" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/tsm_system_time" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/tsm_system_time/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/unaccent/moon.yml b/src/extensions/contrib/unaccent/moon.yml index 2de79cc7..88c87b91 100644 --- a/src/extensions/contrib/unaccent/moon.yml +++ b/src/extensions/contrib/unaccent/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/unaccent" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/unaccent" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/unaccent/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/contrib/uuid_ossp/moon.yml b/src/extensions/contrib/uuid_ossp/moon.yml index f1582d75..e4f2dfb1 100644 --- a/src/extensions/contrib/uuid_ossp/moon.yml +++ b/src/extensions/contrib/uuid_ossp/moon.yml @@ -23,14 +23,14 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/contrib/uuid_ossp" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/contrib/uuid_ossp" deps: - "extension-contrib-postgres18:check" - "extension-runtime-contract:check" inputs: - "/src/extensions/contrib/uuid_ossp/**/*" - "/src/extensions/contrib/postgres18.toml" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json b/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json index cade5524..ab42ac7a 100644 --- a/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json +++ b/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json @@ -514,7 +514,7 @@ } ], "schema": "oliphaunt-extension-evidence-v1", - "sourceDigest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8", + "sourceDigest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f", "sourceDigestInputs": [ "src/postgres/versions/18/source.toml", "src/extensions/catalog/extensions.promoted.toml", diff --git a/src/extensions/external/age/moon.yml b/src/extensions/external/age/moon.yml index 15dbb950..55014882 100644 --- a/src/extensions/external/age/moon.yml +++ b/src/extensions/external/age/moon.yml @@ -11,12 +11,12 @@ dependsOn: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/external/age" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/external/age" deps: - "extension-runtime-contract:check" inputs: - "/src/extensions/external/age/**/*" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/external/pg_hashids/moon.yml b/src/extensions/external/pg_hashids/moon.yml index a7aca9b5..0bb64bbd 100644 --- a/src/extensions/external/pg_hashids/moon.yml +++ b/src/extensions/external/pg_hashids/moon.yml @@ -20,12 +20,12 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/external/pg_hashids" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/external/pg_hashids" deps: - "extension-runtime-contract:check" inputs: - "/src/extensions/external/pg_hashids/**/*" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/external/pg_ivm/moon.yml b/src/extensions/external/pg_ivm/moon.yml index 184cebad..778949bd 100644 --- a/src/extensions/external/pg_ivm/moon.yml +++ b/src/extensions/external/pg_ivm/moon.yml @@ -20,12 +20,12 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/external/pg_ivm" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/external/pg_ivm" deps: - "extension-runtime-contract:check" inputs: - "/src/extensions/external/pg_ivm/**/*" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/external/pg_textsearch/moon.yml b/src/extensions/external/pg_textsearch/moon.yml index 91432bb8..09036e61 100644 --- a/src/extensions/external/pg_textsearch/moon.yml +++ b/src/extensions/external/pg_textsearch/moon.yml @@ -20,12 +20,12 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/external/pg_textsearch" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/external/pg_textsearch" deps: - "extension-runtime-contract:check" inputs: - "/src/extensions/external/pg_textsearch/**/*" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/external/pg_uuidv7/moon.yml b/src/extensions/external/pg_uuidv7/moon.yml index d284f098..d30dc80a 100644 --- a/src/extensions/external/pg_uuidv7/moon.yml +++ b/src/extensions/external/pg_uuidv7/moon.yml @@ -20,12 +20,12 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/external/pg_uuidv7" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/external/pg_uuidv7" deps: - "extension-runtime-contract:check" inputs: - "/src/extensions/external/pg_uuidv7/**/*" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/external/pgtap/moon.yml b/src/extensions/external/pgtap/moon.yml index ca6746ce..a406c33e 100644 --- a/src/extensions/external/pgtap/moon.yml +++ b/src/extensions/external/pgtap/moon.yml @@ -20,12 +20,12 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/external/pgtap" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/external/pgtap" deps: - "extension-runtime-contract:check" inputs: - "/src/extensions/external/pgtap/**/*" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/external/postgis/moon.yml b/src/extensions/external/postgis/moon.yml index 9839b169..e0050ee6 100644 --- a/src/extensions/external/postgis/moon.yml +++ b/src/extensions/external/postgis/moon.yml @@ -20,12 +20,12 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/external/postgis" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/external/postgis" deps: - "extension-runtime-contract:check" inputs: - "/src/extensions/external/postgis/**/*" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/external/vector/moon.yml b/src/extensions/external/vector/moon.yml index c46a0a96..2cf5aeda 100644 --- a/src/extensions/external/vector/moon.yml +++ b/src/extensions/external/vector/moon.yml @@ -20,12 +20,12 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-tree.py src/extensions/external/vector" + command: "bun src/extensions/tools/check-extension-tree.mjs src/extensions/external/vector" deps: - "extension-runtime-contract:check" inputs: - "/src/extensions/external/vector/**/*" - - "/src/extensions/tools/check-extension-tree.py" + - "/src/extensions/tools/check-extension-tree.mjs" - "/src/shared/extension-runtime-contract/**/*" options: cache: true diff --git a/src/extensions/generated/docs/extension-evidence.json b/src/extensions/generated/docs/extension-evidence.json index 3f655541..2f23ecd6 100644 --- a/src/extensions/generated/docs/extension-evidence.json +++ b/src/extensions/generated/docs/extension-evidence.json @@ -20,7 +20,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -56,7 +56,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -92,7 +92,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -128,7 +128,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -164,7 +164,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -200,7 +200,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -236,7 +236,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -272,7 +272,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -308,7 +308,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -344,7 +344,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -380,7 +380,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -416,7 +416,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -452,7 +452,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -488,7 +488,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -524,7 +524,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -560,7 +560,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -596,7 +596,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -632,7 +632,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -668,7 +668,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -704,7 +704,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -740,7 +740,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -776,7 +776,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -812,7 +812,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -848,7 +848,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -884,7 +884,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -920,7 +920,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -956,7 +956,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -992,7 +992,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -1028,7 +1028,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -1064,7 +1064,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -1100,7 +1100,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -1136,7 +1136,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -1172,7 +1172,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -1208,7 +1208,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -1244,7 +1244,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -1280,7 +1280,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -1316,7 +1316,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -1352,7 +1352,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -1388,7 +1388,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8" + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" } ], "platform-targets": [ @@ -1420,7 +1420,7 @@ "path": "src/extensions/evidence/runs" } ], - "source-digest": "sha256:61584b4aef0839d0413d0be2a3f031a64f092cf16eaf61a3d99049a6128b98e8", + "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f", "source-digest-inputs": [ "src/postgres/versions/18/source.toml", "src/extensions/catalog/extensions.promoted.toml", diff --git a/src/extensions/tools/check-extension-tree.mjs b/src/extensions/tools/check-extension-tree.mjs new file mode 100755 index 00000000..c42f4a26 --- /dev/null +++ b/src/extensions/tools/check-extension-tree.mjs @@ -0,0 +1,193 @@ +#!/usr/bin/env bun +import { existsSync, statSync } from 'node:fs'; +import { readFile, readdir } from 'node:fs/promises'; +import { basename, dirname, relative, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..'); +const EXTENSION_ARTIFACT_TARGET_SCHEMA = 'oliphaunt-extension-artifact-targets-v1'; + +function fail(message) { + console.error(`extension-tree: ${message}`); + process.exit(1); +} + +function rel(path) { + return relative(ROOT, path); +} + +function isRecord(value) { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +async function parseToml(path) { + try { + return Bun.TOML.parse(await readFile(path, 'utf8')); + } catch (error) { + const detail = error instanceof Error ? error.message : String(error); + fail(`cannot parse ${rel(path)}: ${detail}`); + } +} + +async function tomlFiles(root) { + const files = []; + async function walk(path) { + const entries = await readdir(path, { withFileTypes: true }); + for (const entry of entries) { + const child = resolve(path, entry.name); + if (entry.isDirectory()) { + await walk(child); + } else if (entry.isFile() && child.endsWith('.toml')) { + files.push(child); + } + } + } + await walk(root); + return files.sort(); +} + +async function parseAllToml(path) { + for (const tomlFile of await tomlFiles(path)) { + await parseToml(tomlFile); + } +} + +async function checkExternal(path) { + const source = resolve(path, 'source.toml'); + if (!existsSync(source)) { + fail(`${rel(path)} must own source.toml`); + } + const sourceData = await parseToml(source); + for (const key of ['name', 'url']) { + if (typeof sourceData[key] !== 'string' || sourceData[key].length === 0) { + fail(`${rel(source)} must define non-empty ${key}`); + } + } + + const release = resolve(path, 'release.toml'); + if (existsSync(release)) { + const releaseData = await parseToml(release); + if (releaseData.kind === 'exact-extension-artifact') { + const artifactTargets = resolve(path, 'targets', 'artifacts.toml'); + if (existsSync(artifactTargets)) { + await checkArtifactTargetOverride(artifactTargets); + } + } + } + + await parseAllToml(path); +} + +async function checkContrib(path) { + const manifest = resolve(path, 'postgres18.toml'); + if (!existsSync(manifest)) { + fail(`${rel(path)} must contain postgres18.toml`); + } + const data = await parseToml(manifest); + if (data['format-version'] !== 1) { + fail(`${rel(manifest)} must use format-version = 1`); + } + if (data['postgres-version'] !== '18.4') { + fail(`${rel(manifest)} must target PostgreSQL 18.4`); + } + if (data['source-kind'] !== 'postgres-contrib') { + fail(`${rel(manifest)} must describe postgres-contrib`); + } + if (!Array.isArray(data.extensions) || data.extensions.length === 0) { + fail(`${rel(manifest)} must define extension rows`); + } + await parseAllToml(path); +} + +async function contribManifestRows() { + const manifest = resolve(ROOT, 'src/extensions/contrib/postgres18.toml'); + const data = await parseToml(manifest); + const rows = data.extensions; + if (!Array.isArray(rows)) { + fail(`${rel(manifest)} must define extension rows`); + } + const parsed = new Map(); + for (const row of rows) { + if (!isRecord(row)) { + continue; + } + const extensionId = row.id; + if (typeof extensionId === 'string' && extensionId.length > 0) { + parsed.set(extensionId, row); + } + } + return parsed; +} + +async function checkArtifactProduct(path, { family }) { + const release = resolve(path, 'release.toml'); + if (!existsSync(release)) { + fail(`${rel(path)} must own release.toml`); + } + const releaseData = await parseToml(release); + if (releaseData.kind !== 'exact-extension-artifact') { + fail(`${rel(release)} must declare kind = 'exact-extension-artifact'`); + } + const sqlName = releaseData.extension_sql_name; + if (typeof sqlName !== 'string' || sqlName.length === 0) { + fail(`${rel(release)} must declare extension_sql_name`); + } + const artifactTargets = resolve(path, 'targets', 'artifacts.toml'); + if (existsSync(artifactTargets)) { + await checkArtifactTargetOverride(artifactTargets); + } + if (family === 'contrib') { + const extensionId = basename(path); + const row = (await contribManifestRows()).get(extensionId); + if (row === undefined) { + fail(`${rel(path)} must match a row in src/extensions/contrib/postgres18.toml`); + } + if (row['sql-name'] !== sqlName) { + fail( + `${rel(release)} extension_sql_name ${JSON.stringify(sqlName)} ` + + `must match contrib manifest sql-name ${JSON.stringify(row['sql-name'])}`, + ); + } + } + await parseAllToml(path); +} + +async function checkArtifactTargetOverride(artifactTargets) { + const targetData = await parseToml(artifactTargets); + if (targetData.schema !== EXTENSION_ARTIFACT_TARGET_SCHEMA) { + fail(`${rel(artifactTargets)} must use schema = ${JSON.stringify(EXTENSION_ARTIFACT_TARGET_SCHEMA)}`); + } + if (!Array.isArray(targetData.targets) || targetData.targets.length === 0) { + fail(`${rel(artifactTargets)} must define [[targets]] rows`); + } +} + +async function main(argv) { + if (argv.length !== 1) { + fail('usage: check-extension-tree.mjs }>'); + } + const path = resolve(ROOT, argv[0]); + const relativePath = rel(path); + if (relativePath.startsWith('..') || relativePath === '') { + fail(`path is outside repository: ${path}`); + } + if (!existsSync(path) || !statSync(path).isDirectory()) { + fail(`path does not exist: ${relativePath}`); + } + + if (path === resolve(ROOT, 'src/extensions/contrib')) { + await checkContrib(path); + } else if (dirname(path) === resolve(ROOT, 'src/extensions/contrib')) { + await checkArtifactProduct(path, { family: 'contrib' }); + } else if (dirname(path) === resolve(ROOT, 'src/extensions/external')) { + await checkExternal(path); + const release = resolve(path, 'release.toml'); + if (existsSync(release) && (await parseToml(release)).kind === 'exact-extension-artifact') { + await checkArtifactProduct(path, { family: 'external' }); + } + } else { + fail(`unsupported extension tree path: ${relativePath}`); + } +} + +await main(Bun.argv.slice(2)); diff --git a/src/extensions/tools/check-extension-tree.py b/src/extensions/tools/check-extension-tree.py deleted file mode 100644 index e06f732b..00000000 --- a/src/extensions/tools/check-extension-tree.py +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import pathlib -import sys -import tomllib - - -ROOT = pathlib.Path(__file__).resolve().parents[3] -EXTENSION_ARTIFACT_TARGET_SCHEMA = "oliphaunt-extension-artifact-targets-v1" - - -def fail(message: str) -> None: - raise SystemExit(f"extension-tree: {message}") - - -def parse_toml(path: pathlib.Path) -> object: - try: - return tomllib.loads(path.read_text(encoding="utf-8")) - except Exception as error: - fail(f"cannot parse {path.relative_to(ROOT)}: {error}") - - -def check_external(path: pathlib.Path) -> None: - source = path / "source.toml" - if not source.is_file(): - fail(f"{path.relative_to(ROOT)} must own source.toml") - source_data = parse_toml(source) - for key in ("name", "url"): - if not isinstance(source_data.get(key), str) or not source_data[key]: - fail(f"{source.relative_to(ROOT)} must define non-empty {key}") - - release = path / "release.toml" - if release.is_file(): - release_data = parse_toml(release) - if release_data.get("kind") == "exact-extension-artifact": - artifact_targets = path / "targets" / "artifacts.toml" - if artifact_targets.is_file(): - check_artifact_target_override(artifact_targets) - - for toml_file in sorted(path.rglob("*.toml")): - parse_toml(toml_file) - - -def check_contrib(path: pathlib.Path) -> None: - manifest = path / "postgres18.toml" - if not manifest.is_file(): - fail(f"{path.relative_to(ROOT)} must contain postgres18.toml") - data = parse_toml(manifest) - if data.get("format-version") != 1: - fail(f"{manifest.relative_to(ROOT)} must use format-version = 1") - if data.get("postgres-version") != "18.4": - fail(f"{manifest.relative_to(ROOT)} must target PostgreSQL 18.4") - if data.get("source-kind") != "postgres-contrib": - fail(f"{manifest.relative_to(ROOT)} must describe postgres-contrib") - if not isinstance(data.get("extensions"), list) or not data["extensions"]: - fail(f"{manifest.relative_to(ROOT)} must define extension rows") - for toml_file in sorted(path.rglob("*.toml")): - parse_toml(toml_file) - - -def contrib_manifest_rows() -> dict[str, dict]: - manifest = ROOT / "src/extensions/contrib/postgres18.toml" - data = parse_toml(manifest) - rows = data.get("extensions") - if not isinstance(rows, list): - fail(f"{manifest.relative_to(ROOT)} must define extension rows") - parsed: dict[str, dict] = {} - for row in rows: - if not isinstance(row, dict): - continue - extension_id = row.get("id") - if isinstance(extension_id, str) and extension_id: - parsed[extension_id] = row - return parsed - - -def check_artifact_product(path: pathlib.Path, *, family: str) -> None: - release = path / "release.toml" - if not release.is_file(): - fail(f"{path.relative_to(ROOT)} must own release.toml") - release_data = parse_toml(release) - if release_data.get("kind") != "exact-extension-artifact": - fail(f"{release.relative_to(ROOT)} must declare kind = 'exact-extension-artifact'") - sql_name = release_data.get("extension_sql_name") - if not isinstance(sql_name, str) or not sql_name: - fail(f"{release.relative_to(ROOT)} must declare extension_sql_name") - artifact_targets = path / "targets" / "artifacts.toml" - if artifact_targets.is_file(): - check_artifact_target_override(artifact_targets) - if family == "contrib": - extension_id = path.name - row = contrib_manifest_rows().get(extension_id) - if row is None: - fail(f"{path.relative_to(ROOT)} must match a row in src/extensions/contrib/postgres18.toml") - if row.get("sql-name") != sql_name: - fail( - f"{release.relative_to(ROOT)} extension_sql_name {sql_name!r} " - f"must match contrib manifest sql-name {row.get('sql-name')!r}" - ) - for toml_file in sorted(path.rglob("*.toml")): - parse_toml(toml_file) - - -def check_artifact_target_override(artifact_targets: pathlib.Path) -> None: - target_data = parse_toml(artifact_targets) - if target_data.get("schema") != EXTENSION_ARTIFACT_TARGET_SCHEMA: - fail( - f"{artifact_targets.relative_to(ROOT)} must use schema = " - f"{EXTENSION_ARTIFACT_TARGET_SCHEMA!r}" - ) - if not isinstance(target_data.get("targets"), list) or not target_data["targets"]: - fail(f"{artifact_targets.relative_to(ROOT)} must define [[targets]] rows") - - -def main(argv: list[str]) -> None: - if len(argv) != 2: - fail("usage: check-extension-tree.py }>") - path = (ROOT / argv[1]).resolve() - try: - path.relative_to(ROOT) - except ValueError: - fail(f"path is outside repository: {path}") - if not path.is_dir(): - fail(f"path does not exist: {path.relative_to(ROOT)}") - if path == ROOT / "src/extensions/contrib": - check_contrib(path) - elif path.parent == ROOT / "src/extensions/contrib": - check_artifact_product(path, family="contrib") - elif path.parent == ROOT / "src/extensions/external": - check_external(path) - release = path / "release.toml" - if release.is_file() and parse_toml(release).get("kind") == "exact-extension-artifact": - check_artifact_product(path, family="external") - else: - fail(f"unsupported extension tree path: {path.relative_to(ROOT)}") - - -if __name__ == "__main__": - main(sys.argv) diff --git a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 index 01901ecc..8bab979e 100644 --- a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 +++ b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 @@ -1 +1 @@ -21885820e26443b452e9ebb46ea5bfdb9f904b1f0a4b26fc552667603be07ee5 +791b1fa125476447c37e6dca2836e760700efbf922bc320c754bdc752063d279 diff --git a/src/shared/extension-runtime-contract/tools/check-contract.mjs b/src/shared/extension-runtime-contract/tools/check-contract.mjs old mode 100755 new mode 100644 diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index a556545e..f2ca7a21 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -185,6 +185,17 @@ grep -Fq 'bun src/shared/extension-runtime-contract/tools/check-contract.mjs' sr if [ -e src/shared/extension-runtime-contract/tools/check-contract.py ]; then fail "extension runtime contract checker must not use the retired Python implementation" fi +if [ -e src/extensions/tools/check-extension-tree.py ]; then + fail "extension tree checker must not use the retired Python implementation" +fi +if git grep -n 'check-extension-tree\.py' -- src/extensions >/tmp/oliphaunt-extension-tree-python-grep.$$ 2>/dev/null; then + cat /tmp/oliphaunt-extension-tree-python-grep.$$ >&2 + rm -f /tmp/oliphaunt-extension-tree-python-grep.$$ + fail "extension Moon tasks must use the Bun extension tree checker" +fi +rm -f /tmp/oliphaunt-extension-tree-python-grep.$$ +grep -Fq 'bun src/extensions/tools/check-extension-tree.mjs' src/extensions/contrib/moon.yml || + fail "contrib extension aggregate check must use the Bun extension tree checker" for retired_source_input_checker in tools/policy/check-source-inputs.sh tools/policy/check-source-inputs.mjs; do if git ls-files --error-unmatch "$retired_source_input_checker" >/dev/null 2>&1; then fail "source-input policy parsers must live under tools/policy/assertions/assert-*.mjs" From 223cd075ab07cab50a207ad91741150038de6b57 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 12:20:50 +0000 Subject: [PATCH 079/137] fix: install example deps from local registries --- docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 7 +++++++ examples/tools/check-examples.sh | 2 ++ examples/tools/run-electron-driver-smoke.sh | 1 + examples/tools/run-tauri-webdriver-smoke.sh | 1 + 4 files changed, 11 insertions(+) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index c2fe8748..7dd325cd 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -399,3 +399,10 @@ review production pipelines, then normalize implementation details. release metadata, consumer package shapes, workflow wiring, artifact target derivation, and WASIX registry dependency graph are aligned with the intended Cargo, npm, Maven, SwiftPM, and GitHub release surfaces. +- On 2026-06-26, the example GUI smoke wrappers were tightened to run a + filtered `pnpm install` through `examples/tools/with-local-registries.sh` + before building each Electron/Tauri app. The four GUI smokes passed after + this change (`examples/electron`, `examples/electron-wasix`, + `examples/tauri`, and `examples/tauri-wasix`), and the nested WASIX SQLx + profiler passed with a report containing the `validate split WASIX tools` + startup phase. diff --git a/examples/tools/check-examples.sh b/examples/tools/check-examples.sh index 8252a3a0..1010d98c 100755 --- a/examples/tools/check-examples.sh +++ b/examples/tools/check-examples.sh @@ -89,6 +89,8 @@ require_file "examples/tools/run-electron-driver-smoke.sh" require_file "examples/tools/electron-driver-smoke.mjs" require_file "examples/tools/electron-test-driver.mjs" require_text "examples/tools/run-tauri-webdriver-smoke.sh" 'cargo install tauri-driver --locked --version 2\.0\.6' +require_text "examples/tools/run-tauri-webdriver-smoke.sh" 'pnpm --filter "\./\$app_dir" install --no-frozen-lockfile' +require_text "examples/tools/run-electron-driver-smoke.sh" 'pnpm --filter "\./\$app_dir" install --no-frozen-lockfile' require_text "examples/tools/tauri-webdriver-smoke.mjs" 'tauri webdriver todo smoke passed' require_text "examples/tools/electron-driver-smoke.mjs" 'electron driver todo smoke passed' require_text "examples/tools/electron-test-driver.mjs" 'installElectronTodoTestDriver' diff --git a/examples/tools/run-electron-driver-smoke.sh b/examples/tools/run-electron-driver-smoke.sh index 1880509d..b934786a 100755 --- a/examples/tools/run-electron-driver-smoke.sh +++ b/examples/tools/run-electron-driver-smoke.sh @@ -28,6 +28,7 @@ if [ ! -x "$electron" ]; then fail "missing Electron executable at $electron; run pnpm install" fi +examples/tools/with-local-registries.sh pnpm --filter "./$app_dir" install --no-frozen-lockfile examples/tools/with-local-registries.sh pnpm --dir "$app_dir" build run_smoke=( diff --git a/examples/tools/run-tauri-webdriver-smoke.sh b/examples/tools/run-tauri-webdriver-smoke.sh index 8d046b0e..88691494 100755 --- a/examples/tools/run-tauri-webdriver-smoke.sh +++ b/examples/tools/run-tauri-webdriver-smoke.sh @@ -30,6 +30,7 @@ if [ ! -x "$driver" ]; then cargo install tauri-driver --locked --version 2.0.6 --root "$root/target/e2e-tools" fi +examples/tools/with-local-registries.sh pnpm --filter "./$app_dir" install --no-frozen-lockfile examples/tools/with-local-registries.sh pnpm --dir "$app_dir" tauri build --debug package_name="$( From cb2980d4ce3762fa02691f491a6484ddaa12a7c3 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 12:35:42 +0000 Subject: [PATCH 080/137] test: guard split tool runtime artifacts --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 5 ++ .../rust/crates/oliphaunt-build/src/lib.rs | 65 +++++++++++++++++++ tools/policy/check-sdk-parity.sh | 14 ++++ tools/policy/sdk-check-lib.sh | 11 ++++ 4 files changed, 95 insertions(+) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 7dd325cd..5627300a 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -406,3 +406,8 @@ review production pipelines, then normalize implementation details. `examples/tauri`, and `examples/tauri-wasix`), and the nested WASIX SQLx profiler passed with a report containing the `validate split WASIX tools` startup phase. +- On 2026-06-26, the SDK parity guard was tightened so Swift, Kotlin + Android/common, and React Native source trees reject accidental standalone + `pg_dump` or `psql` APIs. This keeps mobile native-direct/delegating SDKs + aligned with the parity matrix: desktop Rust and TypeScript own split client + tool package access, while mobile SDKs consume runtime resources only. diff --git a/src/sdks/rust/crates/oliphaunt-build/src/lib.rs b/src/sdks/rust/crates/oliphaunt-build/src/lib.rs index 5dedfb6f..be092c8f 100644 --- a/src/sdks/rust/crates/oliphaunt-build/src/lib.rs +++ b/src/sdks/rust/crates/oliphaunt-build/src/lib.rs @@ -1362,6 +1362,71 @@ runtime-version = "0.1.0" assert!(error.to_string().contains("runtime/bin/psql")); } + #[test] + fn artifact_manifest_rejects_native_runtime_client_tool_payloads() { + for tool in ["runtime/bin/pg_dump", "runtime/bin/psql"] { + let temp = app_with_metadata(""); + let runtime_manifest = write_artifact_manifest_with_relatives( + &temp, + "runtime.toml", + "liboliphaunt-native", + "0.1.0", + "native-runtime", + "x86_64-unknown-linux-gnu", + None, + &[ + "runtime/bin/postgres", + "runtime/bin/initdb", + "runtime/bin/pg_ctl", + tool, + ], + ); + let context = BuildContext { + manifest_dir: temp.path().to_path_buf(), + out_dir: temp.path().join("out"), + target: "x86_64-unknown-linux-gnu".to_owned(), + artifact_manifest_paths: vec![runtime_manifest], + }; + + let error = context + .read_artifact_manifests() + .expect_err("native runtime must not contain split client tools"); + + assert!(error.to_string().contains("must not contain payload")); + assert!(error.to_string().contains(tool)); + } + } + + #[test] + fn artifact_manifest_rejects_wasix_runtime_client_tool_payloads() { + for tool in ["bin/pg_dump.wasix.wasm", "bin/psql.wasix.wasm"] { + let temp = app_with_metadata(""); + let runtime_manifest = write_artifact_manifest_with_relatives( + &temp, + "wasix-runtime.toml", + "liboliphaunt-wasix", + "0.1.0", + "wasix-runtime", + "portable", + None, + &["oliphaunt.wasix.tar.zst", "bin/initdb.wasix.wasm", tool], + ); + let context = BuildContext { + manifest_dir: temp.path().to_path_buf(), + out_dir: temp.path().join("out"), + target: "wasm32-wasip1".to_owned(), + artifact_manifest_paths: vec![runtime_manifest], + }; + + let error = context + .read_artifact_manifests() + .expect_err("WASIX runtime must not contain split client tools"); + + assert!(error.to_string().contains("must not contain payload")); + assert!(error.to_string().contains(tool)); + } + } + #[test] fn artifact_manifest_rejects_wasix_pg_ctl_tool_payload() { let temp = app_with_metadata(""); diff --git a/tools/policy/check-sdk-parity.sh b/tools/policy/check-sdk-parity.sh index 65d564f5..5899d97f 100755 --- a/tools/policy/check-sdk-parity.sh +++ b/tools/policy/check-sdk-parity.sh @@ -184,6 +184,20 @@ require_manifest_text react-native 'extension_resolution = "delegated-exact-exte "SDK manifest must declare React Native delegated exact-extension resolution" require_manifest_text react-native 'resource_override = "runtimeDirectory-resourceRoot"' \ "SDK manifest must declare React Native's delegated local runtime-resource overrides" +for mobile_tool in pg_dump psql; do + reject_tree_text src/sdks/swift/Sources "$mobile_tool" \ + "Swift native-direct must not expose standalone PostgreSQL client tools; desktop tool access belongs to Rust/TypeScript split tool packages" + reject_tree_text src/sdks/kotlin/oliphaunt/src/commonMain "$mobile_tool" \ + "Kotlin common SDK must not expose standalone PostgreSQL client tools; Android native-direct has no mobile tool runtime" + reject_tree_text src/sdks/kotlin/oliphaunt/src/androidMain "$mobile_tool" \ + "Kotlin Android native-direct must not expose standalone PostgreSQL client tools; Android package resources are runtime-only" + reject_tree_text src/sdks/react-native/src "$mobile_tool" \ + "React Native must not expose a separate standalone PostgreSQL tool API; tool behavior is delegated to platform SDK capabilities" + reject_tree_text src/sdks/react-native/ios "$mobile_tool" \ + "React Native iOS must not grow a standalone PostgreSQL tool runtime; runtime behavior delegates to Swift" + reject_tree_text src/sdks/react-native/android/src/main "$mobile_tool" \ + "React Native Android must not grow a standalone PostgreSQL tool runtime; runtime behavior delegates to Kotlin" +done require_manifest_text typescript 'classification = "sdk"' \ "SDK manifest must classify TypeScript as an SDK" require_manifest_text typescript 'package_name = "@oliphaunt/ts"' \ diff --git a/tools/policy/sdk-check-lib.sh b/tools/policy/sdk-check-lib.sh index 3aef2175..534b15c0 100755 --- a/tools/policy/sdk-check-lib.sh +++ b/tools/policy/sdk-check-lib.sh @@ -94,3 +94,14 @@ reject_text() { exit 1 fi } + +reject_tree_text() { + path="$1" + text="$2" + message="$3" + if [ -e "$path" ] && rg -n --fixed-strings -- "$text" "$path" >&2; then + echo "$message" >&2 + echo "unexpected '$text' under $path" >&2 + exit 1 + fi +} From db9dfb08d80abe4b977b364f1c92a3456d0b58d6 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 12:38:28 +0000 Subject: [PATCH 081/137] test: compile wasix split tools feature --- docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 5 +++++ src/bindings/wasix-rust/tools/check-unit.sh | 3 +++ tools/policy/check-rust-test-topology.sh | 2 ++ tools/policy/check-test-strategy.mjs | 1 + 4 files changed, 11 insertions(+) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 5627300a..3b3a46af 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -411,3 +411,8 @@ review production pipelines, then normalize implementation details. `pg_dump` or `psql` APIs. This keeps mobile native-direct/delegating SDKs aligned with the parity matrix: desktop Rust and TypeScript own split client tool package access, while mobile SDKs consume runtime resources only. +- On 2026-06-26, the WASIX Rust product test wrapper was tightened to compile + the `extensions,tools` feature path for the split-tools preflight test without + requiring generated runtime assets in the unit lane. The full runtime-smoke + lane remains responsible for executing `pg_dump` and `psql` once assets are + available. diff --git a/src/bindings/wasix-rust/tools/check-unit.sh b/src/bindings/wasix-rust/tools/check-unit.sh index d14aa2b5..90f5dd8f 100755 --- a/src/bindings/wasix-rust/tools/check-unit.sh +++ b/src/bindings/wasix-rust/tools/check-unit.sh @@ -17,3 +17,6 @@ cargo test -p oliphaunt-wasix --doc --locked printf '\n==> cargo nextest run -p oliphaunt-wasix --locked --profile ci --no-default-features --lib --no-tests=fail --test-threads=1\n' cargo nextest run -p oliphaunt-wasix --locked --profile ci --no-default-features --lib --no-tests=fail --test-threads=1 + +printf '\n==> cargo test -p oliphaunt-wasix --locked --no-default-features --features extensions,tools --lib preflight_wasix_tools_loads_split_artifacts --no-run\n' +cargo test -p oliphaunt-wasix --locked --no-default-features --features extensions,tools --lib preflight_wasix_tools_loads_split_artifacts --no-run diff --git a/tools/policy/check-rust-test-topology.sh b/tools/policy/check-rust-test-topology.sh index 9a0bcc7d..a92336cb 100755 --- a/tools/policy/check-rust-test-topology.sh +++ b/tools/policy/check-rust-test-topology.sh @@ -42,6 +42,8 @@ require_text src/bindings/wasix-rust/tools/check-unit.sh 'cargo test -p oliphaun "WASIX Rust doctests must run in the WASIX Rust product test task" require_text src/bindings/wasix-rust/tools/check-unit.sh 'cargo nextest run -p oliphaunt-wasix --locked --profile ci --no-default-features --lib --no-tests=fail --test-threads=1' \ "WASIX Rust unit tests must run through cargo-nextest in the WASIX Rust product test task" +require_text src/bindings/wasix-rust/tools/check-unit.sh 'cargo test -p oliphaunt-wasix --locked --no-default-features --features extensions,tools --lib preflight_wasix_tools_loads_split_artifacts --no-run' \ + "WASIX Rust product test task must compile the split tools feature path without requiring generated runtime assets" require_text src/runtimes/broker/moon.yml 'command: "cargo test -p oliphaunt-broker --locked"' \ "Broker runtime tests must be owned by the broker runtime product task" require_text tools/xtask/moon.yml 'template-runner-check:' \ diff --git a/tools/policy/check-test-strategy.mjs b/tools/policy/check-test-strategy.mjs index b49a98a5..8d3b40e6 100755 --- a/tools/policy/check-test-strategy.mjs +++ b/tools/policy/check-test-strategy.mjs @@ -476,6 +476,7 @@ if (wasmTestCommand !== 'bash src/bindings/wasix-rust/tools/check-unit.sh') { } requireText('src/bindings/wasix-rust/tools/check-unit.sh', 'cargo test -p oliphaunt-wasix --doc --locked'); requireText('src/bindings/wasix-rust/tools/check-unit.sh', 'cargo nextest run -p oliphaunt-wasix --locked --profile ci --no-default-features --lib --no-tests=fail --test-threads=1'); +requireText('src/bindings/wasix-rust/tools/check-unit.sh', 'cargo test -p oliphaunt-wasix --locked --no-default-features --features extensions,tools --lib preflight_wasix_tools_loads_split_artifacts --no-run'); if (!taskCommand(tasks, 'liboliphaunt-wasix', 'regression').includes('runtime-smoke.sh regression')) { fail('liboliphaunt-wasix:regression must use the full regression runtime-smoke mode'); } From 1a11a9bbf608411eba988186dc0f9506433b0113 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 12:43:55 +0000 Subject: [PATCH 082/137] chore: guard python tooling inventory --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 8 ++ tools/policy/check-python-entrypoints.mjs | 81 +++++++++++++++++++ tools/policy/check-tooling-stack.sh | 4 + tools/policy/python-entrypoints.allowlist | 45 +++++++++++ 4 files changed, 138 insertions(+) create mode 100644 tools/policy/check-python-entrypoints.mjs create mode 100644 tools/policy/python-entrypoints.allowlist diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 3b3a46af..c7dbfd16 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -256,6 +256,14 @@ review production pipelines, then normalize implementation details. `tools/release/archive_dir.mjs` helper for release asset tar/zip creation and shell `tar` for npm package membership checks, removing inline Python from that packaging script while keeping the existing release validators intact. +- The remaining tracked Python files are now an explicit policy inventory in + `tools/policy/python-entrypoints.allowlist`, checked by + `bun tools/policy/check-python-entrypoints.mjs` from `check-tooling-stack.sh`. + That inventory currently contains release orchestration/package validators, + graph/coverage helpers, extension model checks, runtime lock helpers, and + release fixture builders. New Python files must either be intentionally + allowlisted or ported to Bun. The Rust-helper review and per-script migration + decisions remain open. - CI/release producer-to-consumer audit found no P0/P1 mapping gaps across Cargo, npm, Maven, SwiftPM, or GitHub release assets. Existing `release.py check`, artifact-target, release-metadata, consumer-shape, and diff --git a/tools/policy/check-python-entrypoints.mjs b/tools/policy/check-python-entrypoints.mjs new file mode 100644 index 00000000..5bd6488e --- /dev/null +++ b/tools/policy/check-python-entrypoints.mjs @@ -0,0 +1,81 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; +import { readFileSync } from "node:fs"; + +const ALLOWLIST = "tools/policy/python-entrypoints.allowlist"; +const PYTHON_PATHSPEC = ":(glob)**/*.py"; + +function fail(message) { + console.error(`check-python-entrypoints.mjs: ${message}`); + process.exit(1); +} + +function gitLsFiles(pathspec) { + const result = spawnSync("git", ["ls-files", "-z", "--", pathspec], { + encoding: "buffer", + }); + if (result.status !== 0) { + fail(result.stderr.toString("utf8").trim() || "git ls-files failed"); + } + return result.stdout + .toString("utf8") + .split("\0") + .filter(Boolean) + .sort(); +} + +function parseAllowlist() { + const text = readFileSync(ALLOWLIST, "utf8"); + const entries = []; + for (const [index, rawLine] of text.split(/\r?\n/).entries()) { + const line = rawLine.trim(); + if (!line || line.startsWith("#")) { + continue; + } + if (line.startsWith("/") || line.includes("..") || !line.endsWith(".py")) { + fail(`${ALLOWLIST}:${index + 1} is not a repo-relative Python path: ${line}`); + } + entries.push(line); + } + return entries; +} + +function assertSortedUnique(entries) { + const sorted = [...entries].sort(); + const sortedText = sorted.join("\n"); + if (entries.join("\n") !== sortedText) { + fail(`${ALLOWLIST} must be sorted lexicographically`); + } + for (let index = 1; index < entries.length; index += 1) { + if (entries[index] === entries[index - 1]) { + fail(`${ALLOWLIST} contains duplicate entry: ${entries[index]}`); + } + } +} + +const trackedPython = gitLsFiles(PYTHON_PATHSPEC); +const allowlistedPython = parseAllowlist(); +assertSortedUnique(allowlistedPython); + +const tracked = new Set(trackedPython); +const allowed = new Set(allowlistedPython); +const missing = trackedPython.filter((path) => !allowed.has(path)); +const stale = allowlistedPython.filter((path) => !tracked.has(path)); + +if (missing.length > 0 || stale.length > 0) { + if (missing.length > 0) { + console.error("tracked Python files missing from the intentional inventory:"); + for (const path of missing) { + console.error(` ${path}`); + } + } + if (stale.length > 0) { + console.error("stale Python inventory entries:"); + for (const path of stale) { + console.error(` ${path}`); + } + } + fail("update the inventory or port the Python file to Bun"); +} + +console.log(`Python entrypoint inventory verified (${trackedPython.length} tracked files).`); diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index f2ca7a21..ac19b0d5 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -38,6 +38,8 @@ require_file docs/maintainers/tooling.md require_file tools/test/moon.yml require_file tools/test/run-js-tests.mjs require_file tools/graph/cache-witness.mjs +require_file tools/policy/check-python-entrypoints.mjs +require_file tools/policy/python-entrypoints.allowlist require_file tools/runtime/preflight.sh require_file tools/dev/bun.sh require_file tools/dev/deno.sh @@ -238,6 +240,8 @@ grep -Fq 'RIPGREP_VERSION="${RIPGREP_VERSION:-15.1.0}"' tools/dev/bootstrap-tool fail "local tool bootstrap must pin ripgrep" grep -Fq 'install_cargo_tool ripgrep rg "$RIPGREP_VERSION"' tools/dev/bootstrap-tools.sh || fail "local tool bootstrap must install the pinned ripgrep binary" + +bun tools/policy/check-python-entrypoints.mjs if grep -Fq 'python3' tools/dev/bootstrap-tools.sh; then fail "local tool bootstrap must not use Python for archive extraction" fi diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist new file mode 100644 index 00000000..81119ded --- /dev/null +++ b/tools/policy/python-entrypoints.allowlist @@ -0,0 +1,45 @@ +# Intentional Python tooling inventory. +# New Python files should be ported to Bun or deliberately added here. +src/extensions/tools/check-extension-model.py +src/shared/contracts/tools/check-test-matrix.py +tools/coverage/coverage.py +tools/graph/affected.py +tools/graph/ci_plan.py +tools/graph/graph.py +tools/policy/check-final-source-architecture.py +tools/policy/check-release-policy.py +tools/release/artifact_target_matrix.py +tools/release/artifact_targets.py +tools/release/build-extension-ci-artifacts.py +tools/release/build_maven_artifact_manifest.py +tools/release/check_artifact_targets.py +tools/release/check_broker_release_assets.py +tools/release/check_consumer_shape.py +tools/release/check_cratesio_publication.py +tools/release/check_github_release_assets.py +tools/release/check_liboliphaunt_release_assets.py +tools/release/check_node_direct_release_assets.py +tools/release/check_registry_publication.py +tools/release/check_release_metadata.py +tools/release/check_release_pr_coverage.py +tools/release/check_release_versions.py +tools/release/check_staged_artifacts.py +tools/release/extension_artifact_targets.py +tools/release/local_registry_publish.py +tools/release/optimize_native_runtime_payload.py +tools/release/package_broker_cargo_artifacts.py +tools/release/package_liboliphaunt_cargo_artifacts.py +tools/release/package_liboliphaunt_wasix_cargo_artifacts.py +tools/release/product_metadata.py +tools/release/publish_swiftpm_source_tag.py +tools/release/release.py +tools/release/release_plan.py +tools/release/render_swiftpm_release_package.py +tools/release/strip_native_release_binaries.py +tools/release/sync_release_pr.py +tools/release/upload_github_release_assets.py +tools/release/verify_github_release_attestations.py +tools/runtime/with-native-runtime-lock.py +tools/test/create-broker-release-fixture.py +tools/test/create-liboliphaunt-release-fixture.py +tools/test/release_fixture_utils.py From 548553097b8b26c383f2693b371db335a91cbda4 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 12:45:07 +0000 Subject: [PATCH 083/137] docs: record rust tooling inventory --- docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index c7dbfd16..281260e2 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -264,6 +264,13 @@ review production pipelines, then normalize implementation details. release fixture builders. New Python files must either be intentionally allowlisted or ported to Bun. The Rust-helper review and per-script migration decisions remain open. +- Rust helper inventory is currently limited to `tools/xtask` and + `tools/perf/runner`. Both remain Rust-owned for now: `xtask` owns WASIX asset + parsing, archive/hash work, AOT/template feature-gated paths, and release + workspace assembly; `tools/perf/runner` links the Rust SDK/runtime code and + database clients for benchmark controls. Future Bun migration should target + individual release/policy orchestration scripts first, not these Rust crates + wholesale. - CI/release producer-to-consumer audit found no P0/P1 mapping gaps across Cargo, npm, Maven, SwiftPM, or GitHub release assets. Existing `release.py check`, artifact-target, release-metadata, consumer-shape, and From 49a0397e9cc59883f7e34f363e2c34e0c1397de5 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 12:51:58 +0000 Subject: [PATCH 084/137] chore: port release fixture generators to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 13 +- src/sdks/rust/tools/check-sdk.sh | 6 +- tools/policy/python-entrypoints.allowlist | 3 - tools/release/archive_dir.mjs | 3 + tools/release/check_release_metadata.py | 8 +- tools/test/create-broker-release-fixture.mjs | 51 ++++ tools/test/create-broker-release-fixture.py | 57 ---- .../create-liboliphaunt-release-fixture.mjs | 248 ++++++++++++++++++ .../create-liboliphaunt-release-fixture.py | 227 ---------------- tools/test/moon.yml | 2 +- tools/test/release-fixture-utils.mjs | 74 ++++++ tools/test/release_fixture_utils.py | 50 ---- 12 files changed, 393 insertions(+), 349 deletions(-) create mode 100644 tools/test/create-broker-release-fixture.mjs delete mode 100644 tools/test/create-broker-release-fixture.py create mode 100644 tools/test/create-liboliphaunt-release-fixture.mjs delete mode 100644 tools/test/create-liboliphaunt-release-fixture.py create mode 100644 tools/test/release-fixture-utils.mjs delete mode 100644 tools/test/release_fixture_utils.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 281260e2..0c7adead 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -260,10 +260,15 @@ review production pipelines, then normalize implementation details. `tools/policy/python-entrypoints.allowlist`, checked by `bun tools/policy/check-python-entrypoints.mjs` from `check-tooling-stack.sh`. That inventory currently contains release orchestration/package validators, - graph/coverage helpers, extension model checks, runtime lock helpers, and - release fixture builders. New Python files must either be intentionally - allowlisted or ported to Bun. The Rust-helper review and per-script migration - decisions remain open. + graph/coverage helpers, extension model checks, and runtime lock helpers. New + Python files must either be intentionally allowlisted or ported to Bun. The + per-Python-script migration decisions remain open. +- Rust SDK release-shaped fixture generation now uses Bun instead of Python. + `tools/test/create-liboliphaunt-release-fixture.mjs` and + `tools/test/create-broker-release-fixture.mjs` stage the same fixture + layouts and call the shared deterministic `tools/release/archive_dir.mjs` + helper for tar.gz/zip output. The retired Python fixture generators and + shared Python utility were removed from the Python inventory. - Rust helper inventory is currently limited to `tools/xtask` and `tools/perf/runner`. Both remain Rust-owned for now: `xtask` owns WASIX asset parsing, archive/hash work, AOT/template feature-gated paths, and release diff --git a/src/sdks/rust/tools/check-sdk.sh b/src/sdks/rust/tools/check-sdk.sh index 95521be8..f40ba72e 100755 --- a/src/sdks/rust/tools/check-sdk.sh +++ b/src/sdks/rust/tools/check-sdk.sh @@ -87,7 +87,7 @@ check_release_asset_fixture() { fixture_cache="$(prepare_scratch_dir liboliphaunt-release-cache)" fixture_output="$(prepare_scratch_dir liboliphaunt-release-output)" fixture_log="$scratch_base/$mode/liboliphaunt-release-assets.log" - run python3 tools/test/create-liboliphaunt-release-fixture.py \ + run bun tools/test/create-liboliphaunt-release-fixture.mjs \ --asset-dir "$fixture_assets" \ --version "$liboliphaunt_version" run cargo run -p oliphaunt --bin oliphaunt-resources --locked -- \ @@ -115,7 +115,7 @@ check_broker_release_asset_fixture() { fixture_cache="$(prepare_scratch_dir broker-release-cache)" fixture_output="$(prepare_scratch_dir broker-release-output)" fixture_log="$scratch_base/$mode/broker-release-assets.log" - run python3 tools/test/create-broker-release-fixture.py \ + run bun tools/test/create-broker-release-fixture.mjs \ --asset-dir "$fixture_assets" \ --version "$broker_version" run cargo run -p oliphaunt --bin oliphaunt-resources --locked -- \ @@ -163,7 +163,7 @@ check_broker_cargo_relay_fixture() { liboliphaunt_version="$(cat src/runtimes/liboliphaunt/native/VERSION)" liboliphaunt_fixture_assets="$(prepare_scratch_dir liboliphaunt-cargo-release-assets)" liboliphaunt_cargo_artifacts="$(prepare_scratch_dir liboliphaunt-cargo-artifacts)" - run python3 tools/test/create-liboliphaunt-release-fixture.py \ + run bun tools/test/create-liboliphaunt-release-fixture.mjs \ --asset-dir "$liboliphaunt_fixture_assets" \ --version "$liboliphaunt_version" run python3 tools/release/package_liboliphaunt_cargo_artifacts.py \ diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index 81119ded..a4efc676 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -40,6 +40,3 @@ tools/release/sync_release_pr.py tools/release/upload_github_release_assets.py tools/release/verify_github_release_attestations.py tools/runtime/with-native-runtime-lock.py -tools/test/create-broker-release-fixture.py -tools/test/create-liboliphaunt-release-fixture.py -tools/test/release_fixture_utils.py diff --git a/tools/release/archive_dir.mjs b/tools/release/archive_dir.mjs index b6338e40..7755ab37 100755 --- a/tools/release/archive_dir.mjs +++ b/tools/release/archive_dir.mjs @@ -182,6 +182,9 @@ async function createZip(root) { const { time, date } = dosDateTime(); for (const entry of await archiveEntries(root)) { + if (entry.name === '.') { + continue; + } const stat = await fs.stat(entry.fullPath); const mode = normalizedMode(stat, entry.isDirectory); const name = Buffer.from(zipName(entry)); diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index cad73afb..9e957674 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -397,8 +397,8 @@ def validate_rust() -> None: ) require_text( "src/sdks/rust/tools/check-sdk.sh", - "create-liboliphaunt-release-fixture.py", - "Rust SDK package check must use deterministic release-shaped liboliphaunt asset fixtures", + "create-liboliphaunt-release-fixture.mjs", + "Rust SDK package check must use deterministic Bun release-shaped liboliphaunt asset fixtures", ) require_text( "src/sdks/rust/tools/check-sdk.sh", @@ -407,8 +407,8 @@ def validate_rust() -> None: ) require_text( "src/sdks/rust/tools/check-sdk.sh", - "create-broker-release-fixture.py", - "Rust SDK package check must use deterministic release-shaped broker asset fixtures", + "create-broker-release-fixture.mjs", + "Rust SDK package check must use deterministic Bun release-shaped broker asset fixtures", ) require_text( "src/sdks/rust/src/bin/package_resources.rs", diff --git a/tools/test/create-broker-release-fixture.mjs b/tools/test/create-broker-release-fixture.mjs new file mode 100644 index 00000000..4fc2c1e5 --- /dev/null +++ b/tools/test/create-broker-release-fixture.mjs @@ -0,0 +1,51 @@ +#!/usr/bin/env bun +import fs from "node:fs/promises"; +import path from "node:path"; + +import { + parseCommonArgs, + writeChecksumManifest, + writeEntriesArchive, +} from "./release-fixture-utils.mjs"; + +function brokerEntries(target, executable) { + return { + [executable]: "#!/bin/sh\necho oliphaunt-broker release fixture\n", + "manifest.properties": [ + "schema=oliphaunt-broker-release-assets-v1", + "product=oliphaunt-broker", + `target=${target}`, + `binary=${executable}`, + "", + ].join("\n"), + }; +} + +async function writeFixtureAssets(assetDir, version) { + await fs.mkdir(assetDir, { recursive: true }); + const executableModes = { + "bin/oliphaunt-broker": 0o755, + "bin/oliphaunt-broker.exe": 0o755, + }; + + for (const target of ["macos-arm64", "linux-x64-gnu", "linux-arm64-gnu"]) { + await writeEntriesArchive( + path.join(assetDir, `oliphaunt-broker-${version}-${target}.tar.gz`), + brokerEntries(target, "bin/oliphaunt-broker"), + executableModes, + ); + } + + await writeEntriesArchive( + path.join(assetDir, `oliphaunt-broker-${version}-windows-x64-msvc.zip`), + brokerEntries("windows-x64-msvc", "bin/oliphaunt-broker.exe"), + executableModes, + ); + await writeChecksumManifest(assetDir, `oliphaunt-broker-${version}-release-assets.sha256`); +} + +const { assetDir, version } = parseCommonArgs( + Bun.argv.slice(2), + "Create small oliphaunt-broker release-shaped assets for SDK checks.", +); +await writeFixtureAssets(assetDir, version); diff --git a/tools/test/create-broker-release-fixture.py b/tools/test/create-broker-release-fixture.py deleted file mode 100644 index d82bcedd..00000000 --- a/tools/test/create-broker-release-fixture.py +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env python3 -"""Create small oliphaunt-broker release-shaped assets for SDK checks.""" - -from __future__ import annotations - -import argparse -from pathlib import Path - -from release_fixture_utils import write_checksum_manifest, write_tar_gz, write_zip - - -def broker_entries(target: str, executable: str) -> dict[str, bytes]: - return { - executable: b"#!/bin/sh\necho oliphaunt-broker release fixture\n", - "manifest.properties": ( - b"schema=oliphaunt-broker-release-assets-v1\n" - b"product=oliphaunt-broker\n" - + f"target={target}\n".encode() - + f"binary={executable}\n".encode() - ), - } - - -def write_fixture_assets(asset_dir: Path, version: str) -> None: - asset_dir.mkdir(parents=True, exist_ok=True) - executable_modes = {"bin/oliphaunt-broker": 0o755, "bin/oliphaunt-broker.exe": 0o755} - - for target in ["macos-arm64", "linux-x64-gnu", "linux-arm64-gnu"]: - write_tar_gz( - asset_dir / f"oliphaunt-broker-{version}-{target}.tar.gz", - broker_entries(target, "bin/oliphaunt-broker"), - executable_modes, - ) - - write_zip( - asset_dir / f"oliphaunt-broker-{version}-windows-x64-msvc.zip", - broker_entries("windows-x64-msvc", "bin/oliphaunt-broker.exe"), - executable_modes, - ) - write_checksum_manifest(asset_dir, f"oliphaunt-broker-{version}-release-assets.sha256") - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--asset-dir", required=True, help="directory to write release-shaped assets into") - parser.add_argument("--version", required=True, help="oliphaunt-broker version to encode in asset names") - return parser.parse_args() - - -def main() -> int: - args = parse_args() - write_fixture_assets(Path(args.asset_dir).resolve(), args.version) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/test/create-liboliphaunt-release-fixture.mjs b/tools/test/create-liboliphaunt-release-fixture.mjs new file mode 100644 index 00000000..caca22a9 --- /dev/null +++ b/tools/test/create-liboliphaunt-release-fixture.mjs @@ -0,0 +1,248 @@ +#!/usr/bin/env bun +import fs from "node:fs/promises"; +import path from "node:path"; + +import { + parseCommonArgs, + writeChecksumManifest, + writeEntriesArchive, +} from "./release-fixture-utils.mjs"; + +const NATIVE_TOOL_STEMS = ["initdb", "pg_ctl", "pg_dump", "postgres", "psql"]; + +function nativeRuntimeEntries({ windows = false } = {}) { + const suffix = windows ? ".exe" : ""; + const entries = Object.fromEntries( + NATIVE_TOOL_STEMS.map((tool) => [ + `runtime/bin/${tool}${suffix}`, + `not-a-real-${tool}${suffix}\n`, + ]), + ); + entries["runtime/share/postgresql/README.release-fixture"] = + "release-shaped native runtime fixture\n"; + return entries; +} + +function nativeRuntimeModes({ windows = false } = {}) { + const suffix = windows ? ".exe" : ""; + return Object.fromEntries( + NATIVE_TOOL_STEMS.map((tool) => [`runtime/bin/${tool}${suffix}`, 0o755]), + ); +} + +function runtimeResourceEntries() { + return { + "oliphaunt/package-size.tsv": [ + "kind\tid\textensions\tfiles\tbytes", + "package\ttotal\t-\t-\t96", + "package\truntime\t-\t-\t31", + "package\ttemplate-pgdata\t-\t-\t20", + "package\tstatic-registry\t-\t-\t45", + "extensions\tselected\t-\t-\t0", + "", + ].join("\n"), + "oliphaunt/runtime/files/share/postgresql/README.release-fixture": + "release-shaped runtime fixture\n", + "oliphaunt/static-registry/manifest.properties": [ + "schema=oliphaunt-static-registry-v1", + "registered=", + "pending=", + "", + ].join("\n"), + "oliphaunt/runtime/manifest.properties": runtimeResourceManifest( + "release-fixture-runtime", + "postgres-runtime-files-v1", + ), + "oliphaunt/template-pgdata/files/PG_VERSION": "18\n", + "oliphaunt/template-pgdata/manifest.properties": runtimeResourceManifest( + "release-fixture-template", + "postgres-template-pgdata-v1", + ), + }; +} + +function runtimeResourceManifest(cacheKey, layout) { + return [ + "schema=oliphaunt-runtime-resources-v1", + `cacheKey=${cacheKey}`, + `layout=${layout}`, + "extensions=", + "runtimeFeatures=", + "sharedPreloadLibraries=", + "mobileStaticRegistryState=not-required", + "mobileStaticRegistryRegistered=", + "mobileStaticRegistryPending=", + "nativeModuleStems=", + "mobileStaticRegistrySource=", + "", + ].join("\n"); +} + +function xmlEscape(value) { + return value + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """); +} + +function plistValue(value, indent = " ") { + if (Array.isArray(value)) { + const lines = [`${indent}`]; + for (const item of value) { + lines.push(plistValue(item, `${indent} `)); + } + lines.push(`${indent}`); + return lines.join("\n"); + } + if (value && typeof value === "object") { + const lines = [`${indent}`]; + for (const key of Object.keys(value).sort()) { + lines.push(`${indent} ${xmlEscape(key)}`); + lines.push(plistValue(value[key], `${indent} `)); + } + lines.push(`${indent}`); + return lines.join("\n"); + } + return `${indent}${xmlEscape(String(value))}`; +} + +function plist(dictionary) { + return [ + '', + '', + '', + plistValue(dictionary, " "), + "", + "", + ].join("\n"); +} + +function xcframeworkEntries() { + const libraries = [ + { + LibraryIdentifier: "macos-arm64", + LibraryPath: "liboliphaunt.framework", + SupportedArchitectures: ["arm64"], + SupportedPlatform: "macos", + }, + { + LibraryIdentifier: "ios-arm64", + LibraryPath: "liboliphaunt.framework", + SupportedArchitectures: ["arm64"], + SupportedPlatform: "ios", + }, + { + LibraryIdentifier: "ios-arm64_x86_64-simulator", + LibraryPath: "liboliphaunt.framework", + SupportedArchitectures: ["arm64", "x86_64"], + SupportedPlatform: "ios", + SupportedPlatformVariant: "simulator", + }, + ]; + const entries = { + "liboliphaunt.xcframework/Info.plist": plist({ + AvailableLibraries: libraries, + CFBundlePackageType: "XFWK", + XCFrameworkFormatVersion: "1.0", + }), + }; + for (const library of libraries) { + const frameworkRoot = `liboliphaunt.xcframework/${library.LibraryIdentifier}/liboliphaunt.framework`; + entries[`${frameworkRoot}/liboliphaunt`] = "not-a-real-framework-binary\n"; + entries[`${frameworkRoot}/Info.plist`] = plist({ + CFBundleExecutable: "liboliphaunt", + CFBundleIdentifier: "dev.oliphaunt.liboliphaunt.fixture", + CFBundleName: "liboliphaunt", + CFBundlePackageType: "FMWK", + }); + } + return entries; +} + +async function writeFixtureAssets(assetDir, version) { + await fs.mkdir(assetDir, { recursive: true }); + + await fs.writeFile( + path.join(assetDir, `liboliphaunt-${version}-package-size.tsv`), + [ + "kind\tid\textensions\tfiles\tbytes", + "package\ttotal\t-\t-\t96", + "package\truntime\t-\t-\t31", + "package\ttemplate-pgdata\t-\t-\t20", + "package\tstatic-registry\t-\t-\t45", + "extensions\tselected\t-\t-\t0", + "", + ].join("\n"), + "utf8", + ); + + await writeEntriesArchive( + path.join(assetDir, `liboliphaunt-${version}-runtime-resources.tar.gz`), + runtimeResourceEntries(), + ); + await writeEntriesArchive( + path.join(assetDir, `liboliphaunt-${version}-icu-data.tar.gz`), + { "share/icu/icudt76l.dat": "not-real-icu-data\n" }, + ); + await writeEntriesArchive( + path.join(assetDir, `liboliphaunt-${version}-macos-arm64.tar.gz`), + { + "lib/liboliphaunt.dylib": "not-a-real-dylib\n", + "lib/modules/plpgsql.dylib": "not-a-real-module\n", + ...nativeRuntimeEntries(), + }, + nativeRuntimeModes(), + ); + await writeEntriesArchive( + path.join(assetDir, `liboliphaunt-${version}-linux-x64-gnu.tar.gz`), + { + "lib/liboliphaunt.so": "not-a-real-elf\n", + "lib/modules/plpgsql.so": "not-a-real-module\n", + ...nativeRuntimeEntries(), + }, + nativeRuntimeModes(), + ); + await writeEntriesArchive( + path.join(assetDir, `liboliphaunt-${version}-linux-arm64-gnu.tar.gz`), + { + "lib/liboliphaunt.so": "not-a-real-elf\n", + "lib/modules/plpgsql.so": "not-a-real-module\n", + ...nativeRuntimeEntries(), + }, + nativeRuntimeModes(), + ); + await writeEntriesArchive( + path.join(assetDir, `liboliphaunt-${version}-ios-xcframework.tar.gz`), + xcframeworkEntries(), + ); + await writeEntriesArchive( + path.join(assetDir, `liboliphaunt-${version}-android-arm64-v8a.tar.gz`), + { "jni/arm64-v8a/liboliphaunt.so": "not-a-real-android-elf\n" }, + ); + await writeEntriesArchive( + path.join(assetDir, `liboliphaunt-${version}-android-x86_64.tar.gz`), + { "jni/x86_64/liboliphaunt.so": "not-a-real-android-elf\n" }, + ); + await writeEntriesArchive( + path.join(assetDir, `liboliphaunt-${version}-windows-x64-msvc.zip`), + { + "bin/oliphaunt.dll": "not-a-real-dll\n", + "lib/modules/plpgsql.dll": "not-a-real-module\n", + ...nativeRuntimeEntries({ windows: true }), + }, + nativeRuntimeModes({ windows: true }), + ); + await writeEntriesArchive( + path.join(assetDir, `liboliphaunt-${version}-apple-spm-xcframework.zip`), + xcframeworkEntries(), + ); + + await writeChecksumManifest(assetDir, `liboliphaunt-${version}-release-assets.sha256`); +} + +const { assetDir, version } = parseCommonArgs( + Bun.argv.slice(2), + "Create small liboliphaunt release-shaped assets for SDK package checks.", +); +await writeFixtureAssets(assetDir, version); diff --git a/tools/test/create-liboliphaunt-release-fixture.py b/tools/test/create-liboliphaunt-release-fixture.py deleted file mode 100644 index db7e7152..00000000 --- a/tools/test/create-liboliphaunt-release-fixture.py +++ /dev/null @@ -1,227 +0,0 @@ -#!/usr/bin/env python3 -"""Create small liboliphaunt release-shaped assets for SDK package checks. - -The generated assets are not runnable PostgreSQL builds. They intentionally -exercise the consumer-facing release contract: product-scoped asset names, -checksums, archive layouts, and runtime-resource extraction. -""" - -from __future__ import annotations - -import argparse -import plistlib -from pathlib import Path - -from release_fixture_utils import write_checksum_manifest, write_tar_gz, write_zip - - -NATIVE_TOOL_STEMS = ("initdb", "pg_ctl", "pg_dump", "postgres", "psql") - - -def native_runtime_entries(*, windows: bool = False) -> dict[str, bytes]: - suffix = ".exe" if windows else "" - entries = { - f"runtime/bin/{tool}{suffix}": f"not-a-real-{tool}{suffix}\n".encode("utf-8") - for tool in NATIVE_TOOL_STEMS - } - entries["runtime/share/postgresql/README.release-fixture"] = b"release-shaped native runtime fixture\n" - return entries - - -def native_runtime_modes(*, windows: bool = False) -> dict[str, int]: - suffix = ".exe" if windows else "" - return {f"runtime/bin/{tool}{suffix}": 0o755 for tool in NATIVE_TOOL_STEMS} - - -def runtime_resource_entries() -> dict[str, bytes]: - return { - "oliphaunt/package-size.tsv": ( - b"kind\tid\textensions\tfiles\tbytes\n" - b"package\ttotal\t-\t-\t96\n" - b"package\truntime\t-\t-\t31\n" - b"package\ttemplate-pgdata\t-\t-\t20\n" - b"package\tstatic-registry\t-\t-\t45\n" - b"extensions\tselected\t-\t-\t0\n" - ), - "oliphaunt/runtime/files/share/postgresql/README.release-fixture": ( - b"release-shaped runtime fixture\n" - ), - "oliphaunt/static-registry/manifest.properties": ( - b"schema=oliphaunt-static-registry-v1\n" - b"registered=\n" - b"pending=\n" - ), - "oliphaunt/runtime/manifest.properties": ( - b"schema=oliphaunt-runtime-resources-v1\n" - b"cacheKey=release-fixture-runtime\n" - b"layout=postgres-runtime-files-v1\n" - b"extensions=\n" - b"runtimeFeatures=\n" - b"sharedPreloadLibraries=\n" - b"mobileStaticRegistryState=not-required\n" - b"mobileStaticRegistryRegistered=\n" - b"mobileStaticRegistryPending=\n" - b"nativeModuleStems=\n" - b"mobileStaticRegistrySource=\n" - ), - "oliphaunt/template-pgdata/files/PG_VERSION": b"18\n", - "oliphaunt/template-pgdata/manifest.properties": ( - b"schema=oliphaunt-runtime-resources-v1\n" - b"cacheKey=release-fixture-template\n" - b"layout=postgres-template-pgdata-v1\n" - b"extensions=\n" - b"runtimeFeatures=\n" - b"sharedPreloadLibraries=\n" - b"mobileStaticRegistryState=not-required\n" - b"mobileStaticRegistryRegistered=\n" - b"mobileStaticRegistryPending=\n" - b"nativeModuleStems=\n" - b"mobileStaticRegistrySource=\n" - ), - } - - -def xcframework_entries() -> dict[str, bytes]: - libraries = [ - { - "LibraryIdentifier": "macos-arm64", - "LibraryPath": "liboliphaunt.framework", - "SupportedArchitectures": ["arm64"], - "SupportedPlatform": "macos", - }, - { - "LibraryIdentifier": "ios-arm64", - "LibraryPath": "liboliphaunt.framework", - "SupportedArchitectures": ["arm64"], - "SupportedPlatform": "ios", - }, - { - "LibraryIdentifier": "ios-arm64_x86_64-simulator", - "LibraryPath": "liboliphaunt.framework", - "SupportedArchitectures": ["arm64", "x86_64"], - "SupportedPlatform": "ios", - "SupportedPlatformVariant": "simulator", - }, - ] - info = plistlib.dumps( - { - "AvailableLibraries": libraries, - "CFBundlePackageType": "XFWK", - "XCFrameworkFormatVersion": "1.0", - }, - sort_keys=True, - ) - entries = {"liboliphaunt.xcframework/Info.plist": info} - for library in libraries: - identifier = library["LibraryIdentifier"] - framework_root = f"liboliphaunt.xcframework/{identifier}/liboliphaunt.framework" - entries[f"{framework_root}/liboliphaunt"] = b"not-a-real-framework-binary\n" - entries[f"{framework_root}/Info.plist"] = plistlib.dumps( - { - "CFBundleExecutable": "liboliphaunt", - "CFBundleIdentifier": "dev.oliphaunt.liboliphaunt.fixture", - "CFBundleName": "liboliphaunt", - "CFBundlePackageType": "FMWK", - }, - sort_keys=True, - ) - return entries - - -def write_fixture_assets(asset_dir: Path, version: str) -> None: - asset_dir.mkdir(parents=True, exist_ok=True) - - (asset_dir / f"liboliphaunt-{version}-package-size.tsv").write_text( - "\n".join( - [ - "kind\tid\textensions\tfiles\tbytes", - "package\ttotal\t-\t-\t96", - "package\truntime\t-\t-\t31", - "package\ttemplate-pgdata\t-\t-\t20", - "package\tstatic-registry\t-\t-\t45", - "extensions\tselected\t-\t-\t0", - ] - ) - + "\n", - encoding="utf-8", - ) - - write_tar_gz( - asset_dir / f"liboliphaunt-{version}-runtime-resources.tar.gz", - runtime_resource_entries(), - ) - write_tar_gz( - asset_dir / f"liboliphaunt-{version}-icu-data.tar.gz", - {"share/icu/icudt76l.dat": b"not-real-icu-data\n"}, - ) - write_tar_gz( - asset_dir / f"liboliphaunt-{version}-macos-arm64.tar.gz", - { - "lib/liboliphaunt.dylib": b"not-a-real-dylib\n", - "lib/modules/plpgsql.dylib": b"not-a-real-module\n", - **native_runtime_entries(), - }, - modes=native_runtime_modes(), - ) - write_tar_gz( - asset_dir / f"liboliphaunt-{version}-linux-x64-gnu.tar.gz", - { - "lib/liboliphaunt.so": b"not-a-real-elf\n", - "lib/modules/plpgsql.so": b"not-a-real-module\n", - **native_runtime_entries(), - }, - modes=native_runtime_modes(), - ) - write_tar_gz( - asset_dir / f"liboliphaunt-{version}-linux-arm64-gnu.tar.gz", - { - "lib/liboliphaunt.so": b"not-a-real-elf\n", - "lib/modules/plpgsql.so": b"not-a-real-module\n", - **native_runtime_entries(), - }, - modes=native_runtime_modes(), - ) - write_tar_gz( - asset_dir / f"liboliphaunt-{version}-ios-xcframework.tar.gz", - xcframework_entries(), - ) - write_tar_gz( - asset_dir / f"liboliphaunt-{version}-android-arm64-v8a.tar.gz", - {"jni/arm64-v8a/liboliphaunt.so": b"not-a-real-android-elf\n"}, - ) - write_tar_gz( - asset_dir / f"liboliphaunt-{version}-android-x86_64.tar.gz", - {"jni/x86_64/liboliphaunt.so": b"not-a-real-android-elf\n"}, - ) - write_zip( - asset_dir / f"liboliphaunt-{version}-windows-x64-msvc.zip", - { - "bin/oliphaunt.dll": b"not-a-real-dll\n", - "lib/modules/plpgsql.dll": b"not-a-real-module\n", - **native_runtime_entries(windows=True), - }, - modes=native_runtime_modes(windows=True), - ) - write_zip( - asset_dir / f"liboliphaunt-{version}-apple-spm-xcframework.zip", - xcframework_entries(), - ) - - write_checksum_manifest(asset_dir, f"liboliphaunt-{version}-release-assets.sha256") - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--asset-dir", required=True, help="directory to write release-shaped assets into") - parser.add_argument("--version", required=True, help="liboliphaunt version to encode in asset names") - return parser.parse_args() - - -def main() -> int: - args = parse_args() - write_fixture_assets(Path(args.asset_dir).resolve(), args.version) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/test/moon.yml b/tools/test/moon.yml index c3bca18e..18ad7052 100644 --- a/tools/test/moon.yml +++ b/tools/test/moon.yml @@ -19,7 +19,7 @@ owners: tasks: check: tags: ["quality", "static"] - command: "sh -c 'node --check tools/test/run-js-tests.mjs && python3 -m py_compile tools/test/create-liboliphaunt-release-fixture.py tools/test/create-broker-release-fixture.py tools/test/release_fixture_utils.py'" + command: "sh -c 'node --check tools/test/run-js-tests.mjs && bun build tools/test/create-liboliphaunt-release-fixture.mjs tools/test/create-broker-release-fixture.mjs --target=bun --outdir target/moon/test-tools/check'" inputs: - "/tools/test/**/*" options: diff --git a/tools/test/release-fixture-utils.mjs b/tools/test/release-fixture-utils.mjs new file mode 100644 index 00000000..b0938210 --- /dev/null +++ b/tools/test/release-fixture-utils.mjs @@ -0,0 +1,74 @@ +import { createHash } from "node:crypto"; +import { spawnSync } from "node:child_process"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +const ARCHIVE_DIR = path.resolve(import.meta.dir, "../release/archive_dir.mjs"); + +export function fail(message) { + console.error(`release-fixture-utils.mjs: ${message}`); + process.exit(1); +} + +export function parseCommonArgs(argv, description) { + const args = new Map(); + for (let index = 0; index < argv.length; index += 1) { + const key = argv[index]; + const value = argv[index + 1]; + if (!key.startsWith("--") || value === undefined || value.startsWith("--")) { + fail(`${description}\nusage: --asset-dir --version `); + } + args.set(key, value); + index += 1; + } + const assetDir = args.get("--asset-dir"); + const version = args.get("--version"); + if (!assetDir || !version || args.size !== 2) { + fail(`${description}\nusage: --asset-dir --version `); + } + return { assetDir: path.resolve(assetDir), version }; +} + +export async function writeEntriesArchive(output, entries, modes = {}) { + const stage = await fs.mkdtemp(path.join(os.tmpdir(), "oliphaunt-release-fixture-")); + try { + for (const [name, data] of Object.entries(entries).sort(([left], [right]) => + left.localeCompare(right), + )) { + const file = path.join(stage, ...name.split("/")); + await fs.mkdir(path.dirname(file), { recursive: true }); + await fs.writeFile(file, data); + await fs.chmod(file, modes[name] ?? 0o644); + } + await archiveDirectory(stage, output); + } finally { + await fs.rm(stage, { recursive: true, force: true }); + } +} + +export async function archiveDirectory(source, output) { + const result = spawnSync(process.execPath, [ARCHIVE_DIR, source, output], { + stdio: "inherit", + }); + if (result.status !== 0) { + fail(`failed to create archive ${output}`); + } +} + +export async function writeChecksumManifest(assetDir, name) { + const checksumAsset = path.join(assetDir, name); + const dirents = await fs.readdir(assetDir, { withFileTypes: true }); + const files = dirents + .filter((entry) => entry.isFile() && entry.name !== name) + .map((entry) => entry.name) + .sort(); + const lines = []; + for (const file of files) { + const digest = createHash("sha256") + .update(await fs.readFile(path.join(assetDir, file))) + .digest("hex"); + lines.push(`${digest} ./${file}`); + } + await fs.writeFile(checksumAsset, `${lines.join("\n")}\n`, "utf8"); +} diff --git a/tools/test/release_fixture_utils.py b/tools/test/release_fixture_utils.py deleted file mode 100644 index 4b81f42d..00000000 --- a/tools/test/release_fixture_utils.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env python3 -"""Shared helpers for small release-shaped fixture assets.""" - -from __future__ import annotations - -import hashlib -import io -import tarfile -import zipfile -from pathlib import Path -from tarfile import TarInfo - - -def sha256(path: Path) -> str: - digest = hashlib.sha256() - with path.open("rb") as file: - for chunk in iter(lambda: file.read(1024 * 1024), b""): - digest.update(chunk) - return digest.hexdigest() - - -def add_tar_file(archive: tarfile.TarFile, name: str, data: bytes, mode: int = 0o644) -> None: - info = TarInfo(name) - info.size = len(data) - info.mode = mode - info.mtime = 0 - archive.addfile(info, io.BytesIO(data)) - - -def write_tar_gz(path: Path, entries: dict[str, bytes], modes: dict[str, int] | None = None) -> None: - with tarfile.open(path, "w:gz", format=tarfile.PAX_FORMAT) as archive: - for name, data in sorted(entries.items()): - add_tar_file(archive, name, data, mode=(modes or {}).get(name, 0o644)) - - -def write_zip(path: Path, entries: dict[str, bytes], modes: dict[str, int] | None = None) -> None: - with zipfile.ZipFile(path, "w", compression=zipfile.ZIP_DEFLATED) as archive: - for name, data in sorted(entries.items()): - info = zipfile.ZipInfo(name) - info.date_time = (1980, 1, 1, 0, 0, 0) - info.external_attr = (modes or {}).get(name, 0o644) << 16 - archive.writestr(info, data) - - -def write_checksum_manifest(asset_dir: Path, name: str) -> None: - checksum_asset = asset_dir / name - lines = [] - for asset in sorted(path for path in asset_dir.iterdir() if path.is_file() and path != checksum_asset): - lines.append(f"{sha256(asset)} ./{asset.name}") - checksum_asset.write_text("\n".join(lines) + "\n", encoding="utf-8") From 289d5b40817764ddd8686eb5ed9e769d0b303677 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 13:07:33 +0000 Subject: [PATCH 085/137] chore: port helper asset validators to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 5 + src/runtimes/broker/moon.yml | 5 +- src/runtimes/node-direct/moon.yml | 8 + .../node-direct/tools/build-node-addon.sh | 2 +- tools/policy/python-entrypoints.allowlist | 2 - tools/release/check-broker-release-assets.mjs | 127 +++++++++++ .../check-node-direct-release-assets.mjs | 121 ++++++++++ tools/release/check_artifact_targets.py | 4 +- tools/release/check_broker_release_assets.py | 171 -------------- .../check_node_direct_release_assets.py | 163 -------------- tools/release/check_release_metadata.py | 8 +- tools/release/package-broker-assets.sh | 2 +- tools/release/release-artifact-targets.mjs | 209 ++++++++++++++++++ tools/release/release-asset-validation.mjs | 108 +++++++++ tools/release/release.py | 4 +- 15 files changed, 592 insertions(+), 347 deletions(-) create mode 100644 tools/release/check-broker-release-assets.mjs create mode 100644 tools/release/check-node-direct-release-assets.mjs delete mode 100755 tools/release/check_broker_release_assets.py delete mode 100755 tools/release/check_node_direct_release_assets.py create mode 100644 tools/release/release-artifact-targets.mjs create mode 100644 tools/release/release-asset-validation.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 0c7adead..125be912 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -269,6 +269,11 @@ review production pipelines, then normalize implementation details. layouts and call the shared deterministic `tools/release/archive_dir.mjs` helper for tar.gz/zip output. The retired Python fixture generators and shared Python utility were removed from the Python inventory. +- Broker and Node direct release asset validation now uses Bun. The validators + share archive/checksum parsing through `tools/release/release-asset-validation.mjs` + and derive published target membership from Moon release metadata through + `tools/release/release-artifact-targets.mjs`, keeping the helper/runtime + release checks on the same target graph as CI and publication. - Rust helper inventory is currently limited to `tools/xtask` and `tools/perf/runner`. Both remain Rust-owned for now: `xtask` owns WASIX asset parsing, archive/hash work, AOT/template feature-gated paths, and release diff --git a/src/runtimes/broker/moon.yml b/src/runtimes/broker/moon.yml index ea4c2a9d..3941dad9 100644 --- a/src/runtimes/broker/moon.yml +++ b/src/runtimes/broker/moon.yml @@ -109,7 +109,10 @@ tasks: - "/src/runtimes/broker/**/*" - "/src/sdks/rust/**/*" - "/tools/release/package-broker-assets.sh" - - "/tools/release/check_broker_release_assets.py" + - "/tools/release/check-broker-release-assets.mjs" + - "/tools/release/release-asset-validation.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/policy/moon.mjs" - "/tools/release/artifact_target_matrix.py" - "/release-please-config.json" - "/.release-please-manifest.json" diff --git a/src/runtimes/node-direct/moon.yml b/src/runtimes/node-direct/moon.yml index ee019e59..b228523a 100644 --- a/src/runtimes/node-direct/moon.yml +++ b/src/runtimes/node-direct/moon.yml @@ -40,6 +40,10 @@ tasks: - "/src/runtimes/node-direct/**/*" - "/tools/release/artifact_targets.py" - "/tools/release/check_artifact_targets.py" + - "/tools/release/check-node-direct-release-assets.mjs" + - "/tools/release/release-asset-validation.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/policy/moon.mjs" - "/release-please-config.json" - "/.release-please-manifest.json" - "/src/**/release.toml" @@ -77,6 +81,10 @@ tasks: - "/src/runtimes/node-direct/**/*" - "/tools/release/artifact_target_matrix.py" - "/tools/release/artifact_targets.py" + - "/tools/release/check-node-direct-release-assets.mjs" + - "/tools/release/release-asset-validation.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/policy/moon.mjs" - "/release-please-config.json" - "/.release-please-manifest.json" - "/src/**/release.toml" diff --git a/src/runtimes/node-direct/tools/build-node-addon.sh b/src/runtimes/node-direct/tools/build-node-addon.sh index 9dba06cc..3f99dba1 100755 --- a/src/runtimes/node-direct/tools/build-node-addon.sh +++ b/src/runtimes/node-direct/tools/build-node-addon.sh @@ -233,7 +233,7 @@ tools/release/write_checksum_manifest.mjs \ --pattern 'oliphaunt-node-direct-*.zip' printf 'Node direct addon smoke passed: %s\n' "$addon" -python3 tools/release/check_node_direct_release_assets.py --asset-dir "$asset_dir" --allow-partial +bun tools/release/check-node-direct-release-assets.mjs --asset-dir "$asset_dir" --allow-partial case "$target" in macos-arm64) optional_package="darwin-arm64" ;; linux-x64-gnu) optional_package="linux-x64-gnu" ;; diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index a4efc676..36931285 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -13,12 +13,10 @@ tools/release/artifact_targets.py tools/release/build-extension-ci-artifacts.py tools/release/build_maven_artifact_manifest.py tools/release/check_artifact_targets.py -tools/release/check_broker_release_assets.py tools/release/check_consumer_shape.py tools/release/check_cratesio_publication.py tools/release/check_github_release_assets.py tools/release/check_liboliphaunt_release_assets.py -tools/release/check_node_direct_release_assets.py tools/release/check_registry_publication.py tools/release/check_release_metadata.py tools/release/check_release_pr_coverage.py diff --git a/tools/release/check-broker-release-assets.mjs b/tools/release/check-broker-release-assets.mjs new file mode 100644 index 00000000..01658d2d --- /dev/null +++ b/tools/release/check-broker-release-assets.mjs @@ -0,0 +1,127 @@ +#!/usr/bin/env bun +import path from "node:path"; + +import { + assertFileExists, + checksumManifest, + readArchiveEntries, + sha256, +} from "./release-asset-validation.mjs"; +import { + ROOT, + artifactTargets, + compareText, + currentProductVersion, + expectedAssets, + fail, +} from "./release-artifact-targets.mjs"; + +const PREFIX = "check-broker-release-assets.mjs"; +const PRODUCT = "oliphaunt-broker"; +const KIND = "broker-helper"; + +function parseArgs(argv) { + const args = { + assetDir: path.join(ROOT, "target/oliphaunt-broker/release-assets"), + allowPartial: false, + }; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === "--asset-dir") { + const value = argv[index + 1]; + if (!value) { + fail(PREFIX, "--asset-dir requires a value"); + } + args.assetDir = path.resolve(value); + index += 1; + } else if (arg === "--allow-partial") { + args.allowPartial = true; + } else { + fail(PREFIX, `unknown argument ${arg}`); + } + } + return args; +} + +async function validateArchive(file, target) { + const entries = await readArchiveEntries(file, fail, PREFIX, "broker"); + const executable = target.executableRelativePath; + if (!entries.has(executable)) { + fail(PREFIX, `${path.basename(file)} is missing ${executable}`); + } + if (!entries.has("manifest.properties")) { + fail(PREFIX, `${path.basename(file)} is missing manifest.properties`); + } + const broker = entries.get(executable); + if (!broker.isFile) { + fail(PREFIX, `${path.basename(file)} ${executable} is not a regular file`); + } + if (file.endsWith(".tar.gz") && (broker.mode & 0o111) === 0) { + fail(PREFIX, `${path.basename(file)} ${executable} is not executable`); + } + if (path.extname(file) === ".zip" && broker.size === 0) { + fail(PREFIX, `${path.basename(file)} ${executable} is empty`); + } +} + +async function main() { + const args = parseArgs(Bun.argv.slice(2)); + const version = await currentProductVersion(PRODUCT, PREFIX); + const requiredAssets = expectedAssets(PRODUCT, KIND, version, PREFIX); + const targets = artifactTargets(PRODUCT, KIND, PREFIX); + const targetsByAsset = new Map(targets.map((target) => [target.asset.replaceAll("{version}", version), target])); + const missing = []; + for (const asset of requiredAssets) { + if (!(await assertFileExists(path.join(args.assetDir, asset)))) { + missing.push(asset); + } + } + if (missing.length > 0) { + if (!args.allowPartial) { + fail(PREFIX, `missing oliphaunt-broker release asset(s): ${missing.join(", ")}`); + } + let presentBrokerAssets = 0; + for (const target of targets) { + if (await assertFileExists(path.join(args.assetDir, target.asset.replaceAll("{version}", version)))) { + presentBrokerAssets += 1; + } + } + if (presentBrokerAssets === 0) { + fail(PREFIX, "partial oliphaunt-broker release asset validation requires at least one broker asset"); + } + } + + const checksumAsset = `oliphaunt-broker-${version}-release-assets.sha256`; + const checksumPath = path.join(args.assetDir, checksumAsset); + if (!(await assertFileExists(checksumPath))) { + fail(PREFIX, `missing checksum manifest: ${checksumAsset}`); + } + const checksums = await checksumManifest(checksumPath, fail, PREFIX); + for (const asset of requiredAssets.sort(compareText)) { + const assetPath = path.join(args.assetDir, asset); + if (args.allowPartial && !(await assertFileExists(assetPath))) { + continue; + } + if (asset === checksumAsset) { + continue; + } + const expected = checksums.get(asset); + if (!expected) { + fail(PREFIX, `${checksumAsset} does not cover ${asset}`); + } + const actual = await sha256(assetPath); + if (actual !== expected) { + fail(PREFIX, `checksum mismatch for ${asset}: expected ${expected}, got ${actual}`); + } + } + for (const [asset, target] of targetsByAsset) { + const assetPath = path.join(args.assetDir, asset); + if (args.allowPartial && !(await assertFileExists(assetPath))) { + continue; + } + await validateArchive(assetPath, target); + } + console.log(`oliphaunt-broker release assets validated: ${args.assetDir}`); +} + +await main(); diff --git a/tools/release/check-node-direct-release-assets.mjs b/tools/release/check-node-direct-release-assets.mjs new file mode 100644 index 00000000..430cac74 --- /dev/null +++ b/tools/release/check-node-direct-release-assets.mjs @@ -0,0 +1,121 @@ +#!/usr/bin/env bun +import path from "node:path"; + +import { + assertFileExists, + checksumManifest, + readArchiveEntries, + sha256, +} from "./release-asset-validation.mjs"; +import { + ROOT, + artifactTargets, + compareText, + currentProductVersion, + expectedAssets, + fail, +} from "./release-artifact-targets.mjs"; + +const PREFIX = "check-node-direct-release-assets.mjs"; +const PRODUCT = "oliphaunt-node-direct"; +const KIND = "node-direct-addon"; + +function parseArgs(argv) { + const args = { + assetDir: path.join(ROOT, "target/oliphaunt-node-direct/release-assets"), + allowPartial: false, + }; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === "--asset-dir") { + const value = argv[index + 1]; + if (!value) { + fail(PREFIX, "--asset-dir requires a value"); + } + args.assetDir = path.resolve(value); + index += 1; + } else if (arg === "--allow-partial") { + args.allowPartial = true; + } else { + fail(PREFIX, `unknown argument ${arg}`); + } + } + return args; +} + +async function validateArchive(file, target) { + const entries = await readArchiveEntries(file, fail, PREFIX, "Node direct"); + const memberName = target.libraryRelativePath; + if (!entries.has(memberName)) { + fail(PREFIX, `${path.basename(file)} is missing ${memberName}`); + } + const member = entries.get(memberName); + if (!member.isFile) { + fail(PREFIX, `${path.basename(file)} ${memberName} is not a regular file`); + } + if (member.size === 0) { + fail(PREFIX, `${path.basename(file)} ${memberName} is empty`); + } +} + +async function main() { + const args = parseArgs(Bun.argv.slice(2)); + const version = await currentProductVersion(PRODUCT, PREFIX); + const requiredAssets = expectedAssets(PRODUCT, KIND, version, PREFIX); + const targets = artifactTargets(PRODUCT, KIND, PREFIX); + const targetsByAsset = new Map(targets.map((target) => [target.asset.replaceAll("{version}", version), target])); + const missing = []; + for (const asset of requiredAssets) { + if (!(await assertFileExists(path.join(args.assetDir, asset)))) { + missing.push(asset); + } + } + if (missing.length > 0) { + if (!args.allowPartial) { + fail(PREFIX, `missing oliphaunt-node-direct release asset(s): ${missing.join(", ")}`); + } + let presentAddons = 0; + for (const target of targets) { + if (await assertFileExists(path.join(args.assetDir, target.asset.replaceAll("{version}", version)))) { + presentAddons += 1; + } + } + if (presentAddons === 0) { + fail(PREFIX, "partial oliphaunt-node-direct release asset validation requires at least one addon asset"); + } + } + + const checksumAsset = `oliphaunt-node-direct-${version}-release-assets.sha256`; + const checksumPath = path.join(args.assetDir, checksumAsset); + if (!(await assertFileExists(checksumPath))) { + fail(PREFIX, `missing checksum manifest: ${checksumAsset}`); + } + const checksums = await checksumManifest(checksumPath, fail, PREFIX); + for (const asset of requiredAssets.sort(compareText)) { + const assetPath = path.join(args.assetDir, asset); + if (args.allowPartial && !(await assertFileExists(assetPath))) { + continue; + } + if (asset === checksumAsset) { + continue; + } + const expected = checksums.get(asset); + if (!expected) { + fail(PREFIX, `${checksumAsset} does not cover ${asset}`); + } + const actual = await sha256(assetPath); + if (actual !== expected) { + fail(PREFIX, `checksum mismatch for ${asset}: expected ${expected}, got ${actual}`); + } + } + for (const [asset, target] of targetsByAsset) { + const assetPath = path.join(args.assetDir, asset); + if (args.allowPartial && !(await assertFileExists(assetPath))) { + continue; + } + await validateArchive(assetPath, target); + } + console.log(`oliphaunt-node-direct release assets validated: ${args.assetDir}`); +} + +await main(); diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py index 33785140..2ce6a526 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -271,8 +271,8 @@ def validate_github_asset_helpers() -> None: "liboliphaunt release asset checks must derive required assets from product-local artifact targets", ) require_text( - "tools/release/check_broker_release_assets.py", - "artifact_targets.expected_assets", + "tools/release/check-broker-release-assets.mjs", + "expectedAssets(PRODUCT, KIND, version", "Rust broker release asset checks must derive required assets from product-local artifact targets", ) require_text( diff --git a/tools/release/check_broker_release_assets.py b/tools/release/check_broker_release_assets.py deleted file mode 100755 index a7e89389..00000000 --- a/tools/release/check_broker_release_assets.py +++ /dev/null @@ -1,171 +0,0 @@ -#!/usr/bin/env python3 -"""Validate local oliphaunt-broker GitHub release assets.""" - -from __future__ import annotations - -import argparse -import hashlib -import sys -import tarfile -import zipfile -from pathlib import Path -from typing import NoReturn - -import artifact_targets -import product_metadata - - -ROOT = Path(__file__).resolve().parents[2] - - -def fail(message: str) -> NoReturn: - print(f"check_broker_release_assets.py: {message}", file=sys.stderr) - raise SystemExit(1) - - -def expected_assets(version: str) -> list[str]: - return artifact_targets.expected_assets("oliphaunt-broker", version, surface="github-release") - - -def expected_broker_assets(version: str) -> list[str]: - return artifact_targets.expected_assets( - "oliphaunt-broker", - version, - surface="github-release", - kinds=["broker-helper"], - ) - - -def broker_targets_by_asset(version: str) -> dict[str, artifact_targets.ArtifactTarget]: - return { - target.asset_name(version): target - for target in artifact_targets.artifact_targets( - product="oliphaunt-broker", - surface="github-release", - published_only=True, - ) - if target.kind == "broker-helper" - } - - -def sha256(path: Path) -> str: - digest = hashlib.sha256() - with path.open("rb") as handle: - for chunk in iter(lambda: handle.read(1024 * 1024), b""): - digest.update(chunk) - return digest.hexdigest() - - -def checksum_manifest(path: Path) -> dict[str, str]: - values: dict[str, str] = {} - for index, raw_line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): - line = raw_line.strip() - if not line: - continue - parts = line.split(maxsplit=1) - if len(parts) != 2 or len(parts[0]) != 64: - fail(f"malformed checksum line {index}: {raw_line}") - values[parts[1].removeprefix("./")] = parts[0].lower() - return values - - -def validate_broker_tar_archive(path: Path, executable_path: str) -> None: - with tarfile.open(path, "r:gz") as archive: - names = set(archive.getnames()) - if executable_path not in names: - fail(f"{path.name} is missing {executable_path}") - if "manifest.properties" not in names: - fail(f"{path.name} is missing manifest.properties") - broker = archive.getmember(executable_path) - if not broker.isfile(): - fail(f"{path.name} {executable_path} is not a regular file") - if broker.mode & 0o111 == 0: - fail(f"{path.name} {executable_path} is not executable") - - -def validate_broker_zip_archive(path: Path, executable_path: str) -> None: - with zipfile.ZipFile(path) as archive: - names = set(archive.namelist()) - if executable_path not in names: - fail(f"{path.name} is missing {executable_path}") - if "manifest.properties" not in names: - fail(f"{path.name} is missing manifest.properties") - broker = archive.getinfo(executable_path) - if broker.is_dir(): - fail(f"{path.name} {executable_path} is not a regular file") - if broker.file_size == 0: - fail(f"{path.name} {executable_path} is empty") - - -def validate_broker_archive(path: Path, target: artifact_targets.ArtifactTarget) -> None: - executable_path = target.executable_relative_path - if executable_path is None: - fail(f"{target.id} is missing executable_relative_path") - if path.name.endswith(".tar.gz"): - validate_broker_tar_archive(path, executable_path) - elif path.suffix == ".zip": - validate_broker_zip_archive(path, executable_path) - else: - fail(f"{path.name} has unsupported broker archive extension") - - -def validate(asset_dir: Path, allow_partial: bool = False) -> None: - version = product_metadata.read_current_version("oliphaunt-broker") - required_assets = expected_assets(version) - broker_targets = broker_targets_by_asset(version) - missing = [asset for asset in required_assets if not (asset_dir / asset).is_file()] - if missing: - if not allow_partial: - fail("missing oliphaunt-broker release asset(s): " + ", ".join(missing)) - present_broker_assets = [ - asset for asset in expected_broker_assets(version) if (asset_dir / asset).is_file() - ] - if not present_broker_assets: - fail( - "partial oliphaunt-broker release asset validation requires at least one broker asset" - ) - - checksum_asset = asset_dir / f"oliphaunt-broker-{version}-release-assets.sha256" - if not checksum_asset.is_file(): - fail(f"missing checksum manifest: {checksum_asset.name}") - checksums = checksum_manifest(checksum_asset) - for asset in required_assets: - if allow_partial and not (asset_dir / asset).is_file(): - continue - if asset == checksum_asset.name: - continue - expected_digest = checksums.get(asset) - if expected_digest is None: - fail(f"{checksum_asset.name} does not cover {asset}") - actual = sha256(asset_dir / asset) - if actual != expected_digest: - fail(f"checksum mismatch for {asset}: expected {expected_digest}, got {actual}") - for asset in expected_broker_assets(version): - if allow_partial and not (asset_dir / asset).is_file(): - continue - target = broker_targets.get(asset) - if target is None: - fail(f"no artifact target metadata found for {asset}") - validate_broker_archive(asset_dir / asset, target) - - -def main(argv: list[str]) -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--asset-dir", - default=str(ROOT / "target/oliphaunt-broker/release-assets"), - help="directory containing oliphaunt-broker release assets", - ) - parser.add_argument( - "--allow-partial", - action="store_true", - help="validate the broker assets present in asset-dir without requiring every published target", - ) - args = parser.parse_args(argv) - validate(Path(args.asset_dir).resolve(), allow_partial=args.allow_partial) - print(f"oliphaunt-broker release assets validated: {Path(args.asset_dir).resolve()}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) diff --git a/tools/release/check_node_direct_release_assets.py b/tools/release/check_node_direct_release_assets.py deleted file mode 100755 index 53e1fab4..00000000 --- a/tools/release/check_node_direct_release_assets.py +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env python3 -"""Validate local oliphaunt-node-direct GitHub release assets.""" - -from __future__ import annotations - -import argparse -import hashlib -import sys -import tarfile -import zipfile -from pathlib import Path -from typing import NoReturn - -import artifact_targets -import product_metadata - - -ROOT = Path(__file__).resolve().parents[2] - - -def fail(message: str) -> NoReturn: - print(f"check_node_direct_release_assets.py: {message}", file=sys.stderr) - raise SystemExit(1) - - -def sha256(path: Path) -> str: - digest = hashlib.sha256() - with path.open("rb") as handle: - for chunk in iter(lambda: handle.read(1024 * 1024), b""): - digest.update(chunk) - return digest.hexdigest() - - -def checksum_manifest(path: Path) -> dict[str, str]: - values: dict[str, str] = {} - for index, raw_line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): - line = raw_line.strip() - if not line: - continue - parts = line.split(maxsplit=1) - if len(parts) != 2 or len(parts[0]) != 64: - fail(f"malformed checksum line {index}: {raw_line}") - values[parts[1].removeprefix("./")] = parts[0].lower() - return values - - -def expected_assets(version: str) -> list[str]: - return artifact_targets.expected_assets("oliphaunt-node-direct", version, surface="github-release") - - -def expected_addon_assets(version: str) -> list[str]: - return artifact_targets.expected_assets( - "oliphaunt-node-direct", - version, - surface="github-release", - kinds=["node-direct-addon"], - ) - - -def addon_targets_by_asset(version: str) -> dict[str, artifact_targets.ArtifactTarget]: - return { - target.asset_name(version): target - for target in artifact_targets.artifact_targets( - product="oliphaunt-node-direct", - surface="github-release", - published_only=True, - ) - if target.kind == "node-direct-addon" - } - - -def validate_tar_archive(path: Path, member_name: str) -> None: - with tarfile.open(path, "r:gz") as archive: - names = set(archive.getnames()) - if member_name not in names: - fail(f"{path.name} is missing {member_name}") - member = archive.getmember(member_name) - if not member.isfile(): - fail(f"{path.name} {member_name} is not a regular file") - if member.size == 0: - fail(f"{path.name} {member_name} is empty") - - -def validate_zip_archive(path: Path, member_name: str) -> None: - with zipfile.ZipFile(path) as archive: - names = set(archive.namelist()) - if member_name not in names: - fail(f"{path.name} is missing {member_name}") - member = archive.getinfo(member_name) - if member.is_dir(): - fail(f"{path.name} {member_name} is not a regular file") - if member.file_size == 0: - fail(f"{path.name} {member_name} is empty") - - -def validate_addon_archive(path: Path, target: artifact_targets.ArtifactTarget) -> None: - member_name = target.library_relative_path - if member_name is None: - fail(f"{target.id} is missing library_relative_path") - if path.name.endswith(".tar.gz"): - validate_tar_archive(path, member_name) - elif path.suffix == ".zip": - validate_zip_archive(path, member_name) - else: - fail(f"{path.name} has unsupported Node direct archive extension") - - -def validate(asset_dir: Path, allow_partial: bool = False) -> None: - version = product_metadata.read_current_version("oliphaunt-node-direct") - required_assets = expected_assets(version) - addon_targets = addon_targets_by_asset(version) - missing = [asset for asset in required_assets if not (asset_dir / asset).is_file()] - if missing: - if not allow_partial: - fail("missing oliphaunt-node-direct release asset(s): " + ", ".join(missing)) - present_addons = [asset for asset in expected_addon_assets(version) if (asset_dir / asset).is_file()] - if not present_addons: - fail("partial oliphaunt-node-direct release asset validation requires at least one addon asset") - - checksum_asset = asset_dir / f"oliphaunt-node-direct-{version}-release-assets.sha256" - if not checksum_asset.is_file(): - fail(f"missing checksum manifest: {checksum_asset.name}") - checksums = checksum_manifest(checksum_asset) - for asset in required_assets: - if allow_partial and not (asset_dir / asset).is_file(): - continue - if asset == checksum_asset.name: - continue - expected_digest = checksums.get(asset) - if expected_digest is None: - fail(f"{checksum_asset.name} does not cover {asset}") - actual = sha256(asset_dir / asset) - if actual != expected_digest: - fail(f"checksum mismatch for {asset}: expected {expected_digest}, got {actual}") - for asset in expected_addon_assets(version): - if allow_partial and not (asset_dir / asset).is_file(): - continue - target = addon_targets.get(asset) - if target is None: - fail(f"no artifact target metadata found for {asset}") - validate_addon_archive(asset_dir / asset, target) - - -def main(argv: list[str]) -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--asset-dir", - default=str(ROOT / "target/oliphaunt-node-direct/release-assets"), - help="directory containing oliphaunt-node-direct release assets", - ) - parser.add_argument( - "--allow-partial", - action="store_true", - help="validate the Node direct assets present in asset-dir without requiring every published target", - ) - args = parser.parse_args(argv) - validate(Path(args.asset_dir).resolve(), allow_partial=args.allow_partial) - print(f"oliphaunt-node-direct release assets validated: {Path(args.asset_dir).resolve()}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 9e957674..a2d82a43 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -464,8 +464,8 @@ def validate_broker() -> None: "Broker runtime release must publish a checksum manifest for broker helper assets", ) require_text( - "tools/release/check_broker_release_assets.py", - "executable_relative_path", + "tools/release/check-broker-release-assets.mjs", + "executableRelativePath", "Broker runtime release asset checker must verify the metadata-declared helper executable", ) @@ -1142,12 +1142,12 @@ def validate_typescript( ) require_text( "src/runtimes/node-direct/tools/build-node-addon.sh", - "check_node_direct_release_assets.py", + "check-node-direct-release-assets.mjs", "Node direct release tooling must validate addon archives and checksums after building", ) require_text( "tools/release/release.py", - "check_node_direct_release_assets.py", + "check-node-direct-release-assets.mjs", "Node direct release publishing must validate addon archives and checksums before upload/npm staging", ) require_text( diff --git a/tools/release/package-broker-assets.sh b/tools/release/package-broker-assets.sh index bf4c90e5..42053389 100755 --- a/tools/release/package-broker-assets.sh +++ b/tools/release/package-broker-assets.sh @@ -98,5 +98,5 @@ check_args=(--asset-dir "$out_dir") if [ "${OLIPHAUNT_RELEASE_ASSET_PARTIAL:-0}" = "1" ]; then check_args+=(--allow-partial) fi -tools/release/check_broker_release_assets.py "${check_args[@]}" +bun tools/release/check-broker-release-assets.mjs "${check_args[@]}" echo "oliphauntBrokerReleaseAssetDir=$out_dir" diff --git a/tools/release/release-artifact-targets.mjs b/tools/release/release-artifact-targets.mjs new file mode 100644 index 00000000..7852d2d9 --- /dev/null +++ b/tools/release/release-artifact-targets.mjs @@ -0,0 +1,209 @@ +import fs from "node:fs/promises"; +import path from "node:path"; + +import { runMoon } from "../policy/moon.mjs"; + +export const ROOT = path.resolve(import.meta.dir, "../.."); + +const DESKTOP_TARGETS = { + "linux-arm64-gnu": { + archive: "tar.gz", + brokerExecutable: "bin/oliphaunt-broker", + nodeDirectLibrary: "oliphaunt_node.node", + }, + "linux-x64-gnu": { + archive: "tar.gz", + brokerExecutable: "bin/oliphaunt-broker", + nodeDirectLibrary: "oliphaunt_node.node", + }, + "macos-arm64": { + archive: "tar.gz", + brokerExecutable: "bin/oliphaunt-broker", + nodeDirectLibrary: "oliphaunt_node.node", + }, + "windows-x64-msvc": { + archive: "zip", + brokerExecutable: "bin/oliphaunt-broker.exe", + nodeDirectLibrary: "oliphaunt_node.node", + }, +}; + +const PRODUCT_PRESETS = { + "oliphaunt-broker": "broker-helper", + "oliphaunt-node-direct": "node-direct-addon", +}; + +export function fail(prefix, message) { + console.error(`${prefix}: ${message}`); + process.exit(1); +} + +export function compareText(left, right) { + return left < right ? -1 : left > right ? 1 : 0; +} + +export function rel(file) { + return path.relative(ROOT, file).split(path.sep).join("/"); +} + +function archiveAsset(product, target, archive) { + return `${product}-{version}-${target}.${archive}`; +} + +function parseCargoVersion(text, file, prefix) { + let inPackage = false; + for (const rawLine of text.split(/\r?\n/u)) { + const line = rawLine.trim(); + if (line === "[package]") { + inPackage = true; + continue; + } + if (inPackage && line.startsWith("[")) { + break; + } + if (!inPackage) { + continue; + } + const match = line.match(/^version\s*=\s*"([^"]+)"/u); + if (match) { + return match[1]; + } + } + fail(prefix, `${rel(file)} does not define a package version`); +} + +async function readJson(file, prefix) { + try { + return JSON.parse(await fs.readFile(file, "utf8")); + } catch (error) { + fail(prefix, `failed to read ${rel(file)}: ${error.message}`); + } +} + +function moonReleaseProducts(prefix) { + const value = JSON.parse(runMoon(["query", "projects"])); + if (!Array.isArray(value.projects)) { + fail(prefix, "moon query projects did not return a projects array"); + } + const products = new Map(); + for (const project of value.projects) { + const id = project?.id; + const release = project?.config?.project?.metadata?.release; + if (release === undefined) { + continue; + } + if (typeof id !== "string" || typeof release !== "object" || release === null) { + fail(prefix, "Moon release metadata returned an invalid product row"); + } + products.set(id, release); + } + return products; +} + +export function releaseMetadata(product, prefix) { + const release = moonReleaseProducts(prefix).get(product); + if (!release) { + fail(prefix, `Moon release metadata does not include ${product}`); + } + if (release.component !== product) { + fail(prefix, `Moon release metadata for ${product} must use matching component`); + } + if (typeof release.packagePath !== "string" || !release.packagePath) { + fail(prefix, `Moon release metadata for ${product} must declare packagePath`); + } + const artifactTargets = release.artifactTargets; + const expectedPreset = PRODUCT_PRESETS[product]; + if ( + typeof artifactTargets !== "object" || + artifactTargets === null || + artifactTargets.preset !== expectedPreset + ) { + fail(prefix, `Moon release metadata for ${product} must use artifactTargets preset ${expectedPreset}`); + } + return release; +} + +export async function currentProductVersion(product, prefix) { + const release = releaseMetadata(product, prefix); + const packagePath = release.packagePath; + const config = await readJson(path.join(ROOT, "release-please-config.json"), prefix); + const packageConfig = config.packages?.[packagePath]; + if (typeof packageConfig !== "object" || packageConfig === null) { + fail(prefix, `release-please-config.json does not include ${packagePath}`); + } + const versionFile = + packageConfig["version-file"] ?? + (packageConfig["release-type"] === "rust" + ? "Cargo.toml" + : packageConfig["release-type"] === "node" + ? "package.json" + : null); + if (typeof versionFile !== "string" || !versionFile) { + fail(prefix, `${product} release-please config must declare a supported version file`); + } + const file = path.join(ROOT, packagePath, versionFile); + const text = await fs.readFile(file, "utf8"); + if (path.basename(versionFile) === "Cargo.toml") { + return parseCargoVersion(text, file, prefix); + } + if (path.basename(versionFile) === "package.json") { + const data = JSON.parse(text); + if (typeof data.version === "string" && data.version) { + return data.version; + } + } else if (path.basename(versionFile) === "VERSION") { + const version = text.trim(); + if (version) { + return version; + } + } + fail(prefix, `${rel(file)} does not define a release version for ${product}`); +} + +export function artifactTargets(product, kind, prefix) { + const release = releaseMetadata(product, prefix); + const publishedTargets = release.artifactTargets.publishedTargets; + if ( + !Array.isArray(publishedTargets) || + !publishedTargets.every((target) => typeof target === "string" && target) + ) { + fail(prefix, `Moon release metadata for ${product} must declare publishedTargets`); + } + const targets = []; + for (const target of [...publishedTargets].sort(compareText)) { + const platform = DESKTOP_TARGETS[target]; + if (!platform) { + fail(prefix, `unknown ${product} artifact target ${target}`); + } + if (product === "oliphaunt-broker") { + targets.push({ + id: `${product}.${target}`, + product, + kind, + target, + asset: archiveAsset(product, target, platform.archive), + executableRelativePath: platform.brokerExecutable, + }); + } else if (product === "oliphaunt-node-direct") { + targets.push({ + id: `${product}.${target}`, + product, + kind, + target, + asset: archiveAsset(product, target, platform.archive), + libraryRelativePath: platform.nodeDirectLibrary, + }); + } else { + fail(prefix, `unsupported product ${product}`); + } + } + return targets; +} + +export function expectedAssets(product, kind, version, prefix) { + const assets = artifactTargets(product, kind, prefix).map((target) => + target.asset.replaceAll("{version}", version), + ); + assets.push(`${product}-${version}-release-assets.sha256`); + return assets.sort(compareText); +} diff --git a/tools/release/release-asset-validation.mjs b/tools/release/release-asset-validation.mjs new file mode 100644 index 00000000..7a233520 --- /dev/null +++ b/tools/release/release-asset-validation.mjs @@ -0,0 +1,108 @@ +import { createHash } from "node:crypto"; +import { gunzipSync } from "node:zlib"; +import fs from "node:fs/promises"; +import path from "node:path"; + +export async function assertFileExists(file) { + const stat = await fs.stat(file).catch(() => null); + return stat?.isFile() === true; +} + +export async function sha256(file) { + return createHash("sha256").update(await fs.readFile(file)).digest("hex"); +} + +export async function checksumManifest(file, fail, prefix) { + const values = new Map(); + const lines = (await fs.readFile(file, "utf8")).split(/\r?\n/u); + for (const [index, rawLine] of lines.entries()) { + const line = rawLine.trim(); + if (!line) { + continue; + } + const parts = line.split(/\s+/u); + if (parts.length < 2 || parts[0].length !== 64) { + fail(prefix, `malformed checksum line ${index + 1}: ${rawLine}`); + } + values.set(parts.slice(1).join(" ").replace(/^\.\//u, ""), parts[0].toLowerCase()); + } + return values; +} + +function parseTarString(buffer, start, length) { + const end = buffer.indexOf(0, start); + return buffer + .subarray(start, end >= start && end < start + length ? end : start + length) + .toString("utf8") + .trim(); +} + +function parseTarOctal(buffer, start, length) { + const text = parseTarString(buffer, start, length).replace(/\0/g, "").trim(); + return text ? Number.parseInt(text, 8) : 0; +} + +async function readTarGzEntries(file) { + const buffer = gunzipSync(await fs.readFile(file)); + const entries = new Map(); + for (let offset = 0; offset + 512 <= buffer.length; ) { + const header = buffer.subarray(offset, offset + 512); + if (header.every((byte) => byte === 0)) { + break; + } + const name = parseTarString(header, 0, 100); + const prefix = parseTarString(header, 345, 155); + const fullName = prefix ? `${prefix}/${name}` : name; + const mode = parseTarOctal(header, 100, 8); + const size = parseTarOctal(header, 124, 12); + const type = header.subarray(156, 157).toString("utf8"); + entries.set(fullName, { mode, size, isFile: type === "" || type === "0" }); + offset += 512 + Math.ceil(size / 512) * 512; + } + return entries; +} + +function findEndOfCentralDirectory(buffer, fail, prefix) { + for (let offset = buffer.length - 22; offset >= Math.max(0, buffer.length - 65557); offset -= 1) { + if (buffer.readUInt32LE(offset) === 0x06054b50) { + return offset; + } + } + fail(prefix, "zip archive is missing end of central directory"); +} + +async function readZipEntries(file, fail, prefix) { + const buffer = await fs.readFile(file); + const eocd = findEndOfCentralDirectory(buffer, fail, prefix); + const total = buffer.readUInt16LE(eocd + 10); + let offset = buffer.readUInt32LE(eocd + 16); + const entries = new Map(); + for (let index = 0; index < total; index += 1) { + if (buffer.readUInt32LE(offset) !== 0x02014b50) { + fail(prefix, `${path.basename(file)} has an invalid zip central directory`); + } + const size = buffer.readUInt32LE(offset + 24); + const nameLength = buffer.readUInt16LE(offset + 28); + const extraLength = buffer.readUInt16LE(offset + 30); + const commentLength = buffer.readUInt16LE(offset + 32); + const externalAttributes = buffer.readUInt32LE(offset + 38); + const name = buffer.subarray(offset + 46, offset + 46 + nameLength).toString("utf8"); + entries.set(name, { + mode: externalAttributes >>> 16, + size, + isFile: !name.endsWith("/") && (externalAttributes & 0x10) === 0, + }); + offset += 46 + nameLength + extraLength + commentLength; + } + return entries; +} + +export async function readArchiveEntries(file, fail, prefix, productLabel) { + if (file.endsWith(".tar.gz")) { + return readTarGzEntries(file); + } + if (path.extname(file) === ".zip") { + return readZipEntries(file, fail, prefix); + } + fail(prefix, `${path.basename(file)} has unsupported ${productLabel} archive extension`); +} diff --git a/tools/release/release.py b/tools/release/release.py index 2956ecbb..47e8268c 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -1262,7 +1262,7 @@ def ensure_broker_release_assets() -> None: "oliphaunt-broker-*.zip", ] ) - run(["tools/release/check_broker_release_assets.py", "--asset-dir", str(asset_dir.relative_to(ROOT))]) + run(["bun", "tools/release/check-broker-release-assets.mjs", "--asset-dir", str(asset_dir.relative_to(ROOT))]) def ensure_node_direct_release_assets() -> None: @@ -1288,7 +1288,7 @@ def ensure_node_direct_release_assets() -> None: "oliphaunt-node-direct-*.zip", ] ) - run(["tools/release/check_node_direct_release_assets.py", "--asset-dir", str(asset_dir.relative_to(ROOT))]) + run(["bun", "tools/release/check-node-direct-release-assets.mjs", "--asset-dir", str(asset_dir.relative_to(ROOT))]) def extension_package_dir(product: str) -> Path: From 0e49ced57edb617460a92078646787de7b46c5f1 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 13:21:20 +0000 Subject: [PATCH 086/137] chore: port shared fixture matrix checker to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 5 + src/shared/contracts/moon.yml | 4 +- .../contracts/tools/check-test-matrix.mjs | 524 ++++++++++++++++++ .../contracts/tools/check-test-matrix.py | 408 -------------- src/shared/fixtures/moon.yml | 4 +- tools/policy/check-repo-structure.sh | 2 +- tools/policy/python-entrypoints.allowlist | 1 - 7 files changed, 534 insertions(+), 414 deletions(-) create mode 100644 src/shared/contracts/tools/check-test-matrix.mjs delete mode 100644 src/shared/contracts/tools/check-test-matrix.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 125be912..36476166 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -274,6 +274,11 @@ review production pipelines, then normalize implementation details. and derive published target membership from Moon release metadata through `tools/release/release-artifact-targets.mjs`, keeping the helper/runtime release checks on the same target graph as CI and publication. +- The shared fixture test-matrix checker now uses Bun instead of Python. + `src/shared/contracts/tools/check-test-matrix.mjs` preserves the matrix-only + and fixture-manifest validation modes, the shared contracts/fixtures Moon + projects are modeled as JavaScript tooling, and the Python entrypoint + inventory no longer allows the retired checker path. - Rust helper inventory is currently limited to `tools/xtask` and `tools/perf/runner`. Both remain Rust-owned for now: `xtask` owns WASIX asset parsing, archive/hash work, AOT/template feature-gated paths, and release diff --git a/src/shared/contracts/moon.yml b/src/shared/contracts/moon.yml index 528b4c7c..ba2c497d 100644 --- a/src/shared/contracts/moon.yml +++ b/src/shared/contracts/moon.yml @@ -1,7 +1,7 @@ $schema: "https://moonrepo.dev/schemas/project.json" id: "shared-contracts" -language: "python" +language: "javascript" layer: "tool" stack: "infrastructure" tags: ["shared", "contracts", "fixtures"] @@ -19,7 +19,7 @@ owners: tasks: check: tags: ["quality", "static"] - command: "python3 src/shared/contracts/tools/check-test-matrix.py" + command: "bun src/shared/contracts/tools/check-test-matrix.mjs" inputs: - "/src/shared/contracts/**/*" options: diff --git a/src/shared/contracts/tools/check-test-matrix.mjs b/src/shared/contracts/tools/check-test-matrix.mjs new file mode 100644 index 00000000..4d675a3c --- /dev/null +++ b/src/shared/contracts/tools/check-test-matrix.mjs @@ -0,0 +1,524 @@ +#!/usr/bin/env bun +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..'); +const CONTRACTS_ROOT = path.join(ROOT, 'src/shared/contracts'); +const FIXTURES_ROOT = path.join(ROOT, 'src/shared/fixtures'); +const MATRIX_PATH = path.join(CONTRACTS_ROOT, 'test-matrix.toml'); +const GENERATED_MANIFEST = path.join(ROOT, 'target/shared-fixtures/manifest.generated.json'); +const GENERATED_CONSUMPTION_REPORT = path.join(ROOT, 'target/shared-fixtures/consumption-report.json'); +const ID_RE = /^[a-z0-9][a-z0-9.-]*[a-z0-9]$/u; +const FORMATS = new Set(['json', 'properties', 'tsv']); +const EVIDENCE_KINDS = new Set(['fixture-file', 'semantic-contract']); +const CONSUMPTION_SCAN_ROOTS = [ + 'src/sdks/rust/tests', + 'src/sdks/swift/Tests', + 'src/sdks/kotlin/oliphaunt/src', + 'src/sdks/js/src', + 'src/sdks/react-native/src', + 'src/bindings/wasix-rust/crates/oliphaunt-wasix/src', + 'tools/release', +]; +const CODE_SUFFIXES = new Set([ + '.bash', + '.c', + '.cjs', + '.cpp', + '.gradle', + '.h', + '.java', + '.js', + '.kt', + '.kts', + '.mjs', + '.mm', + '.py', + '.rs', + '.sh', + '.swift', + '.ts', + '.tsx', +]); +const IGNORED_DIR_NAMES = new Set([ + '.build', + '.gradle', + '.moon', + '.next', + '__pycache__', + 'build', + 'DerivedData', + 'dist', + 'lib', + 'node_modules', + 'target', +]); +const PROJECT_ROOTS = { + 'src/runtimes/liboliphaunt/native': 'liboliphaunt-native', + 'src/sdks/rust': 'oliphaunt-rust', + 'src/sdks/swift': 'oliphaunt-swift', + 'src/sdks/kotlin': 'oliphaunt-kotlin', + 'src/sdks/js': 'oliphaunt-js', + 'src/sdks/react-native': 'oliphaunt-react-native', + 'src/bindings/wasix-rust': 'oliphaunt-wasix-rust', + 'tools/policy': 'policy-tools', + 'tools/release': 'release-tools', +}; + +function fail(message) { + console.error(message); + process.exit(1); +} + +function posixRelative(file) { + return path.relative(ROOT, file).split(path.sep).join('/'); +} + +function isPlainObject(value) { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function stableValue(value) { + if (Array.isArray(value)) { + return value.map(stableValue); + } + if (!isPlainObject(value)) { + return value; + } + const sorted = {}; + for (const key of Object.keys(value).sort()) { + sorted[key] = stableValue(value[key]); + } + return sorted; +} + +function stableJson(value) { + return `${JSON.stringify(stableValue(value), null, 2)}\n`; +} + +function readText(file) { + return fs.readFileSync(file, 'utf8'); +} + +function requireString(entry, key) { + const value = entry?.[key]; + if (typeof value !== 'string' || value.length === 0) { + fail(`${MATRIX_PATH}: fixture entry missing string ${JSON.stringify(key)}`); + } + return value; +} + +function isSafeRelative(relativePath) { + const parts = relativePath.split(/[\\/]/u); + return !path.isAbsolute(relativePath) && !parts.includes('..'); +} + +function loadMatrix() { + try { + return Bun.TOML.parse(readText(MATRIX_PATH)); + } catch (error) { + fail(`${MATRIX_PATH}: invalid TOML: ${error.message}`); + } +} + +function validateFixtureEntry(entry, seen) { + const fixtureId = requireString(entry, 'id'); + if (!ID_RE.test(fixtureId)) { + fail(`${MATRIX_PATH}: invalid fixture id ${JSON.stringify(fixtureId)}`); + } + if (seen.has(fixtureId)) { + fail(`${MATRIX_PATH}: duplicate fixture id ${JSON.stringify(fixtureId)}`); + } + seen.add(fixtureId); + + const relativePath = requireString(entry, 'path'); + if (!isSafeRelative(relativePath)) { + fail(`${MATRIX_PATH}: fixture ${fixtureId} has unsafe path ${JSON.stringify(relativePath)}`); + } + + const fixtureFormat = requireString(entry, 'format'); + if (!FORMATS.has(fixtureFormat)) { + fail(`${MATRIX_PATH}: fixture ${fixtureId} has unsupported format ${JSON.stringify(fixtureFormat)}`); + } + + const contract = requireString(entry, 'contract'); + const proofOwner = requireString(entry, 'proof_owner'); + const ciTier = requireString(entry, 'ci_tier'); + if (!/^T[0-8]$/u.test(ciTier)) { + fail(`${MATRIX_PATH}: fixture ${fixtureId} has invalid ci_tier ${JSON.stringify(ciTier)}`); + } + + const consumers = entry.consumers; + if (!Array.isArray(consumers) || consumers.length === 0 || !consumers.every((item) => typeof item === 'string' && item.length > 0)) { + fail(`${MATRIX_PATH}: fixture ${fixtureId} must declare non-empty string consumers`); + } + const nonConsumers = entry.non_consumers; + if (!Array.isArray(nonConsumers) || !nonConsumers.every((item) => typeof item === 'string' && item.length > 0)) { + fail(`${MATRIX_PATH}: fixture ${fixtureId} must declare string non_consumers`); + } + const overlap = consumers.filter((consumer) => nonConsumers.includes(consumer)).sort(); + if (overlap.length > 0) { + fail(`${MATRIX_PATH}: fixture ${fixtureId} declares consumers as non-consumers: ${JSON.stringify(overlap)}`); + } + + const shared = entry.shared; + if (typeof shared !== 'boolean') { + fail(`${MATRIX_PATH}: fixture ${fixtureId} must declare shared = true/false`); + } + if (shared && new Set(consumers).size < 2) { + fail(`${MATRIX_PATH}: shared fixture ${fixtureId} must have at least two consumers`); + } + if (!shared && typeof entry.reason !== 'string') { + fail(`${MATRIX_PATH}: product-specific fixture ${fixtureId} must explain why it is cataloged`); + } + + const evidence = entry.evidence ?? []; + if (!Array.isArray(evidence) || evidence.length === 0) { + fail(`${MATRIX_PATH}: fixture ${fixtureId} must declare evidence for every consumer`); + } + const evidenceConsumers = []; + for (const item of evidence) { + if (!isPlainObject(item)) { + fail(`${MATRIX_PATH}: fixture ${fixtureId} evidence entries must be TOML tables`); + } + const consumer = requireString(item, 'consumer'); + if (!consumers.includes(consumer)) { + fail(`${MATRIX_PATH}: fixture ${fixtureId} has evidence for undeclared consumer ${JSON.stringify(consumer)}`); + } + evidenceConsumers.push(consumer); + const kind = item.kind ?? 'fixture-file'; + if (!EVIDENCE_KINDS.has(kind)) { + fail(`${MATRIX_PATH}: fixture ${fixtureId} evidence for ${consumer} has unsupported kind ${JSON.stringify(kind)}`); + } + const evidencePath = requireString(item, 'path'); + if (!isSafeRelative(evidencePath)) { + fail(`${MATRIX_PATH}: fixture ${fixtureId} evidence for ${consumer} has unsafe path ${JSON.stringify(evidencePath)}`); + } + const markers = item.markers; + if (!Array.isArray(markers) || markers.length === 0 || !markers.every((marker) => typeof marker === 'string' && marker.length > 0)) { + fail(`${MATRIX_PATH}: fixture ${fixtureId} evidence for ${consumer} must declare non-empty string markers`); + } + } + const missingEvidence = consumers.filter((consumer) => !evidenceConsumers.includes(consumer)).sort(); + if (missingEvidence.length > 0) { + fail(`${MATRIX_PATH}: fixture ${fixtureId} lacks evidence for consumers: ${JSON.stringify(missingEvidence)}`); + } + + return { + id: fixtureId, + path: relativePath, + format: fixtureFormat, + contract, + proof_owner: proofOwner, + ci_tier: ciTier, + shared, + consumers, + non_consumers: nonConsumers, + evidence, + }; +} + +function validateProperties(file) { + const entries = readText(file) + .split(/\r?\n/u) + .filter((line) => line.trim().length > 0 && !line.trimStart().startsWith('#')); + if (entries.length === 0) { + fail(`${file}: properties fixture is empty`); + } + for (const line of entries) { + if (!line.includes('=')) { + fail(`${file}: properties line lacks '=': ${JSON.stringify(line)}`); + } + } +} + +function parseTsvLine(line) { + const cells = []; + let cell = ''; + let quoted = false; + for (let index = 0; index < line.length; index += 1) { + const char = line[index]; + if (char === '"') { + if (quoted && line[index + 1] === '"') { + cell += '"'; + index += 1; + } else { + quoted = !quoted; + } + continue; + } + if (char === '\t' && !quoted) { + cells.push(cell); + cell = ''; + continue; + } + cell += char; + } + cells.push(cell); + return cells; +} + +function validateTsv(file) { + const rows = readText(file) + .replace(/\r\n/gu, '\n') + .replace(/\r/gu, '\n') + .split('\n') + .filter((line, index, lines) => index < lines.length - 1 || line.length > 0) + .map(parseTsvLine); + if (rows.length < 2) { + fail(`${file}: TSV fixture must contain a header and at least one data row`); + } + const width = rows[0].length; + if (width === 0) { + fail(`${file}: TSV fixture header is empty`); + } + rows.slice(1).forEach((row, index) => { + if (row.length !== width) { + fail(`${file}: row ${index + 2} has ${row.length} cells, expected ${width}`); + } + }); +} + +function validateEvidenceFile(fixture, evidence) { + const evidencePath = path.join(ROOT, evidence.path); + if (!fs.existsSync(evidencePath) || !fs.statSync(evidencePath).isFile()) { + fail(`${MATRIX_PATH}: fixture ${fixture.id} evidence file does not exist: ${evidencePath}`); + } + const text = readText(evidencePath); + for (const marker of evidence.markers) { + if (!text.includes(marker)) { + fail( + `${MATRIX_PATH}: fixture ${fixture.id} evidence file ${evidence.path} ` + + `for ${evidence.consumer} lacks marker ${JSON.stringify(marker)}`, + ); + } + } + return { + consumer: evidence.consumer, + kind: evidence.kind ?? 'fixture-file', + path: evidence.path, + markers: evidence.markers, + }; +} + +function validateFixtureFile(entry) { + const fixturePath = path.join(FIXTURES_ROOT, entry.path); + if (!fs.existsSync(fixturePath) || !fs.statSync(fixturePath).isFile()) { + fail(`missing shared fixture ${fixturePath}`); + } + + if (entry.format === 'json') { + const parsed = JSON.parse(readText(fixturePath)); + if (!isPlainObject(parsed)) { + fail(`${fixturePath}: JSON fixture must be an object`); + } + } else if (entry.format === 'properties') { + validateProperties(fixturePath); + } else if (entry.format === 'tsv') { + validateTsv(fixturePath); + } + + return { + id: entry.id, + path: `src/shared/fixtures/${entry.path}`, + format: entry.format, + proofOwner: entry.proof_owner, + ciTier: entry.ci_tier, + consumers: entry.consumers, + nonConsumers: entry.non_consumers, + shared: entry.shared, + evidence: entry.evidence.map((evidence) => validateEvidenceFile(entry, evidence)), + }; +} + +function loadProjectRoots() { + const roots = { ...PROJECT_ROOTS }; + for (const [root, projectId] of Object.entries(PROJECT_ROOTS)) { + const moonFile = path.join(ROOT, root, 'moon.yml'); + if (!fs.existsSync(moonFile) || !fs.statSync(moonFile).isFile()) { + fail(`${MATRIX_PATH}: fixture matrix project root ${root} is missing moon.yml`); + } + const match = readText(moonFile).match(/^id:\s*["']?([^"'\s#]+)/mu); + if (match === null) { + fail(`${MATRIX_PATH}: fixture matrix project root ${root} moon.yml has no id`); + } + const actualProjectId = match[1]; + if (actualProjectId !== projectId) { + fail(`${MATRIX_PATH}: fixture matrix project root ${root} expected id ${projectId}, got ${actualProjectId}`); + } + } + return roots; +} + +function projectForPath(file, projectRoots) { + const relative = posixRelative(file); + let bestRoot = ''; + let bestProject = null; + for (const [root, projectId] of Object.entries(projectRoots)) { + if (relative === root || relative.startsWith(`${root}/`)) { + if (root.length > bestRoot.length) { + bestRoot = root; + bestProject = projectId; + } + } + } + return bestProject; +} + +function validateProjectIds(entries, projectRoots) { + const knownIds = new Set(Object.values(projectRoots)); + for (const entry of entries) { + const ids = new Set([ + ...entry.consumers, + ...entry.non_consumers, + ...entry.evidence.map((evidence) => evidence.consumer), + ]); + const unknown = [...ids].filter((id) => !knownIds.has(id)).sort(); + if (unknown.length > 0) { + fail(`${MATRIX_PATH}: fixture ${entry.id} references unknown Moon project ids: ${JSON.stringify(unknown)}`); + } + } +} + +function* walkFiles(root) { + if (!fs.existsSync(root)) { + return; + } + const entries = fs.readdirSync(root, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name)); + for (const entry of entries) { + const file = path.join(root, entry.name); + if (entry.isDirectory()) { + if (!IGNORED_DIR_NAMES.has(entry.name)) { + yield* walkFiles(file); + } + continue; + } + if (entry.isFile()) { + yield file; + } + } +} + +function detectFixtureReferences(entries, projectRoots) { + const byPattern = new Map(); + for (const entry of entries) { + byPattern.set(`src/shared/fixtures/${entry.path}`, entry); + byPattern.set(entry.path, entry); + } + + const detections = []; + const seen = new Set(); + for (const scanRoot of CONSUMPTION_SCAN_ROOTS) { + for (const file of walkFiles(path.join(ROOT, scanRoot))) { + if (!CODE_SUFFIXES.has(path.extname(file))) { + continue; + } + const relativeParts = posixRelative(file).split('/'); + if (relativeParts.some((part) => IGNORED_DIR_NAMES.has(part))) { + continue; + } + let text; + try { + text = readText(file); + } catch (error) { + if (error instanceof TypeError) { + continue; + } + throw error; + } + for (const [pattern, entry] of byPattern.entries()) { + if (!text.includes(pattern)) { + continue; + } + const projectId = projectForPath(file, projectRoots); + if (projectId === null) { + fail(`${MATRIX_PATH}: fixture reference in unmanaged path ${posixRelative(file)}`); + } + if (entry.non_consumers.includes(projectId) || !entry.consumers.includes(projectId)) { + fail( + `${MATRIX_PATH}: ${projectId} references fixture ${entry.id} from ${posixRelative(file)}, ` + + `but allowed consumers are ${JSON.stringify(entry.consumers)}`, + ); + } + const detectionKey = `${entry.id}\0${projectId}\0${posixRelative(file)}`; + if (seen.has(detectionKey)) { + continue; + } + seen.add(detectionKey); + detections.push({ + fixtureId: entry.id, + project: projectId, + path: posixRelative(file), + matched: pattern, + }); + } + } + } + return detections; +} + +function writeConsumptionReport(entries, detections) { + const detectionsByFixture = new Map(entries.map((entry) => [entry.id, []])); + for (const detection of detections) { + if (!detectionsByFixture.has(detection.fixtureId)) { + detectionsByFixture.set(detection.fixtureId, []); + } + detectionsByFixture.get(detection.fixtureId).push(detection); + } + + const report = { + schemaVersion: 1, + fixtures: entries.map((entry) => ({ + id: entry.id, + path: `src/shared/fixtures/${entry.path}`, + consumers: entry.consumers, + evidence: entry.evidence.map((evidence) => ({ + consumer: evidence.consumer, + kind: evidence.kind ?? 'fixture-file', + path: evidence.path, + })), + detectedReferences: detectionsByFixture.get(entry.id) ?? [], + })), + }; + fs.mkdirSync(path.dirname(GENERATED_CONSUMPTION_REPORT), { recursive: true }); + fs.writeFileSync(GENERATED_CONSUMPTION_REPORT, stableJson(report), 'utf8'); +} + +function parseArgs(argv) { + let fixtures = false; + for (const arg of argv) { + if (arg === '--fixtures') { + fixtures = true; + } else { + fail(`unknown argument: ${arg}`); + } + } + return { fixtures }; +} + +const args = parseArgs(Bun.argv.slice(2)); +const matrix = loadMatrix(); +if (matrix.schema_version !== 1) { + fail(`${MATRIX_PATH}: schema_version must be 1`); +} +const rawFixtures = matrix.fixtures; +if (!Array.isArray(rawFixtures) || rawFixtures.length === 0) { + fail(`${MATRIX_PATH}: must declare at least one [[fixtures]] entry`); +} + +const seen = new Set(); +const entries = rawFixtures.map((entry) => validateFixtureEntry(entry, seen)); + +if (args.fixtures) { + const projectRoots = loadProjectRoots(); + validateProjectIds(entries, projectRoots); + const detections = detectFixtureReferences(entries, projectRoots); + const generated = { + schemaVersion: 1, + fixtures: entries.map(validateFixtureFile), + }; + fs.mkdirSync(path.dirname(GENERATED_MANIFEST), { recursive: true }); + fs.writeFileSync(GENERATED_MANIFEST, stableJson(generated), 'utf8'); + writeConsumptionReport(entries, detections); +} diff --git a/src/shared/contracts/tools/check-test-matrix.py b/src/shared/contracts/tools/check-test-matrix.py deleted file mode 100644 index 29230a77..00000000 --- a/src/shared/contracts/tools/check-test-matrix.py +++ /dev/null @@ -1,408 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import argparse -import csv -import json -import re -import sys -import tomllib -from pathlib import Path - - -ROOT = Path(__file__).resolve().parents[4] -CONTRACTS_ROOT = ROOT / "src/shared/contracts" -FIXTURES_ROOT = ROOT / "src/shared/fixtures" -MATRIX_PATH = CONTRACTS_ROOT / "test-matrix.toml" -GENERATED_MANIFEST = ROOT / "target/shared-fixtures/manifest.generated.json" -GENERATED_CONSUMPTION_REPORT = ROOT / "target/shared-fixtures/consumption-report.json" -ID_RE = re.compile(r"^[a-z0-9][a-z0-9.-]*[a-z0-9]$") -FORMATS = {"json", "properties", "tsv"} -EVIDENCE_KINDS = {"fixture-file", "semantic-contract"} -CONSUMPTION_SCAN_ROOTS = [ - "src/sdks/rust/tests", - "src/sdks/swift/Tests", - "src/sdks/kotlin/oliphaunt/src", - "src/sdks/js/src", - "src/sdks/react-native/src", - "src/bindings/wasix-rust/crates/oliphaunt-wasix/src", - "tools/release", -] -CODE_SUFFIXES = { - ".bash", - ".c", - ".cjs", - ".cpp", - ".gradle", - ".h", - ".java", - ".js", - ".kt", - ".kts", - ".mjs", - ".mm", - ".py", - ".rs", - ".sh", - ".swift", - ".ts", - ".tsx", -} -IGNORED_DIR_NAMES = { - ".build", - ".gradle", - ".moon", - ".next", - "__pycache__", - "build", - "DerivedData", - "dist", - "lib", - "node_modules", - "target", -} -PROJECT_ROOTS = { - "src/runtimes/liboliphaunt/native": "liboliphaunt-native", - "src/sdks/rust": "oliphaunt-rust", - "src/sdks/swift": "oliphaunt-swift", - "src/sdks/kotlin": "oliphaunt-kotlin", - "src/sdks/js": "oliphaunt-js", - "src/sdks/react-native": "oliphaunt-react-native", - "src/bindings/wasix-rust": "oliphaunt-wasix-rust", - "tools/policy": "policy-tools", - "tools/release": "release-tools", -} - - -def fail(message: str) -> None: - raise SystemExit(message) - - -def load_matrix() -> dict: - try: - with MATRIX_PATH.open("rb") as handle: - return tomllib.load(handle) - except tomllib.TOMLDecodeError as error: - fail(f"{MATRIX_PATH}: invalid TOML: {error}") - - -def validate_fixture_entry(entry: dict, seen: set[str]) -> dict: - fixture_id = require_string(entry, "id") - if not ID_RE.match(fixture_id): - fail(f"{MATRIX_PATH}: invalid fixture id {fixture_id!r}") - if fixture_id in seen: - fail(f"{MATRIX_PATH}: duplicate fixture id {fixture_id!r}") - seen.add(fixture_id) - - relative_path = require_string(entry, "path") - path = Path(relative_path) - if path.is_absolute() or ".." in path.parts: - fail(f"{MATRIX_PATH}: fixture {fixture_id} has unsafe path {relative_path!r}") - - fixture_format = require_string(entry, "format") - if fixture_format not in FORMATS: - fail(f"{MATRIX_PATH}: fixture {fixture_id} has unsupported format {fixture_format!r}") - - contract = require_string(entry, "contract") - proof_owner = require_string(entry, "proof_owner") - ci_tier = require_string(entry, "ci_tier") - if not re.match(r"^T[0-8]$", ci_tier): - fail(f"{MATRIX_PATH}: fixture {fixture_id} has invalid ci_tier {ci_tier!r}") - consumers = entry.get("consumers") - if not isinstance(consumers, list) or not consumers or not all(isinstance(item, str) and item for item in consumers): - fail(f"{MATRIX_PATH}: fixture {fixture_id} must declare non-empty string consumers") - non_consumers = entry.get("non_consumers") - if not isinstance(non_consumers, list) or not all(isinstance(item, str) and item for item in non_consumers): - fail(f"{MATRIX_PATH}: fixture {fixture_id} must declare string non_consumers") - overlap = set(consumers).intersection(non_consumers) - if overlap: - fail(f"{MATRIX_PATH}: fixture {fixture_id} declares consumers as non-consumers: {sorted(overlap)}") - - shared = entry.get("shared") - if not isinstance(shared, bool): - fail(f"{MATRIX_PATH}: fixture {fixture_id} must declare shared = true/false") - if shared and len(set(consumers)) < 2: - fail(f"{MATRIX_PATH}: shared fixture {fixture_id} must have at least two consumers") - if not shared and not isinstance(entry.get("reason"), str): - fail(f"{MATRIX_PATH}: product-specific fixture {fixture_id} must explain why it is cataloged") - evidence = entry.get("evidence", []) - if not isinstance(evidence, list) or not evidence: - fail(f"{MATRIX_PATH}: fixture {fixture_id} must declare evidence for every consumer") - evidence_consumers: list[str] = [] - for item in evidence: - if not isinstance(item, dict): - fail(f"{MATRIX_PATH}: fixture {fixture_id} evidence entries must be TOML tables") - consumer = require_string(item, "consumer") - if consumer not in consumers: - fail(f"{MATRIX_PATH}: fixture {fixture_id} has evidence for undeclared consumer {consumer!r}") - evidence_consumers.append(consumer) - kind = item.get("kind", "fixture-file") - if kind not in EVIDENCE_KINDS: - fail(f"{MATRIX_PATH}: fixture {fixture_id} evidence for {consumer} has unsupported kind {kind!r}") - evidence_path = require_string(item, "path") - path = Path(evidence_path) - if path.is_absolute() or ".." in path.parts: - fail(f"{MATRIX_PATH}: fixture {fixture_id} evidence for {consumer} has unsafe path {evidence_path!r}") - markers = item.get("markers") - if not isinstance(markers, list) or not markers or not all(isinstance(marker, str) and marker for marker in markers): - fail(f"{MATRIX_PATH}: fixture {fixture_id} evidence for {consumer} must declare non-empty string markers") - missing_evidence = sorted(set(consumers).difference(evidence_consumers)) - if missing_evidence: - fail(f"{MATRIX_PATH}: fixture {fixture_id} lacks evidence for consumers: {missing_evidence}") - - return { - "id": fixture_id, - "path": relative_path, - "format": fixture_format, - "contract": contract, - "proof_owner": proof_owner, - "ci_tier": ci_tier, - "shared": shared, - "consumers": consumers, - "non_consumers": non_consumers, - "evidence": evidence, - } - - -def require_string(entry: dict, key: str) -> str: - value = entry.get(key) - if not isinstance(value, str) or not value: - fail(f"{MATRIX_PATH}: fixture entry missing string {key!r}") - return value - - -def validate_fixture_file(entry: dict) -> dict: - relative_path = entry["path"] - fixture_path = FIXTURES_ROOT / relative_path - if not fixture_path.is_file(): - fail(f"missing shared fixture {fixture_path}") - - if entry["format"] == "json": - with fixture_path.open("r", encoding="utf-8") as handle: - parsed = json.load(handle) - if not isinstance(parsed, dict): - fail(f"{fixture_path}: JSON fixture must be an object") - elif entry["format"] == "properties": - validate_properties(fixture_path) - elif entry["format"] == "tsv": - validate_tsv(fixture_path) - - return { - "id": entry["id"], - "path": f"src/shared/fixtures/{relative_path}", - "format": entry["format"], - "proofOwner": entry["proof_owner"], - "ciTier": entry["ci_tier"], - "consumers": entry["consumers"], - "nonConsumers": entry["non_consumers"], - "shared": entry["shared"], - "evidence": [ - validate_evidence_file(entry, evidence) - for evidence in entry["evidence"] - ], - } - - -def validate_evidence_file(fixture: dict, evidence: dict) -> dict: - evidence_path = ROOT / evidence["path"] - if not evidence_path.is_file(): - fail(f"{MATRIX_PATH}: fixture {fixture['id']} evidence file does not exist: {evidence_path}") - text = evidence_path.read_text(encoding="utf-8") - for marker in evidence["markers"]: - if marker not in text: - fail( - f"{MATRIX_PATH}: fixture {fixture['id']} evidence file {evidence['path']} " - f"for {evidence['consumer']} lacks marker {marker!r}" - ) - return { - "consumer": evidence["consumer"], - "kind": evidence.get("kind", "fixture-file"), - "path": evidence["path"], - "markers": evidence["markers"], - } - - -def load_project_roots() -> dict[str, str]: - roots = dict(PROJECT_ROOTS) - for root, project_id in PROJECT_ROOTS.items(): - moon_file = ROOT / root / "moon.yml" - if not moon_file.is_file(): - fail(f"{MATRIX_PATH}: fixture matrix project root {root} is missing moon.yml") - match = re.search(r"(?m)^id:\s*[\"']?([^\"'\s#]+)", moon_file.read_text(encoding="utf-8")) - if not match: - fail(f"{MATRIX_PATH}: fixture matrix project root {root} moon.yml has no id") - actual_project_id = match.group(1) - if actual_project_id != project_id: - fail( - f"{MATRIX_PATH}: fixture matrix project root {root} expected id " - f"{project_id}, got {actual_project_id}" - ) - return roots - - -def project_for_path(path: Path, project_roots: dict[str, str]) -> str | None: - relative = path.relative_to(ROOT).as_posix() - best_root = "" - best_project: str | None = None - for root, project_id in project_roots.items(): - if relative == root or relative.startswith(f"{root}/"): - if len(root) > len(best_root): - best_root = root - best_project = project_id - return best_project - - -def validate_project_ids(entries: list[dict], project_roots: dict[str, str]) -> None: - known_ids = set(project_roots.values()) - for entry in entries: - ids = set(entry["consumers"]) | set(entry["non_consumers"]) - ids.update(evidence["consumer"] for evidence in entry["evidence"]) - unknown = sorted(ids.difference(known_ids)) - if unknown: - fail(f"{MATRIX_PATH}: fixture {entry['id']} references unknown Moon project ids: {unknown}") - - -def detect_fixture_references(entries: list[dict], project_roots: dict[str, str]) -> list[dict]: - by_pattern: dict[str, dict] = {} - for entry in entries: - relative_path = entry["path"] - by_pattern[f"src/shared/fixtures/{relative_path}"] = entry - by_pattern[relative_path] = entry - - detections: list[dict] = [] - seen: set[tuple[str, str, str]] = set() - for scan_root in CONSUMPTION_SCAN_ROOTS: - root = ROOT / scan_root - if not root.exists(): - continue - for path in root.rglob("*"): - if not path.is_file() or path.suffix not in CODE_SUFFIXES: - continue - relative_parts = path.relative_to(ROOT).parts - if any(part in IGNORED_DIR_NAMES for part in relative_parts): - continue - try: - text = path.read_text(encoding="utf-8") - except UnicodeDecodeError: - continue - for pattern, entry in by_pattern.items(): - if pattern not in text: - continue - project_id = project_for_path(path, project_roots) - if project_id is None: - fail(f"{MATRIX_PATH}: fixture reference in unmanaged path {path.relative_to(ROOT)}") - if project_id in entry["non_consumers"] or project_id not in entry["consumers"]: - fail( - f"{MATRIX_PATH}: {project_id} references fixture {entry['id']} " - f"from {path.relative_to(ROOT)}, but allowed consumers are {entry['consumers']}" - ) - detection_key = (entry["id"], project_id, path.relative_to(ROOT).as_posix()) - if detection_key in seen: - continue - seen.add(detection_key) - detections.append( - { - "fixtureId": entry["id"], - "project": project_id, - "path": path.relative_to(ROOT).as_posix(), - "matched": pattern, - } - ) - return detections - - -def write_consumption_report(entries: list[dict], detections: list[dict]) -> None: - detections_by_fixture: dict[str, list[dict]] = {entry["id"]: [] for entry in entries} - for detection in detections: - detections_by_fixture.setdefault(detection["fixtureId"], []).append(detection) - - report = { - "schemaVersion": 1, - "fixtures": [ - { - "id": entry["id"], - "path": f"src/shared/fixtures/{entry['path']}", - "consumers": entry["consumers"], - "evidence": [ - { - "consumer": evidence["consumer"], - "kind": evidence.get("kind", "fixture-file"), - "path": evidence["path"], - } - for evidence in entry["evidence"] - ], - "detectedReferences": detections_by_fixture.get(entry["id"], []), - } - for entry in entries - ], - } - GENERATED_CONSUMPTION_REPORT.parent.mkdir(parents=True, exist_ok=True) - GENERATED_CONSUMPTION_REPORT.write_text( - json.dumps(report, indent=2, sort_keys=True) + "\n", - encoding="utf-8", - ) - - -def validate_properties(path: Path) -> None: - lines = path.read_text(encoding="utf-8").splitlines() - entries = [ - line - for line in lines - if line.strip() and not line.lstrip().startswith("#") - ] - if not entries: - fail(f"{path}: properties fixture is empty") - for line in entries: - if "=" not in line: - fail(f"{path}: properties line lacks '=': {line!r}") - - -def validate_tsv(path: Path) -> None: - with path.open("r", encoding="utf-8", newline="") as handle: - rows = list(csv.reader(handle, delimiter="\t")) - if len(rows) < 2: - fail(f"{path}: TSV fixture must contain a header and at least one data row") - width = len(rows[0]) - if width == 0: - fail(f"{path}: TSV fixture header is empty") - for index, row in enumerate(rows[1:], start=2): - if len(row) != width: - fail(f"{path}: row {index} has {len(row)} cells, expected {width}") - - -def main() -> int: - parser = argparse.ArgumentParser() - parser.add_argument( - "--fixtures", - action="store_true", - help="also validate fixture files and emit the generated manifest", - ) - args = parser.parse_args() - - matrix = load_matrix() - if matrix.get("schema_version") != 1: - fail(f"{MATRIX_PATH}: schema_version must be 1") - raw_fixtures = matrix.get("fixtures") - if not isinstance(raw_fixtures, list) or not raw_fixtures: - fail(f"{MATRIX_PATH}: must declare at least one [[fixtures]] entry") - - seen: set[str] = set() - entries = [validate_fixture_entry(entry, seen) for entry in raw_fixtures] - - if args.fixtures: - project_roots = load_project_roots() - validate_project_ids(entries, project_roots) - detections = detect_fixture_references(entries, project_roots) - generated = { - "schemaVersion": 1, - "fixtures": [validate_fixture_file(entry) for entry in entries], - } - GENERATED_MANIFEST.parent.mkdir(parents=True, exist_ok=True) - GENERATED_MANIFEST.write_text(json.dumps(generated, indent=2, sort_keys=True) + "\n", encoding="utf-8") - write_consumption_report(entries, detections) - - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/src/shared/fixtures/moon.yml b/src/shared/fixtures/moon.yml index c8711b85..1de8cd05 100644 --- a/src/shared/fixtures/moon.yml +++ b/src/shared/fixtures/moon.yml @@ -1,7 +1,7 @@ $schema: "https://moonrepo.dev/schemas/project.json" id: "shared-fixtures" -language: "unknown" +language: "javascript" layer: "tool" stack: "infrastructure" tags: ["shared", "fixtures", "tests"] @@ -22,7 +22,7 @@ dependsOn: tasks: check: tags: ["quality", "static"] - command: "python3 src/shared/contracts/tools/check-test-matrix.py --fixtures" + command: "bun src/shared/contracts/tools/check-test-matrix.mjs --fixtures" deps: - "shared-contracts:check" inputs: diff --git a/tools/policy/check-repo-structure.sh b/tools/policy/check-repo-structure.sh index edf395f3..9fbbd002 100755 --- a/tools/policy/check-repo-structure.sh +++ b/tools/policy/check-repo-structure.sh @@ -219,7 +219,7 @@ require_file tools/graph/synthetic/release.toml require_file tools/graph/synthetic/coverage.toml require_file src/shared/contracts/moon.yml require_file src/shared/contracts/test-matrix.toml -require_file src/shared/contracts/tools/check-test-matrix.py +require_file src/shared/contracts/tools/check-test-matrix.mjs require_file src/shared/fixtures/moon.yml require_file src/shared/fixtures/manifest.toml require_file .github/scripts/run-affected-moon-task.sh diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index 36931285..53cfb28f 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -1,7 +1,6 @@ # Intentional Python tooling inventory. # New Python files should be ported to Bun or deliberately added here. src/extensions/tools/check-extension-model.py -src/shared/contracts/tools/check-test-matrix.py tools/coverage/coverage.py tools/graph/affected.py tools/graph/ci_plan.py From 8de11ba7d0e20c24ccc45d32d505c4d61d303b80 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 13:32:52 +0000 Subject: [PATCH 087/137] chore: port release pr coverage check to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 4 + tools/policy/check-release-policy.py | 2 +- tools/policy/python-entrypoints.allowlist | 1 - tools/release/check_release_pr_coverage.mjs | 169 ++++++++++++++++++ tools/release/check_release_pr_coverage.py | 124 ------------- tools/release/release.py | 2 +- 6 files changed, 175 insertions(+), 127 deletions(-) create mode 100644 tools/release/check_release_pr_coverage.mjs delete mode 100755 tools/release/check_release_pr_coverage.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 36476166..afa7dc53 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -279,6 +279,10 @@ review production pipelines, then normalize implementation details. and fixture-manifest validation modes, the shared contracts/fixtures Moon projects are modeled as JavaScript tooling, and the Python entrypoint inventory no longer allows the retired checker path. +- Release PR product-version coverage now uses Bun instead of Python. + `tools/release/check_release_pr_coverage.mjs` keeps release-please manifest + diffs tied to `tools/release/release.py plan --format json`, and the release + check command invokes the Bun checker directly. - Rust helper inventory is currently limited to `tools/xtask` and `tools/perf/runner`. Both remain Rust-owned for now: `xtask` owns WASIX asset parsing, archive/hash work, AOT/template feature-gated paths, and release diff --git a/tools/policy/check-release-policy.py b/tools/policy/check-release-policy.py index 389068fa..cbdfe19d 100644 --- a/tools/policy/check-release-policy.py +++ b/tools/policy/check-release-policy.py @@ -630,7 +630,7 @@ def check_ci_policy() -> None: fail(f"missing consumer shape fixture: {CONSUMER_SHAPE_PRODUCTS_FIXTURE}") assert_contains( "tools/release/release.py", - "check_release_pr_coverage.py", + "check_release_pr_coverage.mjs", "release checks must verify release-please version bumps cover Moon-selected products", ) for path in ( diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index 53cfb28f..8d348ade 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -18,7 +18,6 @@ tools/release/check_github_release_assets.py tools/release/check_liboliphaunt_release_assets.py tools/release/check_registry_publication.py tools/release/check_release_metadata.py -tools/release/check_release_pr_coverage.py tools/release/check_release_versions.py tools/release/check_staged_artifacts.py tools/release/extension_artifact_targets.py diff --git a/tools/release/check_release_pr_coverage.mjs b/tools/release/check_release_pr_coverage.mjs new file mode 100644 index 00000000..2a4699fc --- /dev/null +++ b/tools/release/check_release_pr_coverage.mjs @@ -0,0 +1,169 @@ +#!/usr/bin/env bun +import { spawnSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); +const MANIFEST = '.release-please-manifest.json'; + +function fail(message) { + console.error(`check_release_pr_coverage.mjs: ${message}`); + process.exit(1); +} + +function run(command, args, { check = true } = {}) { + const result = spawnSync(command, args, { + cwd: ROOT, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + if (result.error) { + if (check) { + fail(`failed to run ${command}: ${result.error.message}`); + } + return result; + } + if (check && result.status !== 0) { + fail(`${command} ${args.join(' ')} failed: ${result.stderr.trim()}`); + } + return result; +} + +function git(args, options = {}) { + return run('git', args, options); +} + +function gitStdout(args) { + return git(args).stdout; +} + +function refExists(ref) { + return git(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], { check: false }).status === 0; +} + +function baseRef() { + const candidates = []; + const baseBranch = process.env.GITHUB_BASE_REF; + if (baseBranch) { + candidates.push(`origin/${baseBranch}`, baseBranch); + } + candidates.push('origin/main', 'main'); + return candidates.find(refExists) ?? null; +} + +function parseJsonObject(raw, context) { + let value; + try { + value = JSON.parse(raw); + } catch (error) { + fail(`${context} must be valid JSON: ${error.message}`); + } + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + fail(`${context} must be a JSON object`); + } + return value; +} + +function requireStringObject(value, context) { + if ( + value === null || + typeof value !== 'object' || + Array.isArray(value) || + Object.entries(value).some(([key, item]) => typeof key !== 'string' || typeof item !== 'string') + ) { + fail(`${context} must be a JSON string object`); + } + return value; +} + +function manifestAt(ref) { + if (git(['cat-file', '-e', `${ref}:${MANIFEST}`], { check: false }).status !== 0) { + return {}; + } + const raw = gitStdout(['show', `${ref}:${MANIFEST}`]); + return requireStringObject(parseJsonObject(raw, `${MANIFEST} at ${ref}`), `${MANIFEST} at ${ref}`); +} + +function currentManifest() { + const raw = fs.readFileSync(path.join(ROOT, MANIFEST), 'utf8'); + return requireStringObject(parseJsonObject(raw, MANIFEST), MANIFEST); +} + +function releasePleaseProductPaths() { + const config = parseJsonObject( + fs.readFileSync(path.join(ROOT, 'release-please-config.json'), 'utf8'), + 'release-please-config.json', + ); + const packages = config.packages; + if (packages === null || typeof packages !== 'object' || Array.isArray(packages)) { + fail('release-please-config.json must define packages'); + } + const productPaths = new Map(); + for (const [packagePath, packageConfig] of Object.entries(packages)) { + const component = packageConfig?.component; + if (typeof component !== 'string' || component.length === 0) { + fail(`release-please package ${packagePath} must define component`); + } + if (productPaths.has(component)) { + fail(`release-please-config.json declares duplicate component ${component}`); + } + productPaths.set(component, packagePath); + } + return productPaths; +} + +function releasePlan(ref) { + const result = run('tools/release/release.py', [ + 'plan', + '--base-ref', + ref, + '--head-ref', + 'HEAD', + '--format', + 'json', + ]); + return parseJsonObject(result.stdout, 'release plan output'); +} + +const ref = baseRef(); +if (ref === null) { + fail('could not resolve base ref for release PR coverage check'); +} + +const plan = releasePlan(ref); +const files = Array.isArray(plan.changedFiles) ? plan.changedFiles : []; +if (!files.includes(MANIFEST)) { + console.log('release PR coverage check skipped; release-please manifest is unchanged'); + process.exit(0); +} + +const beforeManifest = manifestAt(ref); +const afterManifest = currentManifest(); +const productPaths = releasePleaseProductPaths(); +const knownProducts = new Set(Array.isArray(plan.productIds) ? plan.productIds : []); +const versionedProducts = new Set(); + +for (const [product, packagePath] of productPaths.entries()) { + if (beforeManifest[packagePath] !== afterManifest[packagePath]) { + versionedProducts.add(product); + } +} + +const selectedProducts = new Set(Array.isArray(plan.releaseProducts) ? plan.releaseProducts : []); +const missing = [...selectedProducts].filter(product => !versionedProducts.has(product)).sort(); +if (missing.length > 0) { + fail( + 'release-please did not version every Moon-selected release product. ' + + 'Moon remains the dependency authority, but release-please must own ' + + 'the corresponding versions/tags. Missing product version bumps: ' + + missing.join(', '), + ); +} + +const unknownVersioned = [...versionedProducts].filter(product => !knownProducts.has(product)).sort(); +if (unknownVersioned.length > 0) { + fail(`${MANIFEST} changed unknown products: ${unknownVersioned.join(', ')}`); +} + +console.log('release PR product coverage checks passed'); diff --git a/tools/release/check_release_pr_coverage.py b/tools/release/check_release_pr_coverage.py deleted file mode 100755 index 76711574..00000000 --- a/tools/release/check_release_pr_coverage.py +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env python3 -"""Ensure release-please version bumps cover Moon-selected release products.""" - -from __future__ import annotations - -import json -import os -import subprocess -import sys -from typing import NoReturn - -import product_metadata -import release_plan - - -ROOT = product_metadata.ROOT -MANIFEST = ".release-please-manifest.json" - - -def fail(message: str) -> NoReturn: - print(f"check_release_pr_coverage.py: {message}", file=sys.stderr) - raise SystemExit(1) - - -def git(args: list[str], *, check: bool = True) -> subprocess.CompletedProcess[str]: - return subprocess.run( - ["git", *args], - cwd=ROOT, - text=True, - capture_output=True, - check=check, - ) - - -def git_stdout(args: list[str]) -> str: - return git(args).stdout - - -def ref_exists(ref: str) -> bool: - return git(["rev-parse", "--verify", "--quiet", f"{ref}^{{commit}}"], check=False).returncode == 0 - - -def base_ref() -> str | None: - base_branch = os.environ.get("GITHUB_BASE_REF") - candidates: list[str] = [] - if base_branch: - candidates.extend([f"origin/{base_branch}", base_branch]) - candidates.extend(["origin/main", "main"]) - for candidate in candidates: - if ref_exists(candidate): - return candidate - return None - - -def manifest_at(ref: str) -> dict[str, str]: - if git(["cat-file", "-e", f"{ref}:{MANIFEST}"], check=False).returncode != 0: - return {} - try: - raw = git_stdout(["show", f"{ref}:{MANIFEST}"]) - except subprocess.CalledProcessError as error: - fail(f"failed to read {MANIFEST} at {ref}: {error.stderr.strip()}") - value = json.loads(raw) - if not isinstance(value, dict) or not all( - isinstance(key, str) and isinstance(item, str) for key, item in value.items() - ): - fail(f"{MANIFEST} at {ref} must be a JSON string object") - return value - - -def current_manifest() -> dict[str, str]: - value = json.loads((ROOT / MANIFEST).read_text(encoding="utf-8")) - if not isinstance(value, dict) or not all( - isinstance(key, str) and isinstance(item, str) for key, item in value.items() - ): - fail(f"{MANIFEST} must be a JSON string object") - return value - - -def changed_files(ref: str) -> list[str]: - return release_plan.normalize_files( - release_plan.changed_files_from_refs(ref, "HEAD") - ) - - -def main() -> int: - ref = base_ref() - if ref is None: - fail("could not resolve base ref for release PR coverage check") - files = changed_files(ref) - if MANIFEST not in files: - print("release PR coverage check skipped; release-please manifest is unchanged") - return 0 - - before_manifest = manifest_at(ref) - after_manifest = current_manifest() - graph = release_plan.load_graph() - products = graph["products"] - - versioned_products = { - product - for product in product_metadata.product_ids(graph) - if before_manifest.get(product_metadata.package_path(product)) != after_manifest.get( - product_metadata.package_path(product) - ) - } - plan = release_plan.build_plan(graph, files) - selected_products = set(plan.get("releaseProducts", [])) - missing = sorted(selected_products - versioned_products) - if missing: - fail( - "release-please did not version every Moon-selected release product. " - "Moon remains the dependency authority, but release-please must own " - "the corresponding versions/tags. Missing product version bumps: " - + ", ".join(missing) - ) - unknown_versioned = sorted(versioned_products - set(products)) - if unknown_versioned: - fail(f"{MANIFEST} changed unknown products: {', '.join(unknown_versioned)}") - print("release PR product coverage checks passed") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/release/release.py b/tools/release/release.py index 47e8268c..50dfdb56 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -1675,7 +1675,7 @@ def command_check(args: list[str]) -> None: run(["tools/release/check_release_please_config.mjs"]) run(["python3", "tools/release/check_artifact_targets.py"]) run(["tools/release/sync_release_pr.py", "--check"]) - run(["python3", "tools/release/check_release_pr_coverage.py"]) + run(["bun", "tools/release/check_release_pr_coverage.mjs"]) run(["python3", "tools/release/check_release_metadata.py"]) run(["tools/release/release.py", "consumer-shape", "--format", "json", "--require-ready"]) run( From 83d190f9cbc2e1bfe0368074818b2fc2206c2fc9 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 13:37:14 +0000 Subject: [PATCH 088/137] chore: port native boundary policy to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 4 + tools/policy/check-native-boundaries.mjs | 355 ++++++++++++++++++ tools/policy/check-native-boundaries.sh | 329 +--------------- tools/policy/check-tooling-stack.sh | 4 + 4 files changed, 364 insertions(+), 328 deletions(-) create mode 100644 tools/policy/check-native-boundaries.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index afa7dc53..3aaf0bf1 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -283,6 +283,10 @@ review production pipelines, then normalize implementation details. `tools/release/check_release_pr_coverage.mjs` keeps release-please manifest diffs tied to `tools/release/release.py plan --format json`, and the release check command invokes the Bun checker directly. +- Native-boundary policy now uses Bun instead of inline Python. The stable + `tools/policy/check-native-boundaries.sh` entrypoint delegates to + `tools/policy/check-native-boundaries.mjs`, and `check-tooling-stack.sh` + rejects reintroducing the inline Python block. - Rust helper inventory is currently limited to `tools/xtask` and `tools/perf/runner`. Both remain Rust-owned for now: `xtask` owns WASIX asset parsing, archive/hash work, AOT/template feature-gated paths, and release diff --git a/tools/policy/check-native-boundaries.mjs b/tools/policy/check-native-boundaries.mjs new file mode 100644 index 00000000..527b4ee9 --- /dev/null +++ b/tools/policy/check-native-boundaries.mjs @@ -0,0 +1,355 @@ +#!/usr/bin/env bun +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); +const errors = []; + +const legacyPackageNames = new Set([ + 'oliphaunt-wasix', + 'liboliphaunt-wasix-portable', + 'oliphaunt-wasix-tools', +]); +const legacyNamePrefixes = [ + 'liboliphaunt-wasix-aot-', + 'oliphaunt-wasix-tools-aot-', +]; +const legacyRuntimeNames = new Set([ + 'wasmer', + 'wasmer-wasix', + 'wasmer-vfs', + 'wasmer-types', + 'wasmer-headless', +]); +const legacyPathFragments = [ + 'src/bindings/wasix-rust/crates/oliphaunt-wasix', + 'src/runtimes/liboliphaunt/wasix/crates/assets', + 'src/runtimes/liboliphaunt/wasix/crates/aot', + 'src/runtimes/liboliphaunt/wasix/crates/tools', + 'src/runtimes/liboliphaunt/wasix/crates/tools-aot', +]; + +function rel(file) { + return path.relative(root, file).split(path.sep).join('/'); +} + +function readText(relativePath) { + return fs.readFileSync(path.join(root, relativePath), 'utf8'); +} + +function readToml(relativePath) { + return Bun.TOML.parse(readText(relativePath)); +} + +function readJson(relativePath) { + return JSON.parse(readText(relativePath)); +} + +function isPlainObject(value) { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function* dependencyTables(manifest) { + for (const tableName of ['dependencies', 'dev-dependencies', 'build-dependencies']) { + yield [tableName, isPlainObject(manifest[tableName]) ? manifest[tableName] : {}]; + } + const targetTables = isPlainObject(manifest.target) ? manifest.target : {}; + for (const [cfg, table] of Object.entries(targetTables)) { + if (!isPlainObject(table)) { + continue; + } + for (const tableName of ['dependencies', 'dev-dependencies', 'build-dependencies']) { + yield [`target.${cfg}.${tableName}`, isPlainObject(table[tableName]) ? table[tableName] : {}]; + } + } +} + +function dependencyName(depKey, spec) { + return isPlainObject(spec) && typeof spec.package === 'string' ? spec.package : depKey; +} + +function dependencyPath(spec) { + return isPlainObject(spec) && typeof spec.path === 'string' ? spec.path : null; +} + +function isBlockedRustDependency(name) { + return ( + legacyPackageNames.has(name) || + legacyRuntimeNames.has(name) || + legacyNamePrefixes.some(prefix => name.startsWith(prefix)) + ); +} + +function pathInsideFragment(relativePath, fragment) { + return relativePath === fragment || relativePath.startsWith(`${fragment}/`); +} + +function checkNativeRustManifest(relativePath) { + const manifestPath = path.join(root, relativePath); + const manifest = readToml(relativePath); + for (const [tableName, deps] of dependencyTables(manifest)) { + for (const [depKey, spec] of Object.entries(deps)) { + const name = dependencyName(depKey, spec); + if (isBlockedRustDependency(name)) { + errors.push(`${relativePath} ${tableName}.${depKey} depends on legacy runtime resources ${JSON.stringify(name)}`); + } + const pathValue = dependencyPath(spec); + if (pathValue === null) { + continue; + } + const dependencyTarget = path.resolve(path.dirname(manifestPath), pathValue); + const dependencyTargetRel = rel(dependencyTarget); + if (legacyPathFragments.some(fragment => pathInsideFragment(dependencyTargetRel, fragment))) { + errors.push(`${relativePath} ${tableName}.${depKey} points at legacy path ${dependencyTargetRel}`); + } + } + } +} + +function checkJsonManifest(relativePath) { + const manifest = readJson(relativePath); + for (const tableName of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) { + const deps = isPlainObject(manifest[tableName]) ? manifest[tableName] : {}; + for (const name of Object.keys(deps)) { + if (legacyPackageNames.has(name) || legacyNamePrefixes.some(prefix => name.startsWith(prefix))) { + errors.push(`${relativePath} ${tableName}.${name} depends on legacy WASIX package`); + } + } + } +} + +function requireText(relativePath, text, message) { + if (!readText(relativePath).includes(text)) { + errors.push(`${relativePath}: ${message}; expected ${JSON.stringify(text)}`); + } +} + +function rejectManifestText(relativePath, patterns) { + const text = readText(relativePath); + for (const [label, pattern] of patterns) { + if (new RegExp(pattern, 'i').test(text)) { + errors.push(`${relativePath} contains blocked native-boundary reference: ${label}`); + } + } +} + +function checkToolCrateBoundaries() { + const manifest = readToml('tools/xtask/Cargo.toml'); + const features = isPlainObject(manifest.features) ? manifest.features : {}; + const dependencies = isPlainObject(manifest.dependencies) ? manifest.dependencies : {}; + + if (JSON.stringify(features.default ?? null) !== '[]') { + errors.push('tools/xtask/Cargo.toml must keep the default feature set empty'); + } + for (const removedFeature of ['perf', 'legacy-oliphaunt']) { + if (removedFeature in features) { + errors.push(`tools/xtask/Cargo.toml must not define product-aware feature ${JSON.stringify(removedFeature)}; use tools/perf/runner`); + } + } + + const forbiddenXtaskDependencies = [ + 'directories', + 'futures-util', + 'oliphaunt', + 'oliphaunt-wasix', + 'rusqlite', + 'sqlx', + 'tokio-postgres', + ]; + for (const depName of forbiddenXtaskDependencies) { + if (depName in dependencies) { + errors.push(`tools/xtask/Cargo.toml must not depend on product/perf crate ${JSON.stringify(depName)}; use tools/perf/runner`); + } + } + + for (const depName of ['wasmer', 'wasmer-types', 'wasmer-wasix', 'webc', 'tokio']) { + const spec = dependencies[depName]; + if (!isPlainObject(spec) || spec.optional !== true) { + errors.push(`tools/xtask/Cargo.toml dependency ${JSON.stringify(depName)} must stay optional so default xtask builds do not compile template/AOT runtime support`); + } + } + + const perfManifest = readToml('tools/perf/runner/Cargo.toml'); + const perfFeatures = isPlainObject(perfManifest.features) ? perfManifest.features : {}; + const perfDependencies = isPlainObject(perfManifest.dependencies) ? perfManifest.dependencies : {}; + if (JSON.stringify(perfFeatures.default ?? null) !== '[]') { + errors.push('tools/perf/runner/Cargo.toml must keep the default feature set empty'); + } + const legacyFeature = new Set(Array.isArray(perfFeatures['legacy-oliphaunt']) ? perfFeatures['legacy-oliphaunt'] : []); + for (const depName of ['dep:directories', 'dep:oliphaunt-wasix']) { + if (!legacyFeature.has(depName)) { + errors.push(`tools/perf/runner/Cargo.toml legacy-oliphaunt feature must gate ${depName}`); + } + } + for (const depName of ['oliphaunt', 'rusqlite', 'sqlx', 'tokio-postgres']) { + if (!(depName in perfDependencies)) { + errors.push(`tools/perf/runner/Cargo.toml must own benchmark dependency ${JSON.stringify(depName)}`); + } + } + + const wasixRunner = new Set(Array.isArray(features['wasix-runner']) ? features['wasix-runner'] : []); + for (const depName of ['dep:wasmer', 'dep:wasmer-wasix', 'dep:webc']) { + if (!wasixRunner.has(depName)) { + errors.push(`tools/xtask/Cargo.toml wasix-runner feature must explicitly gate ${depName}`); + } + } + + const aotSerializer = new Set(Array.isArray(features['aot-serializer']) ? features['aot-serializer'] : []); + if (!aotSerializer.has('dep:wasmer-types')) { + errors.push('tools/xtask/Cargo.toml aot-serializer feature must explicitly gate dep:wasmer-types'); + } +} + +function checkNativeScriptBoundary() { + requireText( + 'tools/perf/matrix/run_native_oliphaunt_matrix.sh', + 'cargo build --release -p oliphaunt-perf -p oliphaunt --bins', + 'native perf matrix must build the dedicated perf runner and native broker helper', + ); + requireText( + 'tools/perf/matrix/run_native_oliphaunt_matrix.sh', + 'legacyWasixControls=false', + 'native perf matrix plan must classify itself as native-only', + ); + requireText( + 'src/runtimes/liboliphaunt/native/tools/check-track.sh', + 'run src/runtimes/liboliphaunt/native/tools/check-patch-stack.mjs --check', + 'native track validation must keep the PostgreSQL patch-stack audit in the native lane', + ); + requireText( + 'src/runtimes/liboliphaunt/native/moon.yml', + 'command: "bash src/runtimes/liboliphaunt/native/tools/check-track.sh host-smoke"', + 'liboliphaunt host-smoke validation must run the host C ABI smoke rather than workspace legacy validation', + ); + rejectManifestText( + 'tools/policy/check-policy-tools.sh', + [ + [ + 'tools/policy/check-sdk-parity.sh', + 'policy-tools must stay a thin repository-policy aggregator; SDK parity evidence belongs to dedicated SDK/contract tasks', + ], + ], + ); +} + +function* walkFiles(relativeRoots, suffixes) { + const suffixSet = new Set(suffixes); + for (const relativeRoot of relativeRoots) { + const start = path.join(root, relativeRoot); + if (!fs.existsSync(start)) { + errors.push(`missing expected native boundary path: ${relativeRoot}`); + continue; + } + const stack = [start]; + while (stack.length > 0) { + const current = stack.pop(); + const entries = fs.readdirSync(current, { withFileTypes: true }).sort((left, right) => right.name.localeCompare(left.name)); + for (const entry of entries) { + const file = path.join(current, entry.name); + if (entry.isDirectory()) { + stack.push(file); + } else if (entry.isFile() && suffixSet.has(path.extname(file))) { + yield file; + } + } + } + } +} + +checkNativeRustManifest('src/sdks/rust/Cargo.toml'); +checkJsonManifest('src/sdks/react-native/package.json'); +checkJsonManifest('src/sdks/react-native/examples/expo/package.json'); +checkToolCrateBoundaries(); +checkNativeScriptBoundary(); + +const manifestTextPatterns = [ + ['oliphaunt-wasix package', String.raw`\boliphaunt-wasix\b`], + ['WASIX runtime', String.raw`\bwasix\b`], + ['Wasmer runtime', String.raw`\bwasmer\b`], +]; +for (const manifestPath of [ + 'src/sdks/swift/Package.swift', + 'src/sdks/react-native/OliphauntReactNative.podspec', + 'src/sdks/kotlin/build.gradle.kts', + 'src/sdks/kotlin/oliphaunt/build.gradle.kts', + 'src/sdks/react-native/android/build.gradle', + 'src/sdks/react-native/android/settings.gradle', +]) { + rejectManifestText(manifestPath, manifestTextPatterns); +} + +const sourcePatterns = [ + ['Rust import of legacy crate', String.raw`\b(use|extern\s+crate)\s+oliphaunt_wasix\b`], + ['Rust path to legacy crate', String.raw`\boliphaunt_wasix::`], + ['JavaScript import of legacy package', String.raw`\b(import|require)\s*(?:.+?\s+from\s*)?['"]oliphaunt-wasix['"]`], + ['Swift/Kotlin legacy module import', String.raw`\bimport\s+OliphauntWasm\b`], +]; +for (const filePath of walkFiles( + [ + 'src/sdks/rust/src', + 'src/sdks/rust/tests', + 'src/runtimes/liboliphaunt/native/include', + 'src/runtimes/liboliphaunt/native/src', + 'src/sdks/swift/Sources', + 'src/sdks/swift/Tests', + 'src/sdks/kotlin/oliphaunt/src', + 'src/sdks/react-native/src', + 'src/sdks/react-native/ios', + 'src/sdks/react-native/android/src', + ], + ['.rs', '.c', '.h', '.swift', '.kt', '.java', '.ts', '.tsx', '.m', '.mm', '.cpp'], +)) { + const text = fs.readFileSync(filePath, 'utf8'); + for (const [label, pattern] of sourcePatterns) { + if (new RegExp(pattern).test(text)) { + errors.push(`${rel(filePath)} contains blocked native-boundary code reference: ${label}`); + } + } +} + +const sdkManifest = readToml('tools/policy/sdk-manifest.toml'); +const expectedPaths = { + rust: 'src/sdks/rust', + swift: 'src/sdks/swift', + kotlin: 'src/sdks/kotlin', + 'react-native': 'src/sdks/react-native', +}; +const seenPaths = new Map(); +const sdkSections = isPlainObject(sdkManifest.sdks) ? sdkManifest.sdks : {}; +for (const [sdk, expectedPath] of Object.entries(expectedPaths)) { + const section = sdkSections[sdk]; + if (!isPlainObject(section)) { + errors.push(`tools/policy/sdk-manifest.toml is missing [sdks.${sdk}]`); + continue; + } + const actualPath = section.implementation_path; + if (actualPath !== expectedPath) { + errors.push(`tools/policy/sdk-manifest.toml [sdks.${sdk}].implementation_path is ${JSON.stringify(actualPath)}; expected ${JSON.stringify(expectedPath)}`); + } + if (seenPaths.has(actualPath)) { + errors.push(`tools/policy/sdk-manifest.toml shares implementation_path ${JSON.stringify(actualPath)} between ${seenPaths.get(actualPath)} and ${sdk}`); + } + seenPaths.set(actualPath, sdk); +} + +const reactNative = isPlainObject(sdkSections['react-native']) ? sdkSections['react-native'] : {}; +if (reactNative.runtime_owner !== false) { + errors.push('React Native SDK must stay a delegating adapter with runtime_owner = false'); +} +if (reactNative.delegates_apple_to !== 'swift') { + errors.push('React Native Apple runtime delegation must point at the Swift SDK'); +} +if (reactNative.delegates_android_to !== 'kotlin') { + errors.push('React Native Android runtime delegation must point at the Kotlin SDK'); +} + +if (errors.length > 0) { + console.error('native product boundary violations:'); + for (const error of errors) { + console.error(` - ${error}`); + } + process.exit(1); +} + +console.log('native product boundaries ok'); diff --git a/tools/policy/check-native-boundaries.sh b/tools/policy/check-native-boundaries.sh index e2d8ad82..f546f9cd 100755 --- a/tools/policy/check-native-boundaries.sh +++ b/tools/policy/check-native-boundaries.sh @@ -7,331 +7,4 @@ root="$(git rev-parse --show-toplevel 2>/dev/null)" || { } cd "$root" -python3 <<'PY' -import json -import pathlib -import re -import sys -import tomllib - -root = pathlib.Path.cwd() -errors: list[str] = [] - -legacy_package_names = { - "oliphaunt-wasix", - "liboliphaunt-wasix-portable", - "oliphaunt-wasix-tools", -} -legacy_name_prefixes = ( - "liboliphaunt-wasix-aot-", - "oliphaunt-wasix-tools-aot-", -) -legacy_runtime_names = { - "wasmer", - "wasmer-wasix", - "wasmer-vfs", - "wasmer-types", - "wasmer-headless", -} -legacy_path_fragments = ( - "src/bindings/wasix-rust/crates/oliphaunt-wasix", - "src/runtimes/liboliphaunt/wasix/crates/assets", - "src/runtimes/liboliphaunt/wasix/crates/aot", - "src/runtimes/liboliphaunt/wasix/crates/tools", - "src/runtimes/liboliphaunt/wasix/crates/tools-aot", -) - - -def rel(path: pathlib.Path) -> str: - return path.relative_to(root).as_posix() - - -def read_toml(relative_path: str) -> dict: - path = root / relative_path - return tomllib.loads(path.read_text(encoding="utf-8")) - - -def dependency_tables(manifest: dict): - for table_name in ("dependencies", "dev-dependencies", "build-dependencies"): - yield table_name, manifest.get(table_name, {}) - for cfg, table in manifest.get("target", {}).items(): - for table_name in ("dependencies", "dev-dependencies", "build-dependencies"): - yield f"target.{cfg}.{table_name}", table.get(table_name, {}) - - -def dependency_name(dep_key: str, spec) -> str: - if isinstance(spec, dict): - return spec.get("package", dep_key) - return dep_key - - -def dependency_path(spec): - if isinstance(spec, dict): - return spec.get("path") - return None - - -def is_blocked_rust_dependency(name: str) -> bool: - return ( - name in legacy_package_names - or name in legacy_runtime_names - or any(name.startswith(prefix) for prefix in legacy_name_prefixes) - ) - - -def check_native_rust_manifest(relative_path: str) -> None: - manifest_path = root / relative_path - manifest = read_toml(relative_path) - for table_name, deps in dependency_tables(manifest): - for dep_key, spec in deps.items(): - name = dependency_name(dep_key, spec) - if is_blocked_rust_dependency(name): - errors.append( - f"{relative_path} {table_name}.{dep_key} depends on legacy runtime resources {name!r}" - ) - path_value = dependency_path(spec) - if path_value is None: - continue - dependency_target = (manifest_path.parent / path_value).resolve() - dependency_target_rel = dependency_target.relative_to(root).as_posix() - if any( - dependency_target_rel == fragment - or dependency_target_rel.startswith(f"{fragment}/") - for fragment in legacy_path_fragments - ): - errors.append( - f"{relative_path} {table_name}.{dep_key} points at legacy path {dependency_target_rel}" - ) - - -def check_json_manifest(relative_path: str) -> None: - manifest = json.loads((root / relative_path).read_text(encoding="utf-8")) - for table_name in ( - "dependencies", - "devDependencies", - "peerDependencies", - "optionalDependencies", - ): - deps = manifest.get(table_name, {}) - for name in deps: - if name in legacy_package_names or any( - name.startswith(prefix) for prefix in legacy_name_prefixes - ): - errors.append( - f"{relative_path} {table_name}.{name} depends on legacy WASIX package" - ) - - -def require_text(relative_path: str, text: str, message: str) -> None: - if text not in (root / relative_path).read_text(encoding="utf-8"): - errors.append(f"{relative_path}: {message}; expected {text!r}") - - -def check_tool_crate_boundaries() -> None: - manifest = read_toml("tools/xtask/Cargo.toml") - features = manifest.get("features", {}) - dependencies = manifest.get("dependencies", {}) - - if features.get("default") != []: - errors.append( - "tools/xtask/Cargo.toml must keep the default feature set empty" - ) - for removed_feature in ("perf", "legacy-oliphaunt"): - if removed_feature in features: - errors.append( - f"tools/xtask/Cargo.toml must not define product-aware feature {removed_feature!r}; use tools/perf/runner" - ) - - forbidden_xtask_dependencies = ( - "directories", - "futures-util", - "oliphaunt", - "oliphaunt-wasix", - "rusqlite", - "sqlx", - "tokio-postgres", - ) - for dep_name in forbidden_xtask_dependencies: - if dep_name in dependencies: - errors.append( - f"tools/xtask/Cargo.toml must not depend on product/perf crate {dep_name!r}; use tools/perf/runner" - ) - - for dep_name in ("wasmer", "wasmer-types", "wasmer-wasix", "webc", "tokio"): - spec = dependencies.get(dep_name) - if not isinstance(spec, dict) or spec.get("optional") is not True: - errors.append( - f"tools/xtask/Cargo.toml dependency {dep_name!r} must stay optional so default xtask builds do not compile template/AOT runtime support" - ) - - perf_manifest = read_toml("tools/perf/runner/Cargo.toml") - perf_features = perf_manifest.get("features", {}) - perf_dependencies = perf_manifest.get("dependencies", {}) - if perf_features.get("default") != []: - errors.append( - "tools/perf/runner/Cargo.toml must keep the default feature set empty" - ) - legacy_feature = set(perf_features.get("legacy-oliphaunt", [])) - for dep_name in ("dep:directories", "dep:oliphaunt-wasix"): - if dep_name not in legacy_feature: - errors.append( - f"tools/perf/runner/Cargo.toml legacy-oliphaunt feature must gate {dep_name}" - ) - for dep_name in ("oliphaunt", "rusqlite", "sqlx", "tokio-postgres"): - if dep_name not in perf_dependencies: - errors.append( - f"tools/perf/runner/Cargo.toml must own benchmark dependency {dep_name!r}" - ) - - wasix_runner = set(features.get("wasix-runner", [])) - for dep_name in ("dep:wasmer", "dep:wasmer-wasix", "dep:webc"): - if dep_name not in wasix_runner: - errors.append( - f"tools/xtask/Cargo.toml wasix-runner feature must explicitly gate {dep_name}" - ) - - aot_serializer = set(features.get("aot-serializer", [])) - if "dep:wasmer-types" not in aot_serializer: - errors.append( - "tools/xtask/Cargo.toml aot-serializer feature must explicitly gate dep:wasmer-types" - ) - - -def check_native_script_boundary() -> None: - require_text( - "tools/perf/matrix/run_native_oliphaunt_matrix.sh", - "cargo build --release -p oliphaunt-perf -p oliphaunt --bins", - "native perf matrix must build the dedicated perf runner and native broker helper", - ) - require_text( - "tools/perf/matrix/run_native_oliphaunt_matrix.sh", - "legacyWasixControls=false", - "native perf matrix plan must classify itself as native-only", - ) - require_text( - "src/runtimes/liboliphaunt/native/tools/check-track.sh", - "run src/runtimes/liboliphaunt/native/tools/check-patch-stack.mjs --check", - "native track validation must keep the PostgreSQL patch-stack audit in the native lane", - ) - require_text( - "src/runtimes/liboliphaunt/native/moon.yml", - 'command: "bash src/runtimes/liboliphaunt/native/tools/check-track.sh host-smoke"', - "liboliphaunt host-smoke validation must run the host C ABI smoke rather than workspace legacy validation", - ) - reject_manifest_text( - "tools/policy/check-policy-tools.sh", - [ - ( - "tools/policy/check-sdk-parity.sh", - "policy-tools must stay a thin repository-policy aggregator; SDK parity evidence belongs to dedicated SDK/contract tasks", - ), - ], - ) - - -def reject_manifest_text(relative_path: str, patterns: list[tuple[str, str]]) -> None: - path = root / relative_path - text = path.read_text(encoding="utf-8") - for label, pattern in patterns: - if re.search(pattern, text, flags=re.IGNORECASE): - errors.append(f"{relative_path} contains blocked native-boundary reference: {label}") - - -def walk_files(relative_roots: list[str], suffixes: tuple[str, ...]): - for relative_root in relative_roots: - path = root / relative_root - if not path.exists(): - errors.append(f"missing expected native boundary path: {relative_root}") - continue - for file_path in path.rglob("*"): - if file_path.is_file() and file_path.suffix in suffixes: - yield file_path - - -check_native_rust_manifest("src/sdks/rust/Cargo.toml") -check_json_manifest("src/sdks/react-native/package.json") -check_json_manifest("src/sdks/react-native/examples/expo/package.json") -check_tool_crate_boundaries() -check_native_script_boundary() - -manifest_text_patterns = [ - ("oliphaunt-wasix package", r"\boliphaunt-wasix\b"), - ("WASIX runtime", r"\bwasix\b"), - ("Wasmer runtime", r"\bwasmer\b"), -] -for manifest_path in ( - "src/sdks/swift/Package.swift", - "src/sdks/react-native/OliphauntReactNative.podspec", - "src/sdks/kotlin/build.gradle.kts", - "src/sdks/kotlin/oliphaunt/build.gradle.kts", - "src/sdks/react-native/android/build.gradle", - "src/sdks/react-native/android/settings.gradle", -): - reject_manifest_text(manifest_path, manifest_text_patterns) - -source_patterns = [ - ("Rust import of legacy crate", r"\b(use|extern\s+crate)\s+oliphaunt_wasix\b"), - ("Rust path to legacy crate", r"\boliphaunt_wasix::"), - ("JavaScript import of legacy package", r"\b(import|require)\s*(?:.+?\s+from\s*)?['\"]oliphaunt-wasix['\"]"), - ("Swift/Kotlin legacy module import", r"\bimport\s+OliphauntWasm\b"), -] -for file_path in walk_files( - [ - "src/sdks/rust/src", - "src/sdks/rust/tests", - "src/runtimes/liboliphaunt/native/include", - "src/runtimes/liboliphaunt/native/src", - "src/sdks/swift/Sources", - "src/sdks/swift/Tests", - "src/sdks/kotlin/oliphaunt/src", - "src/sdks/react-native/src", - "src/sdks/react-native/ios", - "src/sdks/react-native/android/src", - ], - (".rs", ".c", ".h", ".swift", ".kt", ".java", ".ts", ".tsx", ".m", ".mm", ".cpp"), -): - text = file_path.read_text(encoding="utf-8", errors="ignore") - for label, pattern in source_patterns: - if re.search(pattern, text): - errors.append(f"{rel(file_path)} contains blocked native-boundary code reference: {label}") - -sdk_manifest = read_toml("tools/policy/sdk-manifest.toml") -expected_paths = { - "rust": "src/sdks/rust", - "swift": "src/sdks/swift", - "kotlin": "src/sdks/kotlin", - "react-native": "src/sdks/react-native", -} -seen_paths: dict[str, str] = {} -for sdk, expected_path in expected_paths.items(): - section = sdk_manifest.get("sdks", {}).get(sdk) - if section is None: - errors.append(f"tools/policy/sdk-manifest.toml is missing [sdks.{sdk}]") - continue - actual_path = section.get("implementation_path") - if actual_path != expected_path: - errors.append( - f"tools/policy/sdk-manifest.toml [sdks.{sdk}].implementation_path is {actual_path!r}; expected {expected_path!r}" - ) - if actual_path in seen_paths: - errors.append( - f"tools/policy/sdk-manifest.toml shares implementation_path {actual_path!r} between {seen_paths[actual_path]} and {sdk}" - ) - seen_paths[actual_path] = sdk - -react_native = sdk_manifest.get("sdks", {}).get("react-native", {}) -if react_native.get("runtime_owner") is not False: - errors.append("React Native SDK must stay a delegating adapter with runtime_owner = false") -if react_native.get("delegates_apple_to") != "swift": - errors.append("React Native Apple runtime delegation must point at the Swift SDK") -if react_native.get("delegates_android_to") != "kotlin": - errors.append("React Native Android runtime delegation must point at the Kotlin SDK") - -if errors: - print("native product boundary violations:", file=sys.stderr) - for error in errors: - print(f" - {error}", file=sys.stderr) - sys.exit(1) - -print("native product boundaries ok") -PY +bun tools/policy/check-native-boundaries.mjs diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index ac19b0d5..2471444f 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -39,6 +39,7 @@ require_file tools/test/moon.yml require_file tools/test/run-js-tests.mjs require_file tools/graph/cache-witness.mjs require_file tools/policy/check-python-entrypoints.mjs +require_file tools/policy/check-native-boundaries.mjs require_file tools/policy/python-entrypoints.allowlist require_file tools/runtime/preflight.sh require_file tools/dev/bun.sh @@ -242,6 +243,9 @@ grep -Fq 'install_cargo_tool ripgrep rg "$RIPGREP_VERSION"' tools/dev/bootstrap- fail "local tool bootstrap must install the pinned ripgrep binary" bun tools/policy/check-python-entrypoints.mjs +if grep -Fq "python3 <<'PY'" tools/policy/check-native-boundaries.sh; then + fail "native boundary policy must use the Bun checker instead of inline Python" +fi if grep -Fq 'python3' tools/dev/bootstrap-tools.sh; then fail "local tool bootstrap must not use Python for archive extraction" fi From 3cd9156f3d7185fc5e246b36d80b1c94a8991aaf Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 13:43:20 +0000 Subject: [PATCH 089/137] chore: port wasm preflight manifest check to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 4 +++ tools/policy/check-tooling-stack.sh | 5 +++- tools/runtime/preflight.sh | 27 ++++++++++++------- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 3aaf0bf1..9b09e97d 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -287,6 +287,10 @@ review production pipelines, then normalize implementation details. `tools/policy/check-native-boundaries.sh` entrypoint delegates to `tools/policy/check-native-boundaries.mjs`, and `check-tooling-stack.sh` rejects reintroducing the inline Python block. +- Runtime WASIX asset-mode preflight now uses Bun instead of inline Python while + keeping the shared `tools/runtime/preflight.sh` shell entrypoint POSIX-sh + source-compatible for SDK checks. `check-tooling-stack.sh` rejects + reintroducing the inline Python manifest parser there. - Rust helper inventory is currently limited to `tools/xtask` and `tools/perf/runner`. Both remain Rust-owned for now: `xtask` owns WASIX asset parsing, archive/hash work, AOT/template feature-gated paths, and release diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 2471444f..c2b93aa8 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -243,9 +243,12 @@ grep -Fq 'install_cargo_tool ripgrep rg "$RIPGREP_VERSION"' tools/dev/bootstrap- fail "local tool bootstrap must install the pinned ripgrep binary" bun tools/policy/check-python-entrypoints.mjs -if grep -Fq "python3 <<'PY'" tools/policy/check-native-boundaries.sh; then +if grep -Eq "python3[[:space:]]+(-[[:space:]]+)?<<'PY'" tools/policy/check-native-boundaries.sh; then fail "native boundary policy must use the Bun checker instead of inline Python" fi +if grep -Eq "python3[[:space:]]+(-[[:space:]]+)?<<'PY'" tools/runtime/preflight.sh; then + fail "runtime preflight must use Bun instead of inline Python" +fi if grep -Fq 'python3' tools/dev/bootstrap-tools.sh; then fail "local tool bootstrap must not use Python for archive extraction" fi diff --git a/tools/runtime/preflight.sh b/tools/runtime/preflight.sh index d5fdb544..be9967ca 100755 --- a/tools/runtime/preflight.sh +++ b/tools/runtime/preflight.sh @@ -432,15 +432,24 @@ oliphaunt_runtime_wasm_host_triple() { } oliphaunt_runtime_wasm_asset_mode() { - python3 - <<'PY' -import json -from pathlib import Path - -manifest = json.loads(Path("target/oliphaunt-wasix/assets/manifest.json").read_text()) -has_extensions = bool(manifest.get("extensions")) -has_pg_dump = bool(manifest.get("pg-dump")) -print("full" if has_extensions and has_pg_dump else "core") -PY + if ! command -v bun >/dev/null 2>&1; then + echo "Bun is required to inspect target/oliphaunt-wasix/assets/manifest.json" >&2 + return 1 + fi + bun --eval ' +function pyTruthy(value) { + if (value === null || value === undefined || value === false) return false; + if (Array.isArray(value) || typeof value === "string") return value.length > 0; + if (typeof value === "number") return value !== 0; + if (typeof value === "object") return Object.keys(value).length > 0; + return true; +} + +const manifest = await Bun.file("target/oliphaunt-wasix/assets/manifest.json").json(); +const hasExtensions = pyTruthy(manifest.extensions); +const hasPgDump = pyTruthy(manifest["pg-dump"]); +console.log(hasExtensions && hasPgDump ? "full" : "core"); +' } oliphaunt_runtime_wasm_require() { From 5c2ad65260ff4600d9b735d339847713f0584fe3 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 13:46:46 +0000 Subject: [PATCH 090/137] chore: port rust sdk artifact patch helper to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 5 ++ .../rust/tools/cargo-artifact-patches.mjs | 51 +++++++++++++++++++ src/sdks/rust/tools/check-sdk.sh | 15 ++---- tools/policy/check-tooling-stack.sh | 6 +++ 4 files changed, 65 insertions(+), 12 deletions(-) create mode 100644 src/sdks/rust/tools/cargo-artifact-patches.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 9b09e97d..ba5edbd7 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -291,6 +291,11 @@ review production pipelines, then normalize implementation details. keeping the shared `tools/runtime/preflight.sh` shell entrypoint POSIX-sh source-compatible for SDK checks. `check-tooling-stack.sh` rejects reintroducing the inline Python manifest parser there. +- Rust SDK Cargo artifact relay smoke setup now expands generated + `packages.json` metadata into `[patch.crates-io]` entries with + `src/sdks/rust/tools/cargo-artifact-patches.mjs` instead of an inline Python + JSON parser. The broader release-source staging call still goes through + `release.py` until that release graph is ported as a whole. - Rust helper inventory is currently limited to `tools/xtask` and `tools/perf/runner`. Both remain Rust-owned for now: `xtask` owns WASIX asset parsing, archive/hash work, AOT/template feature-gated paths, and release diff --git a/src/sdks/rust/tools/cargo-artifact-patches.mjs b/src/sdks/rust/tools/cargo-artifact-patches.mjs new file mode 100644 index 00000000..5cb8eef8 --- /dev/null +++ b/src/sdks/rust/tools/cargo-artifact-patches.mjs @@ -0,0 +1,51 @@ +#!/usr/bin/env bun +import fs from 'node:fs/promises'; +import path from 'node:path'; + +function fail(message) { + console.error(`cargo-artifact-patches.mjs: ${message}`); + process.exit(2); +} + +function parseArgs(argv) { + if (argv.length !== 2) { + fail('usage: src/sdks/rust/tools/cargo-artifact-patches.mjs '); + } + return { + root: path.resolve(argv[0]), + manifest: path.isAbsolute(argv[1]) ? argv[1] : path.resolve(argv[0], argv[1]), + }; +} + +function tomlString(value) { + return JSON.stringify(value); +} + +const { root, manifest } = parseArgs(Bun.argv.slice(2)); +let data; +try { + data = JSON.parse(await fs.readFile(manifest, 'utf8')); +} catch (error) { + fail(`could not read Cargo artifact package manifest ${manifest}: ${error.message}`); +} + +if (data === null || typeof data !== 'object' || !Array.isArray(data.packages)) { + fail(`${manifest} must contain a packages array`); +} + +for (const [index, artifact] of data.packages.entries()) { + if (artifact === null || typeof artifact !== 'object' || Array.isArray(artifact)) { + fail(`${manifest} package row ${index} must be an object`); + } + const { name, manifestPath } = artifact; + if (typeof name !== 'string' || name.length === 0) { + fail(`${manifest} package row ${index} must declare a non-empty name`); + } + if (typeof manifestPath !== 'string' || manifestPath.length === 0) { + fail(`${manifest} package row ${index} must declare a non-empty manifestPath`); + } + const artifactManifest = path.isAbsolute(manifestPath) + ? manifestPath + : path.join(root, manifestPath); + console.log(`${name} = { path = ${tomlString(path.dirname(artifactManifest))} }`); +} diff --git a/src/sdks/rust/tools/check-sdk.sh b/src/sdks/rust/tools/check-sdk.sh index f40ba72e..7af73f63 100755 --- a/src/sdks/rust/tools/check-sdk.sh +++ b/src/sdks/rust/tools/check-sdk.sh @@ -212,18 +212,9 @@ extensions = [] [patch.crates-io] EOF - python3 - "$root" "$liboliphaunt_cargo_artifacts/packages.json" >>"$smoke/Cargo.toml" <<'PY' -import json -import sys -from pathlib import Path - -root = Path(sys.argv[1]) -manifest = root / sys.argv[2] -data = json.loads(manifest.read_text(encoding="utf-8")) -for package in data["packages"]: - path = root / Path(package["manifestPath"]).parent - print(f'{package["name"]} = {{ path = "{path}" }}') -PY + bun src/sdks/rust/tools/cargo-artifact-patches.mjs \ + "$root" \ + "$liboliphaunt_cargo_artifacts/packages.json" >>"$smoke/Cargo.toml" cat >>"$smoke/Cargo.toml" < Date: Fri, 26 Jun 2026 13:56:13 +0000 Subject: [PATCH 091/137] chore: port sdk crate filename helper to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 4 +++ tools/policy/check-tooling-stack.sh | 9 +++++ tools/release/build-sdk-ci-artifacts.sh | 33 ++----------------- tools/release/cargo-crate-filename.mjs | 33 +++++++++++++++++++ 4 files changed, 48 insertions(+), 31 deletions(-) create mode 100644 tools/release/cargo-crate-filename.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index ba5edbd7..c2d37d7d 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -296,6 +296,10 @@ review production pipelines, then normalize implementation details. `src/sdks/rust/tools/cargo-artifact-patches.mjs` instead of an inline Python JSON parser. The broader release-source staging call still goes through `release.py` until that release graph is ported as a whole. +- SDK CI artifact staging now resolves Rust `.crate` filenames with + `tools/release/cargo-crate-filename.mjs` instead of an inline Python TOML + parser. The unused inline workspace-exclusion Python helper was removed, and + `check-tooling-stack.sh` rejects drift back to either path. - Rust helper inventory is currently limited to `tools/xtask` and `tools/perf/runner`. Both remain Rust-owned for now: `xtask` owns WASIX asset parsing, archive/hash work, AOT/template feature-gated paths, and release diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 16b2c8c8..eac31e80 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -43,6 +43,7 @@ require_file tools/policy/check-native-boundaries.mjs require_file tools/policy/python-entrypoints.allowlist require_file tools/runtime/preflight.sh require_file src/sdks/rust/tools/cargo-artifact-patches.mjs +require_file tools/release/cargo-crate-filename.mjs require_file tools/dev/bun.sh require_file tools/dev/deno.sh require_file tools/dev/install-actionlint.sh @@ -325,6 +326,14 @@ grep -Fq 'missing package-shape output' tools/release/build-sdk-ci-artifacts.sh if grep -Fq 'OLIPHAUNT_SDK_CHECK_SCRATCH="$work_root/check"' tools/release/build-sdk-ci-artifacts.sh; then fail "SDK artifact builder must not rerun package-shape inside the artifact staging script" fi +grep -Fq 'bun tools/release/cargo-crate-filename.mjs "$manifest"' tools/release/build-sdk-ci-artifacts.sh || + fail "SDK artifact builder must use the Bun helper for Cargo crate filenames" +if grep -Fq 'python3 - "$manifest"' tools/release/build-sdk-ci-artifacts.sh; then + fail "SDK artifact builder must not use inline Python for Cargo crate filenames" +fi +if grep -Fq 'cargo_workspace_excludes_except()' tools/release/build-sdk-ci-artifacts.sh; then + fail "SDK artifact builder must not carry unused inline Python workspace helpers" +fi grep -Fq 'tools/release/write_checksum_manifest.mjs \' tools/release/package-liboliphaunt-aggregate-assets.sh || fail "aggregate liboliphaunt asset packager must use the shared Bun checksum manifest writer" if grep -Fq 'python3 - "$asset_dir" "$checksum_file"' tools/release/package-liboliphaunt-aggregate-assets.sh; then diff --git a/tools/release/build-sdk-ci-artifacts.sh b/tools/release/build-sdk-ci-artifacts.sh index f25b84d4..990af12f 100755 --- a/tools/release/build-sdk-ci-artifacts.sh +++ b/tools/release/build-sdk-ci-artifacts.sh @@ -28,15 +28,7 @@ require_dir() { rust_crate_name() { local manifest="$1" - python3 - "$manifest" <<'PY' -from pathlib import Path -import sys -import tomllib - -data = tomllib.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) -package = data["package"] -print(f"{package['name']}-{package['version']}.crate") -PY + bun tools/release/cargo-crate-filename.mjs "$manifest" } cargo_package_dir() { @@ -47,26 +39,6 @@ cargo_package_dir() { printf '%s/package\n' "$target_dir" } -cargo_workspace_excludes_except() { - python3 - "$@" <<'PY' -import json -import subprocess -import sys - -wanted = set(sys.argv[1:]) -metadata = json.loads( - subprocess.check_output( - ["cargo", "metadata", "--no-deps", "--format-version", "1"], - text=True, - ) -) -for package in metadata["packages"]: - name = package["name"] - if name not in wanted: - print(name) -PY -} - package_npm_workspace() { local package_dir="$1" local destination="$2" @@ -123,7 +95,7 @@ mkdir -p "$artifact_root" "$work_root" case "$product" in oliphaunt-rust) require cargo - require python3 + require bun package_listing="$root/target/liboliphaunt-sdk-check/rust-cargo-package-list.txt" require_file "$package_listing" for package in oliphaunt oliphaunt-build; do @@ -205,7 +177,6 @@ case "$product" in oliphaunt-wasix-rust) require cargo require bun - require python3 package_listing="$root/target/oliphaunt-wasix-rust/package/oliphaunt-wasix.package-files.txt" require_file "$package_listing" bun tools/release/package_oliphaunt_wasix_sdk_crate.mjs --output-dir "$artifact_root" diff --git a/tools/release/cargo-crate-filename.mjs b/tools/release/cargo-crate-filename.mjs new file mode 100644 index 00000000..e5cd0b5e --- /dev/null +++ b/tools/release/cargo-crate-filename.mjs @@ -0,0 +1,33 @@ +#!/usr/bin/env bun + +function fail(message) { + console.error(`cargo-crate-filename.mjs: ${message}`); + process.exit(2); +} + +const manifest = Bun.argv[2]; +if (manifest === undefined || manifest.length === 0) { + fail('usage: tools/release/cargo-crate-filename.mjs '); +} + +let parsed; +try { + parsed = Bun.TOML.parse(await Bun.file(manifest).text()); +} catch (error) { + fail(`could not parse ${manifest}: ${error.message}`); +} + +const packageConfig = parsed.package; +if (packageConfig === null || typeof packageConfig !== 'object' || Array.isArray(packageConfig)) { + fail(`${manifest} must declare a [package] table`); +} + +const { name, version } = packageConfig; +if (typeof name !== 'string' || name.length === 0) { + fail(`${manifest} must declare package.name`); +} +if (typeof version !== 'string' || version.length === 0) { + fail(`${manifest} must declare package.version`); +} + +console.log(`${name}-${version}.crate`); From a20f25f55ecea85b673b8b7409a273af9b24ab3b Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 14:12:51 +0000 Subject: [PATCH 092/137] fix: split native client tools into release assets --- src/sdks/rust/src/bin/package_resources.rs | 12 ++++- src/sdks/rust/tools/check-sdk.sh | 2 +- tools/release/artifact_targets.py | 4 +- tools/release/check_artifact_targets.py | 4 ++ tools/release/check_consumer_shape.py | 7 +-- .../check_liboliphaunt_release_assets.py | 22 ++++++++- tools/release/check_release_metadata.py | 14 ++++-- .../optimize_native_runtime_payload.py | 14 ++++++ .../package-liboliphaunt-linux-assets.sh | 14 +++++- .../package-liboliphaunt-macos-assets.sh | 14 +++++- .../package-liboliphaunt-windows-assets.ps1 | 20 +++++++- .../package_liboliphaunt_cargo_artifacts.py | 49 +++++++++++-------- tools/release/release.py | 16 +++--- .../create-liboliphaunt-release-fixture.mjs | 44 +++++++++++++++-- 14 files changed, 188 insertions(+), 48 deletions(-) diff --git a/src/sdks/rust/src/bin/package_resources.rs b/src/sdks/rust/src/bin/package_resources.rs index ec5a9313..22a7408d 100644 --- a/src/sdks/rust/src/bin/package_resources.rs +++ b/src/sdks/rust/src/bin/package_resources.rs @@ -711,13 +711,21 @@ fn release_asset_names_for_target(version: &str, target: &str) -> oliphaunt::Res let mut assets = vec![format!("liboliphaunt-{version}-runtime-resources.tar.gz")]; match target { "runtime-resources" | "runtime-only" => {} - "macos-arm64" => assets.push(format!("liboliphaunt-{version}-macos-arm64.tar.gz")), - "linux-x64-gnu" => assets.push(format!("liboliphaunt-{version}-linux-x64-gnu.tar.gz")), + "macos-arm64" => { + assets.push(format!("liboliphaunt-{version}-macos-arm64.tar.gz")); + assets.push(format!("oliphaunt-tools-{version}-macos-arm64.tar.gz")); + } + "linux-x64-gnu" => { + assets.push(format!("liboliphaunt-{version}-linux-x64-gnu.tar.gz")); + assets.push(format!("oliphaunt-tools-{version}-linux-x64-gnu.tar.gz")); + } "linux-arm64-gnu" => { assets.push(format!("liboliphaunt-{version}-linux-arm64-gnu.tar.gz")); + assets.push(format!("oliphaunt-tools-{version}-linux-arm64-gnu.tar.gz")); } "windows-x64-msvc" => { assets.push(format!("liboliphaunt-{version}-windows-x64-msvc.zip")); + assets.push(format!("oliphaunt-tools-{version}-windows-x64-msvc.zip")); } "ios-xcframework" | "ios" => { assets.push(format!("liboliphaunt-{version}-ios-xcframework.tar.gz")); diff --git a/src/sdks/rust/tools/check-sdk.sh b/src/sdks/rust/tools/check-sdk.sh index 7af73f63..7ba882fe 100755 --- a/src/sdks/rust/tools/check-sdk.sh +++ b/src/sdks/rust/tools/check-sdk.sh @@ -99,7 +99,7 @@ check_release_asset_fixture() { --output "$fixture_output" \ --force >"$fixture_log" cat "$fixture_log" - if ! grep -Fq "liboliphauntReleaseAssets=liboliphaunt-$liboliphaunt_version-linux-x64-gnu.tar.gz,liboliphaunt-$liboliphaunt_version-runtime-resources.tar.gz" "$fixture_log"; then + if ! grep -Fq "liboliphauntReleaseAssets=liboliphaunt-$liboliphaunt_version-linux-x64-gnu.tar.gz,liboliphaunt-$liboliphaunt_version-runtime-resources.tar.gz,oliphaunt-tools-$liboliphaunt_version-linux-x64-gnu.tar.gz" "$fixture_log"; then echo "Rust SDK release asset resolver did not select the expected release-shaped liboliphaunt assets" >&2 exit 1 fi diff --git a/tools/release/artifact_targets.py b/tools/release/artifact_targets.py index 9c6cf8d1..d68f641b 100644 --- a/tools/release/artifact_targets.py +++ b/tools/release/artifact_targets.py @@ -372,12 +372,12 @@ def _liboliphaunt_native_target_tables() -> list[dict]: "target": target, "triple": platform["triple"], "runner": platform["runner"], - "asset": _archive_asset("liboliphaunt", target, platform.get("archive", "tar.gz")), + "asset": _archive_asset("oliphaunt-tools", target, platform.get("archive", "tar.gz")), "npm_package": platform.get("liboliphaunt_tools_npm_package"), "npm_os": platform.get("npm_os"), "npm_cpu": platform.get("npm_cpu"), "npm_libc": platform.get("npm_libc"), - "surfaces": ["typescript-native-direct"], + "surfaces": ["github-release", "rust-native-direct", "typescript-native-direct"], "published": True, "_source_file": "Moon release metadata", } diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py index 2ce6a526..ba8ebdf7 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -1329,9 +1329,13 @@ def validate_expected_product_assets() -> None: expected = { "liboliphaunt-native": { "liboliphaunt-{version}-macos-arm64.tar.gz", + "oliphaunt-tools-{version}-macos-arm64.tar.gz", "liboliphaunt-{version}-linux-x64-gnu.tar.gz", + "oliphaunt-tools-{version}-linux-x64-gnu.tar.gz", "liboliphaunt-{version}-linux-arm64-gnu.tar.gz", + "oliphaunt-tools-{version}-linux-arm64-gnu.tar.gz", "liboliphaunt-{version}-windows-x64-msvc.zip", + "oliphaunt-tools-{version}-windows-x64-msvc.zip", "liboliphaunt-{version}-ios-xcframework.tar.gz", "liboliphaunt-{version}-apple-spm-xcframework.zip", "liboliphaunt-{version}-android-arm64-v8a.tar.gz", diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 4f5cf463..4c1c76dc 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -419,8 +419,9 @@ def check_liboliphaunt(findings: list[Finding]) -> None: "liboliphaunt-native-tool-split", set(optimize_native_runtime_payload.NATIVE_RUNTIME_TOOL_STEMS) == {"initdb", "pg_ctl", "postgres"} and set(optimize_native_runtime_payload.NATIVE_TOOLS_TOOL_STEMS) == {"pg_dump", "psql"} - and "copy_tools_payload" in native_packager - and "required_tools_member_paths" in native_packager + and "missing oliphaunt-tools native release asset" in native_packager + and "extract_archive(tools_archive, tools_root)" in native_packager + and "validate_tools_target_pair" in native_packager and "package_base=TOOLS_PRODUCT" in native_packager and 'artifact_product=TOOLS_PRODUCT' in native_packager and 'tool_set="runtime"' in native_packager @@ -428,7 +429,7 @@ def check_liboliphaunt(findings: list[Finding]) -> None: and "required_runtime_member_paths" in release_cli and "required_tools_member_paths" in release_cli and "stage_liboliphaunt_tools_npm_payloads" in release_cli - and "remove_native_tools_from_runtime" in release_cli + and "ensure_native_tools_absent_from_runtime" in release_cli and "NATIVE_RUNTIME_TOOL_STEMS" in native_optimizer and "NATIVE_TOOLS_TOOL_STEMS" in native_optimizer, "Native root packages and crates must keep postgres/initdb/pg_ctl only, with pg_dump/psql published through oliphaunt-tools packages/crates.", diff --git a/tools/release/check_liboliphaunt_release_assets.py b/tools/release/check_liboliphaunt_release_assets.py index da8cae02..835db777 100755 --- a/tools/release/check_liboliphaunt_release_assets.py +++ b/tools/release/check_liboliphaunt_release_assets.py @@ -191,7 +191,13 @@ def extract_archive(path: Path, destination: Path) -> None: fail(f"{path} is not a readable tar archive: {error}") -def validate_native_target_artifact(path: Path, target: str, *, require_runtime: bool) -> None: +def validate_native_target_artifact( + path: Path, + target: str, + *, + require_runtime: bool, + tool_set: optimize_native_runtime_payload.NativeToolSet, +) -> None: with tempfile.TemporaryDirectory(prefix=f"oliphaunt-native-{target}-") as temp: extracted = Path(temp) / "payload" extract_archive(path, extracted) @@ -199,6 +205,7 @@ def validate_native_target_artifact(path: Path, target: str, *, require_runtime: extracted, target, require_runtime=require_runtime, + tool_set=tool_set, ) @@ -222,6 +229,19 @@ def validate_native_target_artifacts(asset_dir: Path, version: str) -> None: asset_dir / target.asset_name(version), target.target, require_runtime=target.target in runtime_targets, + tool_set="runtime", + ) + for target in artifact_targets.artifact_targets( + product="liboliphaunt-native", + kind="native-tools", + surface="github-release", + published_only=True, + ): + validate_native_target_artifact( + asset_dir / target.asset_name(version), + target.target, + require_runtime=True, + tool_set="tools", ) diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index a2d82a43..b8f17d15 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -432,9 +432,14 @@ def validate_rust() -> None: ) require_text( "src/sdks/rust/src/bin/package_resources.rs", - '"linux-x64-gnu" => assets.push(format!("liboliphaunt-{version}-linux-x64-gnu.tar.gz"))', + 'assets.push(format!("liboliphaunt-{version}-linux-x64-gnu.tar.gz"))', "Rust SDK release asset resolver must support Linux x64 liboliphaunt assets", ) + require_text( + "src/sdks/rust/src/bin/package_resources.rs", + 'assets.push(format!("oliphaunt-tools-{version}-linux-x64-gnu.tar.gz"))', + "Rust SDK release asset resolver must support split Linux x64 oliphaunt-tools assets", + ) require_text( "src/sdks/rust/src/bin/package_resources.rs", '"linux-arm64-gnu" =>', @@ -1351,8 +1356,11 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None if ( optimize_native_runtime_payload.NATIVE_RUNTIME_TOOL_STEMS != ("initdb", "pg_ctl", "postgres") or optimize_native_runtime_payload.NATIVE_TOOLS_TOOL_STEMS != ("pg_dump", "psql") - or "copy_tools_payload" not in native_packager_source - or "required_tools_member_paths" not in native_packager_source + or "missing oliphaunt-tools native release asset" not in native_packager_source + or "extract_archive(tools_archive, tools_root)" not in native_packager_source + or "validate_tools_target_pair" not in native_packager_source + or 'tool_set="runtime"' not in native_packager_source + or 'tool_set="tools"' not in native_packager_source or "package_base=TOOLS_PRODUCT" not in native_packager_source or 'artifact_product=TOOLS_PRODUCT' not in native_packager_source ): diff --git a/tools/release/optimize_native_runtime_payload.py b/tools/release/optimize_native_runtime_payload.py index b93b1087..933aa35e 100644 --- a/tools/release/optimize_native_runtime_payload.py +++ b/tools/release/optimize_native_runtime_payload.py @@ -189,6 +189,11 @@ def prune_runtime_payload( elif name not in required_tools: remove_path(path) + if tool_set == "tools" and runtime_dir.is_dir(): + for path in sorted(runtime_dir.iterdir()): + if path.name != "bin": + remove_path(path) + for relative in DEV_RUNTIME_DIRS: remove_path(runtime_dir.joinpath(*relative.parts)) @@ -314,6 +319,15 @@ def validate_runtime_tree( elif path.name not in required_tools: errors.append(f"{rel(path)} is an extra runtime tool") + if tool_set == "tools" and runtime_dir.is_dir(): + allowed = {PurePosixPath("bin") / tool for tool in required_tools} + for path in sorted(runtime_dir.rglob("*")): + if not path.is_file(): + continue + relative = PurePosixPath(path.relative_to(runtime_dir).as_posix()) + if relative not in allowed: + errors.append(f"{rel(path)} is not part of the native tools payload") + for relative in DEV_RUNTIME_DIRS: path = runtime_dir.joinpath(*relative.parts) if path.exists(): diff --git a/tools/release/package-liboliphaunt-linux-assets.sh b/tools/release/package-liboliphaunt-linux-assets.sh index cc07c782..609731be 100755 --- a/tools/release/package-liboliphaunt-linux-assets.sh +++ b/tools/release/package-liboliphaunt-linux-assets.sh @@ -50,10 +50,12 @@ embedded_modules="$work_root/out/modules" runtime="$work_root/install" stage="$stage_root/liboliphaunt-${version}-${target_id}" asset="liboliphaunt-${version}-${target_id}.tar.gz" +tools_stage="$stage_root/oliphaunt-tools-${version}-${target_id}" +tools_asset="oliphaunt-tools-${version}-${target_id}.tar.gz" catalog_file="$stage_root/extension-catalog.tsv" rm -rf "$stage_root" -mkdir -p "$out_dir" "$stage/include" "$stage/lib" "$stage/runtime" +mkdir -p "$out_dir" "$stage/include" "$stage/lib" "$stage/runtime" "$tools_stage/runtime/bin" fetch_release_source_assets @@ -75,9 +77,15 @@ rsync -a --delete "$headers_dir/" "$stage/include/" cp "$lib" "$stage/lib/" rsync -a --delete "$embedded_modules/" "$stage/lib/modules/" rsync -a --delete --exclude 'share/icu/***' "$runtime/" "$stage/runtime/" +for tool in pg_dump psql; do + cp -p "$runtime/bin/$tool" "$tools_stage/runtime/bin/" +done echo "==> Optimizing staged liboliphaunt $target_id release payload" -python3 tools/release/optimize_native_runtime_payload.py "$stage" --target "$target_id" +python3 tools/release/optimize_native_runtime_payload.py "$stage" --target "$target_id" --tool-set runtime + +echo "==> Optimizing staged oliphaunt-tools $target_id release payload" +python3 tools/release/optimize_native_runtime_payload.py "$tools_stage" --target "$target_id" --tool-set tools echo "==> Smoke testing staged liboliphaunt $target_id release layout" env \ @@ -89,4 +97,6 @@ env \ node src/runtimes/liboliphaunt/native/tools/run-host-c-smoke.mjs tools/release/archive_dir.mjs "$stage" "$out_dir/$asset" +tools/release/archive_dir.mjs "$tools_stage" "$out_dir/$tools_asset" echo "liboliphauntLinuxReleaseAsset=$out_dir/$asset" +echo "oliphauntToolsLinuxReleaseAsset=$out_dir/$tools_asset" diff --git a/tools/release/package-liboliphaunt-macos-assets.sh b/tools/release/package-liboliphaunt-macos-assets.sh index 46b0032f..289918e9 100755 --- a/tools/release/package-liboliphaunt-macos-assets.sh +++ b/tools/release/package-liboliphaunt-macos-assets.sh @@ -41,10 +41,12 @@ embedded_modules="$work_root/out/modules" runtime="$work_root/install" stage="$stage_root/liboliphaunt-${version}-${target_id}" asset="liboliphaunt-${version}-${target_id}.tar.gz" +tools_stage="$stage_root/oliphaunt-tools-${version}-${target_id}" +tools_asset="oliphaunt-tools-${version}-${target_id}.tar.gz" catalog_file="$stage_root/extension-catalog.tsv" rm -rf "$stage_root" -mkdir -p "$out_dir" "$stage/include" "$stage/lib" "$stage/runtime" +mkdir -p "$out_dir" "$stage/include" "$stage/lib" "$stage/runtime" "$tools_stage/runtime/bin" fetch_release_source_assets @@ -67,9 +69,15 @@ rsync -a --delete "$headers_dir/" "$stage/include/" cp "$lib" "$stage/lib/" rsync -a --delete "$embedded_modules/" "$stage/lib/modules/" rsync -a --delete --exclude 'share/icu/***' "$runtime/" "$stage/runtime/" +for tool in pg_dump psql; do + cp -p "$runtime/bin/$tool" "$tools_stage/runtime/bin/" +done echo "==> Optimizing staged liboliphaunt $target_id release payload" -python3 tools/release/optimize_native_runtime_payload.py "$stage" --target "$target_id" +python3 tools/release/optimize_native_runtime_payload.py "$stage" --target "$target_id" --tool-set runtime + +echo "==> Optimizing staged oliphaunt-tools $target_id release payload" +python3 tools/release/optimize_native_runtime_payload.py "$tools_stage" --target "$target_id" --tool-set tools echo "==> Smoke testing staged liboliphaunt $target_id release layout" env \ @@ -81,4 +89,6 @@ env \ node src/runtimes/liboliphaunt/native/tools/run-host-c-smoke.mjs tools/release/archive_dir.mjs "$stage" "$out_dir/$asset" +tools/release/archive_dir.mjs "$tools_stage" "$out_dir/$tools_asset" echo "liboliphauntMacosReleaseAsset=$out_dir/$asset" +echo "oliphauntToolsMacosReleaseAsset=$out_dir/$tools_asset" diff --git a/tools/release/package-liboliphaunt-windows-assets.ps1 b/tools/release/package-liboliphaunt-windows-assets.ps1 index 6af8e068..54941ade 100644 --- a/tools/release/package-liboliphaunt-windows-assets.ps1 +++ b/tools/release/package-liboliphaunt-windows-assets.ps1 @@ -97,9 +97,11 @@ $EmbeddedModules = Join-Path $WorkRoot "out/modules" $Runtime = Join-Path $WorkRoot "install" $Stage = Join-Path $StageRoot "liboliphaunt-$Version-$TargetId" $Asset = "liboliphaunt-$Version-$TargetId.zip" +$ToolsStage = Join-Path $StageRoot "oliphaunt-tools-$Version-$TargetId" +$ToolsAsset = "oliphaunt-tools-$Version-$TargetId.zip" Remove-Item -Recurse -Force $StageRoot -ErrorAction SilentlyContinue -New-Item -ItemType Directory -Force -Path $OutDir, (Join-Path $Stage "include"), (Join-Path $Stage "bin"), (Join-Path $Stage "lib"), (Join-Path $Stage "lib/modules"), (Join-Path $Stage "runtime") | Out-Null +New-Item -ItemType Directory -Force -Path $OutDir, (Join-Path $Stage "include"), (Join-Path $Stage "bin"), (Join-Path $Stage "lib"), (Join-Path $Stage "lib/modules"), (Join-Path $Stage "runtime"), (Join-Path $ToolsStage "runtime/bin") | Out-Null Write-Output "==> Building liboliphaunt $TargetId" pwsh -NoProfile -ExecutionPolicy Bypass -File src/runtimes/liboliphaunt/native/bin/build-postgres18-windows.ps1 *> "$env:TEMP\liboliphaunt-release-$TargetId.log" @@ -136,17 +138,26 @@ Copy-Item -Force $Dll (Join-Path $Stage "bin") Copy-Item -Force $ImportLib (Join-Path $Stage "lib") Copy-Item -Recurse -Force (Join-Path $EmbeddedModules "*") (Join-Path $Stage "lib/modules") Copy-Item -Recurse -Force (Join-Path $Runtime "*") (Join-Path $Stage "runtime") +foreach ($Tool in @("pg_dump.exe", "psql.exe")) { + Copy-Item -Force (Join-Path (Join-Path $Runtime "bin") $Tool) (Join-Path (Join-Path $ToolsStage "runtime/bin") $Tool) +} $StagedIcu = Join-Path $Stage "runtime/share/icu" if (Test-Path $StagedIcu) { Remove-Item -Recurse -Force $StagedIcu } Write-Output "==> Optimizing staged liboliphaunt $TargetId release payload" -python tools/release/optimize_native_runtime_payload.py $Stage --target $TargetId +python tools/release/optimize_native_runtime_payload.py $Stage --target $TargetId --tool-set runtime if ($LASTEXITCODE -ne 0) { Fail "failed to optimize staged Windows liboliphaunt release payload" } +Write-Output "==> Optimizing staged oliphaunt-tools $TargetId release payload" +python tools/release/optimize_native_runtime_payload.py $ToolsStage --target $TargetId --tool-set tools +if ($LASTEXITCODE -ne 0) { + Fail "failed to optimize staged Windows oliphaunt-tools release payload" +} + Write-Output "==> Smoke testing staged liboliphaunt $TargetId release layout" $SmokeRoot = Join-Path $env:TEMP "liboliphaunt-release-smoke-$TargetId" Remove-Item -Recurse -Force $SmokeRoot -ErrorAction SilentlyContinue @@ -165,4 +176,9 @@ bun tools/release/archive_dir.mjs $Stage (Join-Path $OutDir $Asset) if ($LASTEXITCODE -ne 0) { Fail "failed to archive Windows liboliphaunt asset" } +bun tools/release/archive_dir.mjs $ToolsStage (Join-Path $OutDir $ToolsAsset) +if ($LASTEXITCODE -ne 0) { + Fail "failed to archive Windows oliphaunt-tools asset" +} Write-Output "liboliphauntWindowsReleaseAsset=$(Join-Path $OutDir $Asset)" +Write-Output "oliphauntToolsWindowsReleaseAsset=$(Join-Path $OutDir $ToolsAsset)" diff --git a/tools/release/package_liboliphaunt_cargo_artifacts.py b/tools/release/package_liboliphaunt_cargo_artifacts.py index 906bfdcb..f8ba5fc8 100644 --- a/tools/release/package_liboliphaunt_cargo_artifacts.py +++ b/tools/release/package_liboliphaunt_cargo_artifacts.py @@ -637,25 +637,14 @@ def validate_crate_size(crate_path: Path) -> None: fail(f"{rel(crate_path)} is {size} bytes, above the crates.io 10 MiB package limit") -def copy_tools_payload(extracted_root: Path, tools_root: Path, target_id: str) -> None: - shutil.rmtree(tools_root, ignore_errors=True) - required = optimize_native_runtime_payload.required_tools_member_paths( - target_id, - prefix="runtime/bin", - ) - missing: list[str] = [] - for member in required: - source = extracted_root / member - if not source.is_file(): - missing.append(member) - continue - destination = tools_root / member - destination.parent.mkdir(parents=True, exist_ok=True) - shutil.copy2(source, destination) - source.unlink() - if missing: - fail(f"{target_id} optimized payload is missing native tools: {', '.join(missing)}") - optimize_native_runtime_payload.prune_empty_dirs(extracted_root) +def validate_tools_target_pair( + runtime_target: artifact_targets.ArtifactTarget, + tools_target: artifact_targets.ArtifactTarget, +) -> None: + if tools_target.target != runtime_target.target: + fail(f"{tools_target.id} must use target {runtime_target.target}") + if tools_target.triple != runtime_target.triple: + fail(f"{tools_target.id} must use Cargo target triple {runtime_target.triple}") def package_payload( @@ -730,6 +719,7 @@ def package_payload( def package_target( target: artifact_targets.ArtifactTarget, *, + tools_target: artifact_targets.ArtifactTarget, version: str, asset_dir: Path, source_root: Path, @@ -737,13 +727,17 @@ def package_target( cargo_target_dir: Path, part_bytes: int, ) -> list[GeneratedPackage]: + validate_tools_target_pair(target, tools_target) archive = asset_dir / target.asset_name(version) if not archive.is_file(): fail(f"missing liboliphaunt native release asset: {rel(archive)}") + tools_archive = asset_dir / tools_target.asset_name(version) + if not tools_archive.is_file(): + fail(f"missing oliphaunt-tools native release asset: {rel(tools_archive)}") extracted_root = source_root / f"{target.target}-extracted" extract_archive(archive, extracted_root) tools_root = source_root / f"{target.target}-tools-extracted" - copy_tools_payload(extracted_root, tools_root, target.target) + extract_archive(tools_archive, tools_root) optimize_native_runtime_payload.optimize_payload( extracted_root, target.target, @@ -773,7 +767,7 @@ def package_target( source_root, output_dir, cargo_target_dir, - target=target, + target=tools_target, version=version, part_bytes=part_bytes, package_base=TOOLS_PRODUCT, @@ -861,6 +855,15 @@ def main(argv: list[str]) -> int: surface=SURFACE, published_only=True, ) + tools_targets = { + target.target: target + for target in artifact_targets.artifact_targets( + product=PRODUCT, + kind=TOOLS_KIND, + surface=SURFACE, + published_only=True, + ) + } if selected: known = {target.target for target in targets} unknown = sorted(selected - known) @@ -870,9 +873,13 @@ def main(argv: list[str]) -> int: packages: list[GeneratedPackage] = [] for target in targets: + tools_target = tools_targets.get(target.target) + if tools_target is None: + fail(f"missing oliphaunt-tools Cargo artifact target for {target.target}") packages.extend( package_target( target, + tools_target=tools_target, version=args.version, asset_dir=asset_dir, source_root=source_root, diff --git a/tools/release/release.py b/tools/release/release.py index 50dfdb56..34f6220e 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -2373,20 +2373,24 @@ def stage_liboliphaunt_npm_payloads( stage / target.library_relative_path, ) extract_tar_tree(archive, "runtime", stage / "runtime") - remove_native_tools_from_runtime(stage, target.target) + ensure_native_tools_absent_from_runtime(stage, target.target) optimize_native_runtime_payload.optimize_payload(stage, target.target, tool_set="runtime") stages[package_name] = stage return stages -def remove_native_tools_from_runtime(stage: Path, target: str) -> None: +def ensure_native_tools_absent_from_runtime(stage: Path, target: str) -> None: runtime_dir = stage / "runtime" + leaked_tools: list[str] = [] for tool in optimize_native_runtime_payload.required_tools_package_tools(target, runtime_dir): path = runtime_dir / "bin" / tool - if not path.is_file(): - fail(f"{stage.relative_to(ROOT)} is missing native tools payload bin/{tool}") - path.unlink() - optimize_native_runtime_payload.prune_empty_dirs(runtime_dir) + if path.exists(): + leaked_tools.append(f"runtime/bin/{tool}") + if leaked_tools: + fail( + f"{stage.relative_to(ROOT)} root runtime package must not contain split native tools: " + + ", ".join(leaked_tools) + ) def stage_liboliphaunt_tools_npm_payloads( diff --git a/tools/test/create-liboliphaunt-release-fixture.mjs b/tools/test/create-liboliphaunt-release-fixture.mjs index caca22a9..43b5506d 100644 --- a/tools/test/create-liboliphaunt-release-fixture.mjs +++ b/tools/test/create-liboliphaunt-release-fixture.mjs @@ -8,12 +8,13 @@ import { writeEntriesArchive, } from "./release-fixture-utils.mjs"; -const NATIVE_TOOL_STEMS = ["initdb", "pg_ctl", "pg_dump", "postgres", "psql"]; +const NATIVE_RUNTIME_TOOL_STEMS = ["initdb", "pg_ctl", "postgres"]; +const NATIVE_TOOLS_TOOL_STEMS = ["pg_dump", "psql"]; function nativeRuntimeEntries({ windows = false } = {}) { const suffix = windows ? ".exe" : ""; const entries = Object.fromEntries( - NATIVE_TOOL_STEMS.map((tool) => [ + NATIVE_RUNTIME_TOOL_STEMS.map((tool) => [ `runtime/bin/${tool}${suffix}`, `not-a-real-${tool}${suffix}\n`, ]), @@ -26,7 +27,24 @@ function nativeRuntimeEntries({ windows = false } = {}) { function nativeRuntimeModes({ windows = false } = {}) { const suffix = windows ? ".exe" : ""; return Object.fromEntries( - NATIVE_TOOL_STEMS.map((tool) => [`runtime/bin/${tool}${suffix}`, 0o755]), + NATIVE_RUNTIME_TOOL_STEMS.map((tool) => [`runtime/bin/${tool}${suffix}`, 0o755]), + ); +} + +function nativeToolsEntries({ windows = false } = {}) { + const suffix = windows ? ".exe" : ""; + return Object.fromEntries( + NATIVE_TOOLS_TOOL_STEMS.map((tool) => [ + `runtime/bin/${tool}${suffix}`, + `not-a-real-${tool}${suffix}\n`, + ]), + ); +} + +function nativeToolsModes({ windows = false } = {}) { + const suffix = windows ? ".exe" : ""; + return Object.fromEntries( + NATIVE_TOOLS_TOOL_STEMS.map((tool) => [`runtime/bin/${tool}${suffix}`, 0o755]), ); } @@ -194,6 +212,11 @@ async function writeFixtureAssets(assetDir, version) { }, nativeRuntimeModes(), ); + await writeEntriesArchive( + path.join(assetDir, `oliphaunt-tools-${version}-macos-arm64.tar.gz`), + nativeToolsEntries(), + nativeToolsModes(), + ); await writeEntriesArchive( path.join(assetDir, `liboliphaunt-${version}-linux-x64-gnu.tar.gz`), { @@ -203,6 +226,11 @@ async function writeFixtureAssets(assetDir, version) { }, nativeRuntimeModes(), ); + await writeEntriesArchive( + path.join(assetDir, `oliphaunt-tools-${version}-linux-x64-gnu.tar.gz`), + nativeToolsEntries(), + nativeToolsModes(), + ); await writeEntriesArchive( path.join(assetDir, `liboliphaunt-${version}-linux-arm64-gnu.tar.gz`), { @@ -212,6 +240,11 @@ async function writeFixtureAssets(assetDir, version) { }, nativeRuntimeModes(), ); + await writeEntriesArchive( + path.join(assetDir, `oliphaunt-tools-${version}-linux-arm64-gnu.tar.gz`), + nativeToolsEntries(), + nativeToolsModes(), + ); await writeEntriesArchive( path.join(assetDir, `liboliphaunt-${version}-ios-xcframework.tar.gz`), xcframeworkEntries(), @@ -233,6 +266,11 @@ async function writeFixtureAssets(assetDir, version) { }, nativeRuntimeModes({ windows: true }), ); + await writeEntriesArchive( + path.join(assetDir, `oliphaunt-tools-${version}-windows-x64-msvc.zip`), + nativeToolsEntries({ windows: true }), + nativeToolsModes({ windows: true }), + ); await writeEntriesArchive( path.join(assetDir, `liboliphaunt-${version}-apple-spm-xcframework.zip`), xcframeworkEntries(), From 85ae3e9f0a2768c0f76b70681dadba6944e0112e Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 15:03:04 +0000 Subject: [PATCH 093/137] fix: harden split tool package validation --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 98 ++++++ examples/electron-wasix/src-wasix/Cargo.lock | 22 +- examples/tauri-wasix/src-tauri/Cargo.lock | 22 +- examples/tauri/src-tauri/Cargo.lock | 6 +- examples/tools/check-examples.sh | 1 + examples/tools/run-electron-driver-smoke.sh | 26 ++ .../tauri-sqlx-vanilla/src-tauri/Cargo.lock | 22 +- tools/release/check_consumer_shape.py | 2 + tools/release/check_release_metadata.py | 6 + tools/release/local_registry_publish.py | 58 +++- tools/release/sync-example-lockfiles.mjs | 295 ++++++++++++++---- 11 files changed, 455 insertions(+), 103 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index c2d37d7d..8ce01c25 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -5,6 +5,104 @@ installs, package production, SDK parity, dead-code cleanup, and script tooling. Keep the list ordered by dependency: prove the install/runtime shape first, then review production pipelines, then normalize implementation details. +## Active Continuation Queue: 2026-06-26 + +This section is the current working queue for the resumed validation goal. Older +checked items below are historical evidence; do not treat the goal as complete +until the current-state gates here are checked with fresh local evidence. + +### P0: Re-prove Example Local-Registry Install Paths + +- [x] Rebuild or refresh local Cargo and npm registries from current release + fixture/artifact generation paths, including native runtime crates, native + `oliphaunt-tools-*` crates, WASIX runtime/tools/AOT crates, broker crates, + extension crates, and JS packages. +- [x] Verify native Tauri installs `liboliphaunt-native-linux-x64-gnu`, + `oliphaunt-tools-linux-x64-gnu`, and selected extension crates from + `registry = "oliphaunt-local"` with no path dependency fallback. +- [x] Verify native Electron installs `@oliphaunt/ts`, native runtime/tools npm + packages, and extension npm packages from the local Verdaccio registry. +- [x] Verify Tauri WASIX, Electron WASIX, and the nested WASIX SQLx Tauri + example install `oliphaunt-wasix-tools` plus tools-AOT crates from + `registry = "oliphaunt-local"`. +- [x] Exercise runtime code paths in each example: native `pg_dump`, WASIX + `preflight_tools`, WASIX `dump_sql("--schema-only")`, and WASIX noninteractive + `psql SELECT 1`. +- [x] Run GUI/e2e smoke for native Electron, WASIX Electron, native Tauri, and + WASIX Tauri on Linux, or record the exact missing host capability. + +### P1: CI, Release, and SDK Consistency Audit + +- [x] Use subagent reviews for independent codebase audits: + examples/local-registry flows, CI/release package production, and SDK runtime + resolution parity. +- [ ] Check CI/release workflows produce exactly the current package surfaces + declared by release metadata, without duplicated target lists or hidden + registry package synthesis. +- [ ] Check Rust, JS, WASIX Rust, React Native, Kotlin, and Swift SDKs use + consistent runtime setup, extension selection, artifact validation, and tool + access semantics where the platforms overlap. +- [ ] Add or adjust machine checks for any invariant currently enforced only by + convention or docs. + +### P2: Cleanup and Tooling Migration + +- [ ] Run targeted dead-code detection for Rust, TypeScript/JavaScript, shell, + Python, and release helpers. +- [ ] Remove only confirmed dead code with reference evidence. +- [ ] Inventory remaining Python and Rust helper scripts; move nonessential + scripts to Bun where that improves local developer experience without making + critical product code less idiomatic. +- [ ] Re-run Linux CI-like and release/local-registry lanes after each tooling + migration batch. + +### Current Fresh Evidence + +- 2026-06-26: `git status --short --branch` was clean on + `f0rr0/reduce-oliphaunt-icu-crate-size` after pushing commit `a20f25f`. +- 2026-06-26: Web research confirmed `nektos/act` remains the primary local + GitHub Actions runner; use it selectively for Linux workflow smoke because + complex hosted-runner parity is limited. Pair it with static workflow checks + such as existing `actionlint`/`zizmor`-style validation instead of treating + local workflow emulation as full release proof. +- 2026-06-26: Refreshed local Cargo and Verdaccio registries from explicit + current artifact roots. Cargo resolved `oliphaunt-tools-linux-x64-gnu`, + `oliphaunt-wasix-tools`, host tools-AOT crates, selected extension crates, + and runtime crates from `oliphaunt-local`; npm resolved `@oliphaunt/ts` and + `@oliphaunt/tools-linux-x64-gnu` from Verdaccio at `0.1.0`. +- 2026-06-26: `cargo check --locked` passed through + `examples/tools/with-local-registries.sh` for native Tauri, Tauri WASIX, + Electron WASIX sidecar, and the nested WASIX SQLx Tauri example after + regenerating example lockfiles against the refreshed local Cargo registry. +- 2026-06-26: `src/bindings/wasix-rust/tools/check-examples.sh` passed, + including its copied-workspace locked Cargo check and frontend build. +- 2026-06-26: all four GUI smokes passed: + `examples/tools/run-electron-driver-smoke.sh examples/electron`, + `examples/tools/run-electron-driver-smoke.sh examples/electron-wasix`, + `examples/tools/run-tauri-webdriver-smoke.sh examples/tauri`, and + `examples/tools/run-tauri-webdriver-smoke.sh examples/tauri-wasix`. +- 2026-06-26: local Cargo crate audit found no `.crate` over 10 MiB; the + largest published local crate was + `oliphaunt-extension-postgis-wasix-aot-aarch64-unknown-linux-gnu-part-001` + at 9.74 MiB. Native runtime release assets contain `postgres`, `initdb`, and + `pg_ctl`; native tools release assets contain `pg_dump` and `psql`; WASIX + tools contain `pg_dump.wasix.wasm` and `psql.wasix.wasm`. +- 2026-06-26: subagent audits found three current guard gaps. The example + lockfile sync checker now covers native Tauri, Tauri WASIX, Electron WASIX, + and nested WASIX SQLx lockfiles, and validates local-registry checksums when + a staged Cargo index is available. Native Electron GUI smoke now asserts + `@oliphaunt/ts`, `@oliphaunt/liboliphaunt-linux-x64-gnu`, + `@oliphaunt/tools-linux-x64-gnu`, and `@oliphaunt/extension-hstore` resolve + from installed `node_modules` at `0.1.0`. Default local registry discovery no + longer scans stale-prone canonical WASIX build outputs unless they are passed + explicitly with `--artifact-root`. +- 2026-06-26: CI/release audit noted WASIX tool crates are generated and + published from validated WASIX runtime/AOT release assets, but they are not + separate GitHub release assets modeled in `artifact_targets.py` the way native + `oliphaunt-tools-*` archives are. Treat that as a pending release-asset graph + design task rather than adding target rows before producers emit real WASIX + tools archives. + ## Priority 0: Current Acceptance Gates - [x] Confirm generated Cargo crates stay under the crates.io 10 MiB limit. diff --git a/examples/electron-wasix/src-wasix/Cargo.lock b/examples/electron-wasix/src-wasix/Cargo.lock index fdb65219..f0f6f5f7 100644 --- a/examples/electron-wasix/src-wasix/Cargo.lock +++ b/examples/electron-wasix/src-wasix/Cargo.lock @@ -1549,7 +1549,7 @@ dependencies = [ name = "liboliphaunt-wasix-aot-aarch64-apple-darwin" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "f7c773796df578853baca2f0dcfb610dc78c103f17fbd260f053c5945a5d0ba1" +checksum = "19b4cb312b8aad0c3632a151c41c5a7efc482a2d022a772bb06607306aa49e5c" dependencies = [ "serde_json", "sha2 0.10.9", @@ -1559,7 +1559,7 @@ dependencies = [ name = "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "9611d8528c54f4a6981217d6acaddaba0b26cbc20841b8698cb14332fd1b8a64" +checksum = "603fd79b3d921540314b0a2ff2c99b3f7cea3ad00c51835b1b4c8e5a649e6256" dependencies = [ "serde_json", "sha2 0.10.9", @@ -1569,7 +1569,7 @@ dependencies = [ name = "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "43067bd9d8aa2499d867443a39dcba33195f83c525193a730b6e9b7d66570f88" +checksum = "025c4b99a90255fe6ab91bbcd52f7f88178c98ef2fc13ddbeb69a9963f997a25" dependencies = [ "serde_json", "sha2 0.10.9", @@ -1579,7 +1579,7 @@ dependencies = [ name = "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "8856bae97b2d60f323f5847db4223fe768a0ee34ebb785b795b11482bd1a9b86" +checksum = "21d775229d615bbc33473d2db9a99d4507801295632c245f369e4b8228c8db10" dependencies = [ "serde_json", "sha2 0.10.9", @@ -1589,7 +1589,7 @@ dependencies = [ name = "liboliphaunt-wasix-portable" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "c37d60ec719b989025b70a04e72c062afc69da9b55e26c15e2726a566da01fc2" +checksum = "02daa854eeb9f42d4a153a0915ff20f02972f13e9e9677ee4ec9ab2d82f35207" dependencies = [ "oliphaunt-extension-hstore-wasix", "oliphaunt-extension-hstore-wasix-aot-aarch64-apple-darwin", @@ -2021,7 +2021,7 @@ checksum = "5c4389eaa071ac1e9bc837958ec1f5caf7f9d44a75a789b576a4938f3f0ec7cc" name = "oliphaunt-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "4565b6dc142d9e70c4cdb7d63c7e3d2ae528e35dd7643119236bd1f712006221" +checksum = "64d462e41e6db08ef2ac2ff1d12af03be8f129316446131a2436aedf72aa1452" dependencies = [ "anyhow", "async-trait", @@ -2060,7 +2060,7 @@ dependencies = [ name = "oliphaunt-wasix-tools" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "3a767b3afef41b9d6692c74870df7739aeb208bf3078a92a116afb4558872b4d" +checksum = "8d650462930a132844428188fa1d12526dd2484e30ce1656b9723d5cc7d771b8" dependencies = [ "sha2 0.10.9", ] @@ -2069,7 +2069,7 @@ dependencies = [ name = "oliphaunt-wasix-tools-aot-aarch64-apple-darwin" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "5129bc72a7419128b828189dc54a3a5a82eafc1754b08e8b0316528fcdbfea3b" +checksum = "f8a06f357b991187874a05817226c8179fab48f6e2c26ff5d0d2f6f7f5eef3a1" dependencies = [ "serde_json", "sha2 0.10.9", @@ -2079,7 +2079,7 @@ dependencies = [ name = "oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "00ababb85de5d0fde8235e1f833726944cb4b1ff948de487166759e9d9784390" +checksum = "c94c962fff8482b62033972d0226f999d18bfab1a951dfe3b3e9845665fbe232" dependencies = [ "serde_json", "sha2 0.10.9", @@ -2089,7 +2089,7 @@ dependencies = [ name = "oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "f0efc748599c21e28a1900dc055847dbdb65f79948159fb1333229713a4b1bf5" +checksum = "8efd73a996aabcef6fe30cd22df3148cffc6da6b5a5d74c7ffff0c0c09519e75" dependencies = [ "serde_json", "sha2 0.10.9", @@ -2099,7 +2099,7 @@ dependencies = [ name = "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "608a00fadaa05b4e1d714024d1ef77d6ce536f1f547cc1dc37ed686bdf1f2340" +checksum = "161a20f9ab843569e3bd9c963a7f8d6f9f8283d70cc4f65ddf7fc516c8e04a31" dependencies = [ "serde_json", "sha2 0.10.9", diff --git a/examples/tauri-wasix/src-tauri/Cargo.lock b/examples/tauri-wasix/src-tauri/Cargo.lock index 972cdb01..f6425ecc 100644 --- a/examples/tauri-wasix/src-tauri/Cargo.lock +++ b/examples/tauri-wasix/src-tauri/Cargo.lock @@ -2742,7 +2742,7 @@ dependencies = [ name = "liboliphaunt-wasix-aot-aarch64-apple-darwin" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "f7c773796df578853baca2f0dcfb610dc78c103f17fbd260f053c5945a5d0ba1" +checksum = "19b4cb312b8aad0c3632a151c41c5a7efc482a2d022a772bb06607306aa49e5c" dependencies = [ "serde_json", "sha2 0.10.9", @@ -2752,7 +2752,7 @@ dependencies = [ name = "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "9611d8528c54f4a6981217d6acaddaba0b26cbc20841b8698cb14332fd1b8a64" +checksum = "603fd79b3d921540314b0a2ff2c99b3f7cea3ad00c51835b1b4c8e5a649e6256" dependencies = [ "serde_json", "sha2 0.10.9", @@ -2762,7 +2762,7 @@ dependencies = [ name = "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "43067bd9d8aa2499d867443a39dcba33195f83c525193a730b6e9b7d66570f88" +checksum = "025c4b99a90255fe6ab91bbcd52f7f88178c98ef2fc13ddbeb69a9963f997a25" dependencies = [ "serde_json", "sha2 0.10.9", @@ -2772,7 +2772,7 @@ dependencies = [ name = "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "8856bae97b2d60f323f5847db4223fe768a0ee34ebb785b795b11482bd1a9b86" +checksum = "21d775229d615bbc33473d2db9a99d4507801295632c245f369e4b8228c8db10" dependencies = [ "serde_json", "sha2 0.10.9", @@ -2782,7 +2782,7 @@ dependencies = [ name = "liboliphaunt-wasix-portable" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "c37d60ec719b989025b70a04e72c062afc69da9b55e26c15e2726a566da01fc2" +checksum = "02daa854eeb9f42d4a153a0915ff20f02972f13e9e9677ee4ec9ab2d82f35207" dependencies = [ "oliphaunt-extension-hstore-wasix", "oliphaunt-extension-hstore-wasix-aot-aarch64-apple-darwin", @@ -3494,7 +3494,7 @@ checksum = "5c4389eaa071ac1e9bc837958ec1f5caf7f9d44a75a789b576a4938f3f0ec7cc" name = "oliphaunt-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "4565b6dc142d9e70c4cdb7d63c7e3d2ae528e35dd7643119236bd1f712006221" +checksum = "64d462e41e6db08ef2ac2ff1d12af03be8f129316446131a2436aedf72aa1452" dependencies = [ "anyhow", "async-trait", @@ -3533,7 +3533,7 @@ dependencies = [ name = "oliphaunt-wasix-tools" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "3a767b3afef41b9d6692c74870df7739aeb208bf3078a92a116afb4558872b4d" +checksum = "8d650462930a132844428188fa1d12526dd2484e30ce1656b9723d5cc7d771b8" dependencies = [ "sha2 0.10.9", ] @@ -3542,7 +3542,7 @@ dependencies = [ name = "oliphaunt-wasix-tools-aot-aarch64-apple-darwin" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "5129bc72a7419128b828189dc54a3a5a82eafc1754b08e8b0316528fcdbfea3b" +checksum = "f8a06f357b991187874a05817226c8179fab48f6e2c26ff5d0d2f6f7f5eef3a1" dependencies = [ "serde_json", "sha2 0.10.9", @@ -3552,7 +3552,7 @@ dependencies = [ name = "oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "00ababb85de5d0fde8235e1f833726944cb4b1ff948de487166759e9d9784390" +checksum = "c94c962fff8482b62033972d0226f999d18bfab1a951dfe3b3e9845665fbe232" dependencies = [ "serde_json", "sha2 0.10.9", @@ -3562,7 +3562,7 @@ dependencies = [ name = "oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "f0efc748599c21e28a1900dc055847dbdb65f79948159fb1333229713a4b1bf5" +checksum = "8efd73a996aabcef6fe30cd22df3148cffc6da6b5a5d74c7ffff0c0c09519e75" dependencies = [ "serde_json", "sha2 0.10.9", @@ -3572,7 +3572,7 @@ dependencies = [ name = "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "608a00fadaa05b4e1d714024d1ef77d6ce536f1f547cc1dc37ed686bdf1f2340" +checksum = "161a20f9ab843569e3bd9c963a7f8d6f9f8283d70cc4f65ddf7fc516c8e04a31" dependencies = [ "serde_json", "sha2 0.10.9", diff --git a/examples/tauri/src-tauri/Cargo.lock b/examples/tauri/src-tauri/Cargo.lock index 82d353e0..62978a19 100644 --- a/examples/tauri/src-tauri/Cargo.lock +++ b/examples/tauri/src-tauri/Cargo.lock @@ -2144,7 +2144,7 @@ dependencies = [ name = "oliphaunt" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "b7037b1836ef8e0cda38807c553d54ba3f40de2c4054c1c99a02ca4b124af12d" +checksum = "c959c19f99a25ba04dc9a92f0dd042a82269507999ba972754f2b4862dbf23bf" dependencies = [ "crossbeam-channel", "flate2", @@ -2172,7 +2172,7 @@ checksum = "e8789d11e7ee362e2dce2cdf0487cc5a06a3e58441761c02b8f0ba2e27c95765" name = "oliphaunt-build" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "486249fc71f0087353b0fa81e3f3a07007fb8eab33e7586f3de6283b3b16662d" +checksum = "e2bc63e135430246c6fd1ca9c629fc6684765fbd4baa41d961639961f8bdd0d7" dependencies = [ "serde", "sha2", @@ -2230,7 +2230,7 @@ dependencies = [ name = "oliphaunt-tools-linux-x64-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "e742596e96c3ee6f4b78774497fbfdc2dfb87c5474f336f3f999c25ce95f2c38" +checksum = "b7a9bff8191d233e4e86390e4454bdb0635219dbaf8a2aab6a7e828bd9b7eaab" dependencies = [ "oliphaunt-tools-linux-x64-gnu-part-000", "sha2", diff --git a/examples/tools/check-examples.sh b/examples/tools/check-examples.sh index 1010d98c..115d488f 100755 --- a/examples/tools/check-examples.sh +++ b/examples/tools/check-examples.sh @@ -91,6 +91,7 @@ require_file "examples/tools/electron-test-driver.mjs" require_text "examples/tools/run-tauri-webdriver-smoke.sh" 'cargo install tauri-driver --locked --version 2\.0\.6' require_text "examples/tools/run-tauri-webdriver-smoke.sh" 'pnpm --filter "\./\$app_dir" install --no-frozen-lockfile' require_text "examples/tools/run-electron-driver-smoke.sh" 'pnpm --filter "\./\$app_dir" install --no-frozen-lockfile' +require_text "examples/tools/run-electron-driver-smoke.sh" 'assert_npm_package "@oliphaunt/tools-linux-x64-gnu" "0\.1\.0"' require_text "examples/tools/tauri-webdriver-smoke.mjs" 'tauri webdriver todo smoke passed' require_text "examples/tools/electron-driver-smoke.mjs" 'electron driver todo smoke passed' require_text "examples/tools/electron-test-driver.mjs" 'installElectronTodoTestDriver' diff --git a/examples/tools/run-electron-driver-smoke.sh b/examples/tools/run-electron-driver-smoke.sh index b934786a..a1345250 100755 --- a/examples/tools/run-electron-driver-smoke.sh +++ b/examples/tools/run-electron-driver-smoke.sh @@ -23,12 +23,38 @@ fi command -v node >/dev/null 2>&1 || fail "missing node" command -v pnpm >/dev/null 2>&1 || fail "missing pnpm" +assert_npm_package() { + local package_name="$1" + local expected_version="$2" + examples/tools/with-local-registries.sh pnpm --dir "$app_dir" exec node - "$package_name" "$expected_version" <<'NODE' +const fs = require('node:fs'); +const path = require('node:path'); + +const [packageName, expectedVersion] = process.argv.slice(2); +const packageJson = require.resolve(`${packageName}/package.json`); +const data = JSON.parse(fs.readFileSync(packageJson, 'utf8')); +if (data.version !== expectedVersion) { + throw new Error(`${packageName} resolved version ${data.version}, expected ${expectedVersion}`); +} +const normalized = packageJson.split(path.sep).join('/'); +if (!normalized.includes('/node_modules/')) { + throw new Error(`${packageName} resolved outside node_modules: ${packageJson}`); +} +NODE +} + electron="$root/node_modules/electron/dist/electron" if [ ! -x "$electron" ]; then fail "missing Electron executable at $electron; run pnpm install" fi examples/tools/with-local-registries.sh pnpm --filter "./$app_dir" install --no-frozen-lockfile +if [ "$app_dir" = "examples/electron" ]; then + assert_npm_package "@oliphaunt/ts" "0.1.0" + assert_npm_package "@oliphaunt/liboliphaunt-linux-x64-gnu" "0.1.0" + assert_npm_package "@oliphaunt/tools-linux-x64-gnu" "0.1.0" + assert_npm_package "@oliphaunt/extension-hstore" "0.1.0" +fi examples/tools/with-local-registries.sh pnpm --dir "$app_dir" build run_smoke=( diff --git a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock index 44f7e134..a724a595 100644 --- a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock +++ b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock @@ -2944,7 +2944,7 @@ dependencies = [ name = "liboliphaunt-wasix-aot-aarch64-apple-darwin" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "f7c773796df578853baca2f0dcfb610dc78c103f17fbd260f053c5945a5d0ba1" +checksum = "19b4cb312b8aad0c3632a151c41c5a7efc482a2d022a772bb06607306aa49e5c" dependencies = [ "serde_json", "sha2 0.10.9", @@ -2954,7 +2954,7 @@ dependencies = [ name = "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "9611d8528c54f4a6981217d6acaddaba0b26cbc20841b8698cb14332fd1b8a64" +checksum = "603fd79b3d921540314b0a2ff2c99b3f7cea3ad00c51835b1b4c8e5a649e6256" dependencies = [ "serde_json", "sha2 0.10.9", @@ -2964,7 +2964,7 @@ dependencies = [ name = "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "43067bd9d8aa2499d867443a39dcba33195f83c525193a730b6e9b7d66570f88" +checksum = "025c4b99a90255fe6ab91bbcd52f7f88178c98ef2fc13ddbeb69a9963f997a25" dependencies = [ "serde_json", "sha2 0.10.9", @@ -2974,7 +2974,7 @@ dependencies = [ name = "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "8856bae97b2d60f323f5847db4223fe768a0ee34ebb785b795b11482bd1a9b86" +checksum = "21d775229d615bbc33473d2db9a99d4507801295632c245f369e4b8228c8db10" dependencies = [ "serde_json", "sha2 0.10.9", @@ -2984,7 +2984,7 @@ dependencies = [ name = "liboliphaunt-wasix-portable" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "c37d60ec719b989025b70a04e72c062afc69da9b55e26c15e2726a566da01fc2" +checksum = "02daa854eeb9f42d4a153a0915ff20f02972f13e9e9677ee4ec9ab2d82f35207" dependencies = [ "serde", "serde_json", @@ -3574,7 +3574,7 @@ dependencies = [ name = "oliphaunt-wasix" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "4565b6dc142d9e70c4cdb7d63c7e3d2ae528e35dd7643119236bd1f712006221" +checksum = "64d462e41e6db08ef2ac2ff1d12af03be8f129316446131a2436aedf72aa1452" dependencies = [ "anyhow", "async-trait", @@ -3613,7 +3613,7 @@ dependencies = [ name = "oliphaunt-wasix-tools" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "3a767b3afef41b9d6692c74870df7739aeb208bf3078a92a116afb4558872b4d" +checksum = "8d650462930a132844428188fa1d12526dd2484e30ce1656b9723d5cc7d771b8" dependencies = [ "sha2 0.10.9", ] @@ -3622,7 +3622,7 @@ dependencies = [ name = "oliphaunt-wasix-tools-aot-aarch64-apple-darwin" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "5129bc72a7419128b828189dc54a3a5a82eafc1754b08e8b0316528fcdbfea3b" +checksum = "f8a06f357b991187874a05817226c8179fab48f6e2c26ff5d0d2f6f7f5eef3a1" dependencies = [ "serde_json", "sha2 0.10.9", @@ -3632,7 +3632,7 @@ dependencies = [ name = "oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "00ababb85de5d0fde8235e1f833726944cb4b1ff948de487166759e9d9784390" +checksum = "c94c962fff8482b62033972d0226f999d18bfab1a951dfe3b3e9845665fbe232" dependencies = [ "serde_json", "sha2 0.10.9", @@ -3642,7 +3642,7 @@ dependencies = [ name = "oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "f0efc748599c21e28a1900dc055847dbdb65f79948159fb1333229713a4b1bf5" +checksum = "8efd73a996aabcef6fe30cd22df3148cffc6da6b5a5d74c7ffff0c0c09519e75" dependencies = [ "serde_json", "sha2 0.10.9", @@ -3652,7 +3652,7 @@ dependencies = [ name = "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "608a00fadaa05b4e1d714024d1ef77d6ce536f1f547cc1dc37ed686bdf1f2340" +checksum = "161a20f9ab843569e3bd9c963a7f8d6f9f8283d70cc4f65ddf7fc516c8e04a31" dependencies = [ "serde_json", "sha2 0.10.9", diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 4c1c76dc..649cee2a 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -413,6 +413,7 @@ def check_liboliphaunt(findings: list[Finding]) -> None: native_packager = read_text("tools/release/package_liboliphaunt_cargo_artifacts.py") native_optimizer = read_text("tools/release/optimize_native_runtime_payload.py") release_cli = read_text("tools/release/release.py") + local_registry_publisher = read_text("tools/release/local_registry_publish.py") require( findings, product, @@ -430,6 +431,7 @@ def check_liboliphaunt(findings: list[Finding]) -> None: and "required_tools_member_paths" in release_cli and "stage_liboliphaunt_tools_npm_payloads" in release_cli and "ensure_native_tools_absent_from_runtime" in release_cli + and 'oliphaunt-tools-{lib_version}-*' in local_registry_publisher and "NATIVE_RUNTIME_TOOL_STEMS" in native_optimizer and "NATIVE_TOOLS_TOOL_STEMS" in native_optimizer, "Native root packages and crates must keep postgres/initdb/pg_ctl only, with pg_dump/psql published through oliphaunt-tools packages/crates.", diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index b8f17d15..df2c0099 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -359,6 +359,12 @@ def validate_local_registry_publisher() -> None: fail("local registry publisher must not append explicit artifact roots to stale default build roots") if "include_icu=False" in publisher: fail("local registry npm publishing must include the declared @oliphaunt/icu sidecar package") + if f'oliphaunt-tools-{{lib_version}}-*' not in publisher: + fail("local registry publisher must copy split oliphaunt-tools release assets when staging liboliphaunt native packages") + if 'ROOT / "target" / "oliphaunt-wasix" / "cargo-artifacts",' in publisher or ( + 'ROOT / "target" / "oliphaunt-wasix" / "release-assets",' in publisher + ): + fail("local registry publisher defaults must not silently scan stale canonical WASIX build outputs") if "def clear_local_cargo_home_cache" not in publisher or '"cache", "src", "index"' not in publisher: fail("local registry publisher must clear Cargo's local registry cache after same-version Cargo republishes") if ( diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index 42634af6..89eb666c 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -138,8 +138,6 @@ def discover_roots(artifact_roots: Iterable[Path]) -> list[Path]: ROOT / "target" / "package" / "tmp-registry", ROOT / "target" / "local-registry-generated" / "broker-cargo", ROOT / "target" / "oliphaunt-broker" / "cargo-artifacts", - ROOT / "target" / "oliphaunt-wasix" / "cargo-artifacts", - ROOT / "target" / "oliphaunt-wasix" / "release-assets", ROOT / "target" / "extension-artifacts", ] seen: set[Path] = set() @@ -282,6 +280,11 @@ def release_asset_dir_has_files(asset_dir: Path, patterns: tuple[str, ...]) -> b return any(path.is_file() for pattern in patterns for path in asset_dir.glob(pattern)) +def release_asset_dir_selected(roots: list[Path], asset_dir: Path) -> bool: + resolved = asset_dir.resolve() + return any(root.resolve() == resolved for root in roots) + + def host_npm_target() -> str | None: machine = host_platform.machine().lower() if sys.platform == "linux" and machine in {"x86_64", "amd64"}: @@ -847,6 +850,7 @@ def stage_extension_npm_packages( def write_verdaccio_config(root: Path, port: int) -> tuple[Path, bool]: + root = root.resolve() config = root / "config.yaml" storage = root / "storage" storage.mkdir(parents=True, exist_ok=True) @@ -886,6 +890,24 @@ def write_verdaccio_config(root: Path, port: int) -> tuple[Path, bool]: return config, previous != text +def npm_auth_is_valid(registry_url: str, npmrc: Path) -> bool: + completed = run( + [ + "npm", + "whoami", + "--registry", + registry_url, + "--userconfig", + str(npmrc), + "--loglevel=error", + ], + check=False, + capture=True, + timeout=10, + ) + return completed.returncode == 0 + + def stop_recorded_verdaccio(root: Path) -> None: pid_file = root / "verdaccio.pid" if not pid_file.is_file(): @@ -987,7 +1009,9 @@ def ensure_verdaccio_npmrc(root: Path, registry_url: str, dry_run: bool) -> Path "\n".join(line for line in text.splitlines() if not line.startswith("always-auth=")) + "\n", encoding="utf-8", ) - return npmrc + if npm_auth_is_valid(registry_url, npmrc): + return npmrc + npmrc.unlink() username = "oliphaunt-local" password = "oliphaunt-local" payload = json.dumps( @@ -1135,8 +1159,9 @@ def stage_release_asset_npm_packages( lib_asset_dir = ROOT / "target" / "liboliphaunt" / "release-assets" lib_version = release.current_product_version("liboliphaunt-native") - copied_lib = copy_release_assets(roots, lib_asset_dir, (f"liboliphaunt-{lib_version}-*",)) - if copied_lib or release.liboliphaunt_release_assets_ready(): + lib_patterns = (f"liboliphaunt-{lib_version}-*", f"oliphaunt-tools-{lib_version}-*") + copied_lib = copy_release_assets(roots, lib_asset_dir, lib_patterns) + if copied_lib or (release_asset_dir_selected(roots, lib_asset_dir) and release.liboliphaunt_release_assets_ready()): if copied_lib: result.staged.append(f"staged {len(copied_lib)} liboliphaunt release asset(s)") tarballs.extend( @@ -1156,8 +1181,9 @@ def stage_release_asset_npm_packages( broker_asset_dir, ("oliphaunt-broker-*.tar.gz", "oliphaunt-broker-*.zip"), ) - if copied_broker or any(broker_asset_dir.glob("oliphaunt-broker-*.tar.gz")) or any( - broker_asset_dir.glob("oliphaunt-broker-*.zip") + if copied_broker or ( + release_asset_dir_selected(roots, broker_asset_dir) + and (any(broker_asset_dir.glob("oliphaunt-broker-*.tar.gz")) or any(broker_asset_dir.glob("oliphaunt-broker-*.zip"))) ): if copied_broker: result.staged.append(f"staged {len(copied_broker)} broker release asset(s)") @@ -2345,13 +2371,16 @@ def stage_release_asset_cargo_packages( host_target = host_cargo_release_target() lib_version = release.current_product_version("liboliphaunt-native") - lib_patterns = (f"liboliphaunt-{lib_version}-*",) + lib_patterns = (f"liboliphaunt-{lib_version}-*", f"oliphaunt-tools-{lib_version}-*") lib_asset_dir = ROOT / "target" / "liboliphaunt" / "release-assets" copied_lib_assets = copy_release_assets(roots, lib_asset_dir, lib_patterns) lib_output_dir = output_root / "liboliphaunt-native" if host_target is None: result.add_skip("current host does not map to a supported native runtime Cargo target") - elif copied_lib_assets or release_asset_dir_has_files(lib_asset_dir, lib_patterns): + elif copied_lib_assets or ( + release_asset_dir_selected(roots, lib_asset_dir) + and release_asset_dir_has_files(lib_asset_dir, lib_patterns) + ): if copied_lib_assets: result.staged.append( f"staged {len(copied_lib_assets)} liboliphaunt release asset(s) for Cargo" @@ -2379,7 +2408,10 @@ def stage_release_asset_cargo_packages( broker_output_dir = output_root / "oliphaunt-broker" if host_target is None: result.add_skip("current host does not map to a supported broker Cargo target") - elif copied_broker_assets or release_asset_dir_has_files(broker_asset_dir, broker_patterns): + elif copied_broker_assets or ( + release_asset_dir_selected(roots, broker_asset_dir) + and release_asset_dir_has_files(broker_asset_dir, broker_patterns) + ): if copied_broker_assets: result.staged.append( f"staged {len(copied_broker_assets)} broker release asset(s) for Cargo" @@ -2405,7 +2437,10 @@ def stage_release_asset_cargo_packages( wasix_asset_dir = ROOT / "target" / "oliphaunt-wasix" / "release-assets" copied_wasix_assets = copy_release_assets(roots, wasix_asset_dir, wasix_patterns) wasix_output_dir = output_root / "liboliphaunt-wasix" - if copied_wasix_assets or release_asset_dir_has_files(wasix_asset_dir, wasix_patterns): + if copied_wasix_assets or ( + release_asset_dir_selected(roots, wasix_asset_dir) + and release_asset_dir_has_files(wasix_asset_dir, wasix_patterns) + ): if copied_wasix_assets: result.staged.append( f"staged {len(copied_wasix_assets)} WASIX release asset(s) for Cargo" @@ -2432,6 +2467,7 @@ def stage_release_asset_cargo_packages( def publish_cargo(roots: list[Path], registry_root: Path, dry_run: bool, strict: bool) -> SurfaceResult: + registry_root = registry_root.resolve() result = SurfaceResult("cargo") release_asset_roots = stage_release_asset_cargo_packages(roots, registry_root, dry_run, result) if release_asset_roots: diff --git a/tools/release/sync-example-lockfiles.mjs b/tools/release/sync-example-lockfiles.mjs index d1cb464a..5237fa3c 100755 --- a/tools/release/sync-example-lockfiles.mjs +++ b/tools/release/sync-example-lockfiles.mjs @@ -4,22 +4,14 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); -const lockfiles = [ - 'src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock', -]; -const internalPackageManifests = [ - 'src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml', - 'src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml', - 'src/runtimes/liboliphaunt/wasix/crates/tools/Cargo.toml', - 'src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-apple-darwin/Cargo.toml', - 'src/runtimes/liboliphaunt/wasix/crates/aot/aarch64-unknown-linux-gnu/Cargo.toml', - 'src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-pc-windows-msvc/Cargo.toml', - 'src/runtimes/liboliphaunt/wasix/crates/aot/x86_64-unknown-linux-gnu/Cargo.toml', - 'src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-apple-darwin/Cargo.toml', - 'src/runtimes/liboliphaunt/wasix/crates/tools-aot/aarch64-unknown-linux-gnu/Cargo.toml', - 'src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-pc-windows-msvc/Cargo.toml', - 'src/runtimes/liboliphaunt/wasix/crates/tools-aot/x86_64-unknown-linux-gnu/Cargo.toml', +const wasixAotTriples = [ + 'aarch64-apple-darwin', + 'aarch64-unknown-linux-gnu', + 'x86_64-pc-windows-msvc', + 'x86_64-unknown-linux-gnu', ]; +const exampleExtensions = ['hstore', 'pg-trgm', 'unaccent']; +const localRegistrySourcePrefix = 'registry+file://'; const packageStartRe = /^\s*\[\[package\]\]\s*$/u; const stringKeyRe = /^\s*([A-Za-z0-9_-]+)\s*=\s*"([^"]*)"\s*(?:#.*)?$/u; const versionLineRe = /^(\s*version\s*=\s*)"[^"]*"(\s*(?:#.*)?)$/u; @@ -33,24 +25,104 @@ function rel(file) { return path.relative(root, file).split(path.sep).join('/'); } -async function loadInternalVersions() { - const versions = new Map(); - for (const relative of internalPackageManifests) { - const manifest = path.join(root, relative); - const data = Bun.TOML.parse(await fs.readFile(manifest, 'utf8')); - const pkg = data.package; - if (typeof pkg !== 'object' || pkg === null || Array.isArray(pkg)) { - fail(`${relative} is missing [package]`); +async function pathExists(file) { + try { + await fs.stat(file); + return true; + } catch (error) { + if (error?.code === 'ENOENT') { + return false; } - const { name, version } = pkg; - if (typeof name !== 'string' || typeof version !== 'string') { - fail(`${relative} is missing package.name/version`); + throw error; + } +} + +async function readVersionFile(relative) { + return (await fs.readFile(path.join(root, relative), 'utf8')).trim(); +} + +async function readPackageVersion(relative) { + const manifest = path.join(root, relative); + const data = Bun.TOML.parse(await fs.readFile(manifest, 'utf8')); + const pkg = data.package; + if (typeof pkg !== 'object' || pkg === null || Array.isArray(pkg)) { + fail(`${relative} is missing [package]`); + } + const { version } = pkg; + if (typeof version !== 'string') { + fail(`${relative} is missing package.version`); + } + return version; +} + +async function loadVersions() { + return { + nativeRuntime: await readVersionFile('src/runtimes/liboliphaunt/native/VERSION'), + wasixRuntime: await readVersionFile('src/runtimes/liboliphaunt/wasix/VERSION'), + oliphaunt: await readPackageVersion('src/sdks/rust/Cargo.toml'), + oliphauntBuild: await readPackageVersion('src/sdks/rust/crates/oliphaunt-build/Cargo.toml'), + oliphauntWasix: await readPackageVersion('src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml'), + brokerLinuxX64: await readPackageVersion('src/runtimes/broker/crates/linux-x64-gnu/Cargo.toml'), + }; +} + +function packageSpec(name, version) { + return { name, version }; +} + +function wasixRuntimePackages(versions) { + return [ + packageSpec('oliphaunt-wasix', versions.oliphauntWasix), + packageSpec('liboliphaunt-wasix-portable', versions.wasixRuntime), + packageSpec('oliphaunt-wasix-tools', versions.wasixRuntime), + ...wasixAotTriples.map((triple) => packageSpec(`liboliphaunt-wasix-aot-${triple}`, versions.wasixRuntime)), + ...wasixAotTriples.map((triple) => packageSpec(`oliphaunt-wasix-tools-aot-${triple}`, versions.wasixRuntime)), + ]; +} + +function wasixExtensionPackages(versions) { + const packages = []; + for (const extension of exampleExtensions) { + packages.push(packageSpec(`oliphaunt-extension-${extension}-wasix`, versions.wasixRuntime)); + for (const triple of wasixAotTriples) { + packages.push(packageSpec(`oliphaunt-extension-${extension}-wasix-aot-${triple}`, versions.wasixRuntime)); } - versions.set(name, version); } - return versions; + return packages; +} + +function nativeTauriPackages(versions) { + return [ + packageSpec('oliphaunt', versions.oliphaunt), + packageSpec('oliphaunt-build', versions.oliphauntBuild), + packageSpec('liboliphaunt-native-linux-x64-gnu', versions.nativeRuntime), + packageSpec('oliphaunt-tools-linux-x64-gnu', versions.nativeRuntime), + packageSpec('oliphaunt-broker-linux-x64-gnu', versions.brokerLinuxX64), + ...exampleExtensions.map((extension) => + packageSpec(`oliphaunt-extension-${extension}-linux-x64-gnu`, versions.nativeRuntime), + ), + ]; } +const lockfiles = [ + { + path: 'examples/tauri/src-tauri/Cargo.lock', + expectedPackages: nativeTauriPackages, + }, + { + path: 'examples/tauri-wasix/src-tauri/Cargo.lock', + expectedPackages: (versions) => [...wasixRuntimePackages(versions), ...wasixExtensionPackages(versions)], + }, + { + path: 'examples/electron-wasix/src-wasix/Cargo.lock', + expectedPackages: (versions) => [...wasixRuntimePackages(versions), ...wasixExtensionPackages(versions)], + }, + { + path: 'src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock', + expectedPackages: wasixRuntimePackages, + }, +]; + function stripNewline(line) { if (line.endsWith('\r\n')) { return [line.slice(0, -2), '\r\n']; @@ -101,28 +173,128 @@ function splitLinesKeepEnds(text) { return lines; } -async function checkLockfileContainsInternalPackages(lockfile, versions) { +async function cargoLockPackages(lockfile) { const data = Bun.TOML.parse(await fs.readFile(lockfile, 'utf8')); if (!Array.isArray(data.package)) { fail(`${rel(lockfile)} is missing [[package]] entries`); } - const present = new Set( - data.package - .filter((pkg) => typeof pkg === 'object' && pkg !== null && typeof pkg.name === 'string') - .map((pkg) => pkg.name), - ); - const missing = [...versions.keys()].filter((name) => !present.has(name)).sort(); - if (missing.length > 0) { - fail(`${rel(lockfile)} is missing internal Oliphaunt packages: ${missing.join(', ')}`); + return data.package.filter((pkg) => typeof pkg === 'object' && pkg !== null && typeof pkg.name === 'string'); +} + +function packageByName(packages) { + const byName = new Map(); + for (const pkg of packages) { + const entries = byName.get(pkg.name) ?? []; + entries.push(pkg); + byName.set(pkg.name, entries); } + return byName; } -async function syncLockfile(lockfile, versions, { check }) { - await checkLockfileContainsInternalPackages(lockfile, versions); - const text = await fs.readFile(lockfile, 'utf8'); - const lines = splitLinesKeepEnds(text); +function fileUrlPath(url) { + try { + return fileURLToPath(url); + } catch { + return null; + } +} + +async function localRegistryIndexForPackage(pkg) { + const candidates = []; + const envIndex = process.env.CARGO_REGISTRIES_OLIPHAUNT_LOCAL_INDEX; + if (typeof envIndex === 'string' && envIndex.length > 0) { + candidates.push(envIndex.startsWith('file://') ? fileUrlPath(envIndex) : envIndex); + } + if (typeof pkg.source === 'string' && pkg.source.startsWith(localRegistrySourcePrefix)) { + candidates.push(fileUrlPath(pkg.source.slice('registry+'.length))); + } + candidates.push(path.join(root, 'target/local-registries/cargo/index')); + + for (const candidate of candidates) { + if (typeof candidate === 'string' && candidate.length > 0 && (await pathExists(candidate))) { + return candidate; + } + } + return null; +} + +function cargoIndexRelativePath(crateName) { + const name = crateName.toLowerCase(); + if (name.length === 1) { + return path.join('1', name); + } + if (name.length === 2) { + return path.join('2', name); + } + if (name.length === 3) { + return path.join('3', name[0], name); + } + return path.join(name.slice(0, 2), name.slice(2, 4), name); +} + +async function cargoIndexChecksum(indexDir, crateName, version) { + const indexPath = path.join(indexDir, cargoIndexRelativePath(crateName)); + const text = await fs.readFile(indexPath, 'utf8'); + for (const line of text.split(/\n/u)) { + if (line.trim().length === 0) { + continue; + } + const entry = JSON.parse(line); + if (entry.name === crateName && entry.vers === version) { + return entry.cksum; + } + } + return null; +} + +async function checkLocalRegistryChecksums(lockfile, packages) { + const failures = []; + for (const pkg of packages) { + if (typeof pkg.source !== 'string' || !pkg.source.startsWith(localRegistrySourcePrefix)) { + continue; + } + if (typeof pkg.version !== 'string' || typeof pkg.checksum !== 'string') { + failures.push(`${rel(lockfile)}: ${pkg.name} is missing version/checksum`); + continue; + } + const indexDir = await localRegistryIndexForPackage(pkg); + if (indexDir === null) { + continue; + } + const expected = await cargoIndexChecksum(indexDir, pkg.name, pkg.version); + if (expected === null) { + failures.push(`${rel(lockfile)}: ${pkg.name} ${pkg.version} is missing from ${rel(indexDir)}`); + } else if (pkg.checksum !== expected) { + failures.push( + `${rel(lockfile)}: ${pkg.name} ${pkg.version} checksum ${pkg.checksum} does not match local registry ${expected}`, + ); + } + } + return failures; +} + +function validateExpectedPackages(lockfile, packages, expectedPackages) { + const byName = packageByName(packages); + const failures = []; + for (const expected of expectedPackages) { + const entries = byName.get(expected.name) ?? []; + if (entries.length === 0) { + failures.push(`${rel(lockfile)} is missing ${expected.name}`); + continue; + } + if (!entries.some((entry) => entry.version === expected.version)) { + const actual = entries.map((entry) => entry.version).join(', '); + failures.push(`${rel(lockfile)} has ${expected.name} version ${actual}; expected ${expected.version}`); + } + if (!entries.some((entry) => typeof entry.source === 'string' && entry.source.startsWith(localRegistrySourcePrefix))) { + failures.push(`${rel(lockfile)} must resolve ${expected.name} from the local Cargo registry`); + } + } + return failures; +} + +function syncPathPackageVersions(lockfile, lines, versionsByName, { check }) { const changes = []; - const registryChanges = []; for (const [start, end] of packageBlockRanges(lines)) { const block = lines.slice(start, end); @@ -146,19 +318,15 @@ async function syncLockfile(lockfile, versions, { check }) { } } - if (!versions.has(name) || hasSource) { + if (name === null || hasSource || !versionsByName.has(name)) { continue; } if (versionIndex === null || currentVersion === null) { fail(`${rel(lockfile)} package ${name} is missing version`); } - const expectedVersion = versions.get(name); + const expectedVersion = versionsByName.get(name); if (currentVersion !== expectedVersion) { - if (hasSource) { - registryChanges.push(`${rel(lockfile)}: ${name} ${currentVersion} -> ${expectedVersion}`); - continue; - } if (!check) { lines[versionIndex] = replaceVersionLine(lines[versionIndex], expectedVersion); } @@ -166,12 +334,28 @@ async function syncLockfile(lockfile, versions, { check }) { } } - if (registryChanges.length > 0) { - for (const change of registryChanges) { - console.error(change); + return changes; +} + +async function syncLockfile(lockfileConfig, versions, { check }) { + const lockfile = path.join(root, lockfileConfig.path); + const expectedPackages = lockfileConfig.expectedPackages(versions); + const expectedVersions = new Map(expectedPackages.map((pkg) => [pkg.name, pkg.version])); + const packages = await cargoLockPackages(lockfile); + const text = await fs.readFile(lockfile, 'utf8'); + const lines = splitLinesKeepEnds(text); + const changes = syncPathPackageVersions(lockfile, lines, expectedVersions, { check }); + const failures = [ + ...validateExpectedPackages(lockfile, packages, expectedPackages), + ...(await checkLocalRegistryChecksums(lockfile, packages)), + ]; + + if (failures.length > 0) { + for (const failure of failures) { + console.error(failure); } fail( - 'registry-sourced example lockfiles are stale; run Cargo update through `examples/tools/with-local-registries.sh` after staging the local registry', + 'registry-sourced example lockfiles are stale; run Cargo update through `examples/tools/with-local-registries.sh` after staging the local Cargo registry', ); } if (changes.length > 0 && !check) { @@ -193,15 +377,14 @@ function parseArgs(argv) { } const args = parseArgs(Bun.argv.slice(2)); -const versions = await loadInternalVersions(); +const versions = await loadVersions(); const allChanges = []; -for (const relative of lockfiles) { - const lockfile = path.join(root, relative); +for (const lockfile of lockfiles) { allChanges.push(...(await syncLockfile(lockfile, versions, { check: args.check }))); } if (allChanges.length === 0) { - console.log('example lockfiles match internal package versions'); + console.log('example lockfiles match local-registry package versions and checksums'); process.exit(0); } From 9d2c90c989841c2f80239a804befef589c001097 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 15:09:48 +0000 Subject: [PATCH 094/137] fix: derive wasix package validation from manifest --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 10 +++ tools/release/check_staged_artifacts.py | 34 +++----- tools/release/release.py | 15 +--- tools/release/sync-example-lockfiles.mjs | 77 +++++++++++++++---- 4 files changed, 86 insertions(+), 50 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 8ce01c25..ae198e7e 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -39,6 +39,9 @@ until the current-state gates here are checked with fresh local evidence. - [ ] Check CI/release workflows produce exactly the current package surfaces declared by release metadata, without duplicated target lists or hidden registry package synthesis. +- [x] Derive WASIX runtime/tools Cargo package expectations from the canonical + WASIX artifact package graph in release rendering, staged-artifact validation, + and example lockfile validation. - [ ] Check Rust, JS, WASIX Rust, React Native, Kotlin, and Swift SDKs use consistent runtime setup, extension selection, artifact validation, and tool access semantics where the platforms overlap. @@ -102,6 +105,13 @@ until the current-state gates here are checked with fresh local evidence. `oliphaunt-tools-*` archives are. Treat that as a pending release-asset graph design task rather than adding target rows before producers emit real WASIX tools archives. +- 2026-06-26: WASIX Cargo package expectations are now derived from a single + package graph: `release.py` renders and validates the release `Cargo.toml` + from `public_cargo_package_names()`, staged SDK validation derives root and + tools AOT dependencies from the WASIX artifact packager helper, and + `sync-example-lockfiles.mjs` derives WASIX runtime/tools package names and AOT + triples from the `oliphaunt-wasix` manifest instead of maintaining a separate + hard-coded list. ## Priority 0: Current Acceptance Gates diff --git a/tools/release/check_staged_artifacts.py b/tools/release/check_staged_artifacts.py index cbde3235..26df50e9 100755 --- a/tools/release/check_staged_artifacts.py +++ b/tools/release/check_staged_artifacts.py @@ -26,6 +26,7 @@ from typing import NoReturn import extension_artifact_targets +import package_liboliphaunt_wasix_cargo_artifacts import product_metadata @@ -166,9 +167,9 @@ def validate_wasix_sdk_crate(crate: Path) -> None: if not isinstance(dependencies, dict): fail(f"{rel(crate)} must declare Cargo dependencies") required_dependencies = { - "liboliphaunt-wasix-portable", - "oliphaunt-wasix-tools", - "oliphaunt-icu", + package_liboliphaunt_wasix_cargo_artifacts.RUNTIME_PACKAGE, + package_liboliphaunt_wasix_cargo_artifacts.TOOLS_PACKAGE, + package_liboliphaunt_wasix_cargo_artifacts.ICU_PACKAGE, } for name in sorted(required_dependencies): dependency = dependencies.get(name) @@ -181,28 +182,15 @@ def validate_wasix_sdk_crate(crate: Path) -> None: target_tables = manifest.get("target") if not isinstance(target_tables, dict): fail(f"{rel(crate)} must declare target-specific WASIX AOT dependencies") - expected_targets = { - 'cfg(all(target_os = "macos", target_arch = "aarch64"))': [ - "liboliphaunt-wasix-aot-aarch64-apple-darwin", - "oliphaunt-wasix-tools-aot-aarch64-apple-darwin", - ], - 'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))': [ - "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", - "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu", - ], - 'cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))': [ - "liboliphaunt-wasix-aot-aarch64-unknown-linux-gnu", - "oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu", - ], - 'cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))': [ - "liboliphaunt-wasix-aot-x86_64-pc-windows-msvc", - "oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc", - ], - } - for cfg, crates in expected_targets.items(): + expected_targets: dict[str, list[str]] = {} + for cfg, name in package_liboliphaunt_wasix_cargo_artifacts.public_aot_cargo_dependencies().items(): + expected_targets.setdefault(cfg, []).append(name) + for cfg, name in package_liboliphaunt_wasix_cargo_artifacts.public_tools_aot_cargo_dependencies().items(): + expected_targets.setdefault(cfg, []).append(name) + for cfg, crates in sorted(expected_targets.items()): target = target_tables.get(cfg) target_dependencies = target.get("dependencies", {}) if isinstance(target, dict) else {} - for name in crates: + for name in sorted(crates): dependency = target_dependencies.get(name) if ( not isinstance(dependency, dict) diff --git a/tools/release/release.py b/tools/release/release.py index 34f6220e..b44ce1f2 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -712,13 +712,7 @@ def render_oliphaunt_wasix_release_cargo_toml(source: str, runtime_version: str) 'homepage = "https://oliphaunt.dev"', ) text = re.sub(r', path = "[^"]+"', "", text) - artifact_crates = { - package_liboliphaunt_wasix_cargo_artifacts.ICU_PACKAGE, - package_liboliphaunt_wasix_cargo_artifacts.RUNTIME_PACKAGE, - package_liboliphaunt_wasix_cargo_artifacts.TOOLS_PACKAGE, - *package_liboliphaunt_wasix_cargo_artifacts.AOT_PACKAGES.values(), - *package_liboliphaunt_wasix_cargo_artifacts.TOOLS_AOT_PACKAGES.values(), - } + artifact_crates = set(package_liboliphaunt_wasix_cargo_artifacts.public_cargo_package_names()) for crate in sorted(artifact_crates): pattern = rf'(?m)^({re.escape(crate)}\s*=\s*\{{[^}}\n]*version\s*=\s*")=[^"]+("[^}}\n]*\}})$' text, count = re.subn(pattern, rf"\1={runtime_version}\2", text, count=1) @@ -734,12 +728,7 @@ def validate_generated_oliphaunt_wasix_release_artifact_coverage(manifest_path: if re.search(r'=\s*\{[^}\n]*path\s*=', manifest): fail("generated oliphaunt-wasix release source must not contain local path dependencies") runtime_version = current_product_version("liboliphaunt-wasix") - required_crates = { - package_liboliphaunt_wasix_cargo_artifacts.ICU_PACKAGE, - package_liboliphaunt_wasix_cargo_artifacts.RUNTIME_PACKAGE, - package_liboliphaunt_wasix_cargo_artifacts.TOOLS_PACKAGE, - *cargo_registry_packages("liboliphaunt-wasix"), - } + required_crates = set(package_liboliphaunt_wasix_cargo_artifacts.public_cargo_package_names()) missing = [ crate for crate in sorted(required_crates) diff --git a/tools/release/sync-example-lockfiles.mjs b/tools/release/sync-example-lockfiles.mjs index 5237fa3c..00318983 100755 --- a/tools/release/sync-example-lockfiles.mjs +++ b/tools/release/sync-example-lockfiles.mjs @@ -4,12 +4,6 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); -const wasixAotTriples = [ - 'aarch64-apple-darwin', - 'aarch64-unknown-linux-gnu', - 'x86_64-pc-windows-msvc', - 'x86_64-unknown-linux-gnu', -]; const exampleExtensions = ['hstore', 'pg-trgm', 'unaccent']; const localRegistrySourcePrefix = 'registry+file://'; const packageStartRe = /^\s*\[\[package\]\]\s*$/u; @@ -55,7 +49,64 @@ async function readPackageVersion(relative) { return version; } +async function readCargoManifest(relative) { + return Bun.TOML.parse(await fs.readFile(path.join(root, relative), 'utf8')); +} + +function objectTable(value) { + return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : {}; +} + +function isWasixRuntimeArtifactDependency(name) { + return ( + name === 'liboliphaunt-wasix-portable' || + name === 'oliphaunt-wasix-tools' || + name.startsWith('liboliphaunt-wasix-aot-') || + name.startsWith('oliphaunt-wasix-tools-aot-') + ); +} + +function wasixRuntimeDependencyNames(manifest) { + const names = new Set(['oliphaunt-wasix']); + for (const name of Object.keys(objectTable(manifest.dependencies))) { + if (isWasixRuntimeArtifactDependency(name)) { + names.add(name); + } + } + for (const target of Object.values(objectTable(manifest.target))) { + for (const name of Object.keys(objectTable(objectTable(target).dependencies))) { + if (isWasixRuntimeArtifactDependency(name)) { + names.add(name); + } + } + } + const sorted = [...names].sort(); + for (const required of ['oliphaunt-wasix', 'liboliphaunt-wasix-portable', 'oliphaunt-wasix-tools']) { + if (!names.has(required)) { + fail(`oliphaunt-wasix manifest is missing required local-registry dependency ${required}`); + } + } + if (!sorted.some((name) => name.startsWith('oliphaunt-wasix-tools-aot-'))) { + fail('oliphaunt-wasix manifest is missing split tools-AOT dependencies'); + } + return sorted; +} + +function wasixAotTriplesFromDependencyNames(names) { + const prefix = 'liboliphaunt-wasix-aot-'; + const triples = names + .filter((name) => name.startsWith(prefix)) + .map((name) => name.slice(prefix.length)) + .sort(); + if (triples.length === 0) { + fail('oliphaunt-wasix manifest is missing runtime AOT dependencies'); + } + return triples; +} + async function loadVersions() { + const wasixManifest = await readCargoManifest('src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml'); + const wasixRuntimePackageNames = wasixRuntimeDependencyNames(wasixManifest); return { nativeRuntime: await readVersionFile('src/runtimes/liboliphaunt/native/VERSION'), wasixRuntime: await readVersionFile('src/runtimes/liboliphaunt/wasix/VERSION'), @@ -63,6 +114,8 @@ async function loadVersions() { oliphauntBuild: await readPackageVersion('src/sdks/rust/crates/oliphaunt-build/Cargo.toml'), oliphauntWasix: await readPackageVersion('src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml'), brokerLinuxX64: await readPackageVersion('src/runtimes/broker/crates/linux-x64-gnu/Cargo.toml'), + wasixRuntimePackageNames, + wasixAotTriples: wasixAotTriplesFromDependencyNames(wasixRuntimePackageNames), }; } @@ -71,20 +124,16 @@ function packageSpec(name, version) { } function wasixRuntimePackages(versions) { - return [ - packageSpec('oliphaunt-wasix', versions.oliphauntWasix), - packageSpec('liboliphaunt-wasix-portable', versions.wasixRuntime), - packageSpec('oliphaunt-wasix-tools', versions.wasixRuntime), - ...wasixAotTriples.map((triple) => packageSpec(`liboliphaunt-wasix-aot-${triple}`, versions.wasixRuntime)), - ...wasixAotTriples.map((triple) => packageSpec(`oliphaunt-wasix-tools-aot-${triple}`, versions.wasixRuntime)), - ]; + return versions.wasixRuntimePackageNames.map((name) => + packageSpec(name, name === 'oliphaunt-wasix' ? versions.oliphauntWasix : versions.wasixRuntime), + ); } function wasixExtensionPackages(versions) { const packages = []; for (const extension of exampleExtensions) { packages.push(packageSpec(`oliphaunt-extension-${extension}-wasix`, versions.wasixRuntime)); - for (const triple of wasixAotTriples) { + for (const triple of versions.wasixAotTriples) { packages.push(packageSpec(`oliphaunt-extension-${extension}-wasix-aot-${triple}`, versions.wasixRuntime)); } } From 08e388d063e06f991cfa4a4a616d0f0d6a6cdc47 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 15:24:33 +0000 Subject: [PATCH 095/137] fix: align sdk artifact validation semantics --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 26 +++++++++++++++ .../crates/oliphaunt-wasix/Cargo.toml | 1 + .../wasix-rust/tools/check-package.sh | 32 +++++++++++++++++++ .../oliphaunt/reactnative/OliphauntModule.kt | 6 ++++ .../react-native/ios/OliphauntAdapter.swift | 1 + .../react-native/src/__tests__/client.test.ts | 2 ++ src/sdks/react-native/src/client.ts | 2 ++ .../react-native/src/specs/NativeOliphaunt.ts | 1 + src/sdks/rust/src/config.rs | 1 + src/sdks/rust/tests/sdk_config_modes.rs | 14 ++++++++ tools/release/check_release_metadata.py | 21 ++++++++++++ 11 files changed, 107 insertions(+) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index ae198e7e..e9423b76 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -45,6 +45,13 @@ until the current-state gates here are checked with fresh local evidence. - [ ] Check Rust, JS, WASIX Rust, React Native, Kotlin, and Swift SDKs use consistent runtime setup, extension selection, artifact validation, and tool access semantics where the platforms overlap. +- [x] Align React Native package-size reports with Kotlin and Swift by carrying + `runtimeFeatures` through the native spec, Android bridge, iOS bridge, and JS + normalization. +- [ ] Fix mobile explicit `runtimeDirectory` extension validation so Kotlin, + Swift, and React Native reject selected extensions unless release-shaped + runtime resources prove extension files, static registry readiness, and + shared preload metadata. - [ ] Add or adjust machine checks for any invariant currently enforced only by convention or docs. @@ -112,6 +119,25 @@ until the current-state gates here are checked with fresh local evidence. `sync-example-lockfiles.mjs` derives WASIX runtime/tools package names and AOT triples from the `oliphaunt-wasix` manifest instead of maintaining a separate hard-coded list. +- 2026-06-26: Rust native `OpenConfig::validate()` now resolves selected + extension dependencies before runtime startup, aligning explicit validation + with the JS/Kotlin/Swift/React Native open-time extension normalization path. + The targeted `sdk_config_modes` test covers an extension with a dependency + (`earthdistance -> cube`), and release metadata checks require the validation + path to stay wired. +- 2026-06-26: `oliphaunt-wasix-dump` now declares + `required-features = ["tools"]`, so Cargo install/build semantics match the + optional split `oliphaunt-wasix-tools` package instead of installing a binary + that can only fail at runtime. `check-package.sh` and release metadata checks + enforce the field. +- 2026-06-26: React Native package-size reports now preserve `runtimeFeatures` + from Android and iOS native bridges through the JS report type, matching the + Kotlin and Swift SDK reports. Release metadata checks require the field to + remain wired across the RN surface. +- 2026-06-26: SDK parity audit found a remaining mobile P1: explicit + `runtimeDirectory` paths can bypass release-shaped exact-extension validation + in Kotlin/Swift and therefore React Native. Fixing it requires a coordinated + runtime-resource contract change, not a one-line report mapping. ## Priority 0: Current Acceptance Gates diff --git a/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml b/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml index 50c48d67..5657f7bb 100644 --- a/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml +++ b/src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml @@ -142,6 +142,7 @@ tokio-postgres = "0.7" [[bin]] name = "oliphaunt-wasix-dump" path = "src/bin/oliphaunt_wasix_dump.rs" +required-features = ["tools"] [[bin]] name = "oliphaunt-wasix-proxy" diff --git a/src/bindings/wasix-rust/tools/check-package.sh b/src/bindings/wasix-rust/tools/check-package.sh index 7f15aa8d..adc2d27f 100755 --- a/src/bindings/wasix-rust/tools/check-package.sh +++ b/src/bindings/wasix-rust/tools/check-package.sh @@ -44,4 +44,36 @@ reject_pattern '(^|/)assets/generated(/|$)' reject_pattern '^src/runtimes/' reject_pattern '^src/extensions/generated/' +if ! awk ' + /^\[\[bin\]\]/ { + if (in_bin && name == "oliphaunt-wasix-dump" && !required) { + exit 1 + } + in_bin = 1 + name = "" + required = 0 + next + } + /^\[/ { + if (in_bin && name == "oliphaunt-wasix-dump" && !required) { + exit 1 + } + in_bin = 0 + } + in_bin && /^name = "oliphaunt-wasix-dump"$/ { + name = "oliphaunt-wasix-dump" + } + in_bin && /^required-features = \["tools"\]$/ { + required = 1 + } + END { + if (in_bin && name == "oliphaunt-wasix-dump" && !required) { + exit 1 + } + } +' src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml; then + echo "oliphaunt-wasix-dump must declare required-features = [\"tools\"]" >&2 + exit 1 +fi + echo "oliphaunt-wasix package shape verified: $listing" diff --git a/src/sdks/react-native/android/src/main/java/dev/oliphaunt/reactnative/OliphauntModule.kt b/src/sdks/react-native/android/src/main/java/dev/oliphaunt/reactnative/OliphauntModule.kt index 2d7deac0..669d2090 100644 --- a/src/sdks/react-native/android/src/main/java/dev/oliphaunt/reactnative/OliphauntModule.kt +++ b/src/sdks/react-native/android/src/main/java/dev/oliphaunt/reactnative/OliphauntModule.kt @@ -607,6 +607,12 @@ class OliphauntModule( nativeModuleStems.forEach(::pushString) }, ) + putArray( + "runtimeFeatures", + WritableNativeArray().apply { + runtimeFeatures.forEach(::pushString) + }, + ) putArray( "extensions", WritableNativeArray().apply { diff --git a/src/sdks/react-native/ios/OliphauntAdapter.swift b/src/sdks/react-native/ios/OliphauntAdapter.swift index 23335447..b840bd00 100644 --- a/src/sdks/react-native/ios/OliphauntAdapter.swift +++ b/src/sdks/react-native/ios/OliphauntAdapter.swift @@ -315,6 +315,7 @@ public final class OliphauntAdapterDatabase: NSObject, @unchecked Sendable { values["mobileStaticRegistryRegistered"] = report.mobileStaticRegistryRegistered values["mobileStaticRegistryPending"] = report.mobileStaticRegistryPending values["nativeModuleStems"] = report.nativeModuleStems + values["runtimeFeatures"] = report.runtimeFeatures return values } diff --git a/src/sdks/react-native/src/__tests__/client.test.ts b/src/sdks/react-native/src/__tests__/client.test.ts index 712251a6..22e4441a 100644 --- a/src/sdks/react-native/src/__tests__/client.test.ts +++ b/src/sdks/react-native/src/__tests__/client.test.ts @@ -171,6 +171,7 @@ async function testPackageSizeReportDelegatesToNativeSdk(): Promise { mobileStaticRegistryRegistered: [], mobileStaticRegistryPending: [], nativeModuleStems: [], + runtimeFeatures: ['icu'], extensions: [ { name: 'vector', @@ -1438,6 +1439,7 @@ class MockNative implements Spec { templatePgdataBytes: 40, staticRegistryBytes: 45, selectedExtensionBytes: 30, + runtimeFeatures: ['icu'], extensions: [ { name: 'vector', diff --git a/src/sdks/react-native/src/client.ts b/src/sdks/react-native/src/client.ts index 8606aae2..1b8f4dfc 100644 --- a/src/sdks/react-native/src/client.ts +++ b/src/sdks/react-native/src/client.ts @@ -75,6 +75,7 @@ export type PackageSizeReport = { mobileStaticRegistryRegistered: string[]; mobileStaticRegistryPending: string[]; nativeModuleStems: string[]; + runtimeFeatures: string[]; extensions: ExtensionSizeReport[]; }; @@ -719,6 +720,7 @@ function normalizePackageSizeReport(native: NativePackageSizeReport): PackageSiz mobileStaticRegistryRegistered: [...(native.mobileStaticRegistryRegistered ?? [])], mobileStaticRegistryPending: [...(native.mobileStaticRegistryPending ?? [])], nativeModuleStems: [...(native.nativeModuleStems ?? [])], + runtimeFeatures: [...(native.runtimeFeatures ?? [])], extensions: native.extensions.map((extension) => ({ name: extension.name, fileCount: extension.fileCount, diff --git a/src/sdks/react-native/src/specs/NativeOliphaunt.ts b/src/sdks/react-native/src/specs/NativeOliphaunt.ts index 083313bc..7e4f73dc 100644 --- a/src/sdks/react-native/src/specs/NativeOliphaunt.ts +++ b/src/sdks/react-native/src/specs/NativeOliphaunt.ts @@ -58,6 +58,7 @@ export type NativePackageSizeReport = { mobileStaticRegistryRegistered?: Array; mobileStaticRegistryPending?: Array; nativeModuleStems?: Array; + runtimeFeatures?: Array; extensions: Array; }; diff --git a/src/sdks/rust/src/config.rs b/src/sdks/rust/src/config.rs index a3260a80..fdf1d4f4 100644 --- a/src/sdks/rust/src/config.rs +++ b/src/sdks/rust/src/config.rs @@ -307,6 +307,7 @@ impl OpenConfig { } validate_startup_identity("username", &self.username)?; validate_startup_identity("database", &self.database)?; + let _ = self.resolved_extensions()?; match self.mode { EngineMode::NativeDirect if self.direct.max_client_sessions == 0 => { Err(Error::InvalidConfig( diff --git a/src/sdks/rust/tests/sdk_config_modes.rs b/src/sdks/rust/tests/sdk_config_modes.rs index 7cef7a46..61c39bee 100644 --- a/src/sdks/rust/tests/sdk_config_modes.rs +++ b/src/sdks/rust/tests/sdk_config_modes.rs @@ -36,6 +36,20 @@ fn config_is_native_only_and_extensions_are_explicit() { ); } +#[test] +fn open_config_validation_resolves_extension_dependencies_before_runtime_selection() { + let config = Oliphaunt::builder() + .path("target/test-roots/native-direct") + .extension(Extension::Earthdistance) + .build_config() + .unwrap(); + + assert_eq!( + config.resolved_extensions().unwrap(), + vec![Extension::Cube, Extension::Earthdistance] + ); +} + #[test] fn config_rejects_invalid_connection_identity() { let username_error = Oliphaunt::builder() diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index df2c0099..9e55edca 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -456,6 +456,11 @@ def validate_rust() -> None: '"windows-x64-msvc" =>', "Rust SDK release asset resolver must support Windows x64 liboliphaunt assets", ) + require_text( + "src/sdks/rust/src/config.rs", + "let _ = self.resolved_extensions()?;", + "Rust OpenConfig::validate must resolve extension dependencies before runtime startup", + ) def validate_broker() -> None: @@ -927,6 +932,17 @@ def validate_react_native(rn_version: str, swift_version: str, kotlin_version: s '"icu": true', "React Native README must document the config plugin ICU selector", ) + for path in [ + "src/sdks/react-native/src/specs/NativeOliphaunt.ts", + "src/sdks/react-native/src/client.ts", + "src/sdks/react-native/android/src/main/java/dev/oliphaunt/reactnative/OliphauntModule.kt", + "src/sdks/react-native/ios/OliphauntAdapter.swift", + ]: + require_text( + path, + "runtimeFeatures", + "React Native package-size reports must preserve runtime feature metadata like Kotlin and Swift", + ) def validate_typescript( @@ -1358,6 +1374,11 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None or "oliphaunt-wasix-tools-aot-" not in wasix_dependency_invariant_source ): fail("WASIX release dependency invariants must cover oliphaunt-wasix-tools and tools-AOT artifact crates") + if ( + 'name = "oliphaunt-wasix-dump"\npath = "src/bin/oliphaunt_wasix_dump.rs"\nrequired-features = ["tools"]' + not in read_text("src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml") + ): + fail("oliphaunt-wasix-dump must require the tools feature at Cargo install/build time") native_packager_source = read_text("tools/release/package_liboliphaunt_cargo_artifacts.py") if ( optimize_native_runtime_payload.NATIVE_RUNTIME_TOOL_STEMS != ("initdb", "pg_ctl", "postgres") From aed770b4bd54a0b75ffcd24576d519d44023e5de Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 15:34:06 +0000 Subject: [PATCH 096/137] fix: exercise wasix tools in release check --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 6 +++ src/bindings/wasix-rust/moon.yml | 7 +++- .../wasix-rust/tools/check-release.sh | 42 +++++++++++++++++++ tools/policy/check-moon-product-graph.mjs | 5 +++ tools/release/check_consumer_shape.py | 19 +++++++++ tools/release/check_release_metadata.py | 11 +++++ 6 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 src/bindings/wasix-rust/tools/check-release.sh diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index e9423b76..3f2179c1 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -134,6 +134,12 @@ until the current-state gates here are checked with fresh local evidence. from Android and iOS native bridges through the JS report type, matching the Kotlin and Swift SDK reports. Release metadata checks require the field to remain wired across the RN surface. +- 2026-06-26: WASIX Rust `release-check` now runs a product-owned + `check-release.sh` that depends on release-shaped WASIX AOT artifacts and + executes `preflight_wasix_tools_loads_split_artifacts` with + `OLIPHAUNT_WASM_AOT_VERIFY=full`. Normal unit/package checks still compile + that path without requiring generated runtime assets, while release metadata + and consumer-shape checks require the strict preflight to stay wired. - 2026-06-26: SDK parity audit found a remaining mobile P1: explicit `runtimeDirectory` paths can bypass release-shaped exact-extension validation in Kotlin/Swift and therefore React Native. Fixing it requires a coordinated diff --git a/src/bindings/wasix-rust/moon.yml b/src/bindings/wasix-rust/moon.yml index 8c68a588..0a9d42c9 100644 --- a/src/bindings/wasix-rust/moon.yml +++ b/src/bindings/wasix-rust/moon.yml @@ -109,7 +109,9 @@ tasks: release-check: tags: ["release", "package"] - command: "bash src/bindings/wasix-rust/tools/check-package.sh" + command: "bash src/bindings/wasix-rust/tools/check-release.sh" + deps: + - "liboliphaunt-wasix:runtime-aot" env: CARGO_TARGET_DIR: "target/moon/oliphaunt-wasix-rust/release-check" inputs: @@ -119,6 +121,9 @@ tasks: - "/src/bindings/wasix-rust/**/*" - "/src/runtimes/liboliphaunt/wasix/crates/**/*" - "/src/bindings/wasix-rust/tools/check-package.sh" + - "/src/bindings/wasix-rust/tools/check-release.sh" + - "/target/oliphaunt-wasix/assets/**/*" + - "/target/oliphaunt-wasix/aot/**/*" options: cache: true runFromWorkspaceRoot: true diff --git a/src/bindings/wasix-rust/tools/check-release.sh b/src/bindings/wasix-rust/tools/check-release.sh new file mode 100644 index 00000000..ee78bb32 --- /dev/null +++ b/src/bindings/wasix-rust/tools/check-release.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -euo pipefail + +root="$(git rev-parse --show-toplevel 2>/dev/null)" || { + echo "must run inside the Oliphaunt git checkout" >&2 + exit 1 +} +cd "$root" + +fail() { + echo "check-release.sh: $*" >&2 + exit 1 +} + +run() { + printf '\n==> %s\n' "$*" + "$@" +} + +host_triple="$(rustc -vV | awk '/^host:/{print $2}')" +case "$host_triple" in + aarch64-apple-darwin|aarch64-unknown-linux-gnu|x86_64-pc-windows-msvc|x86_64-unknown-linux-gnu) + ;; + *) + fail "unsupported host target for WASIX release preflight: $host_triple" + ;; +esac + +required_artifacts=( + "target/oliphaunt-wasix/assets/bin/pg_dump.wasix.wasm" + "target/oliphaunt-wasix/assets/bin/psql.wasix.wasm" + "target/oliphaunt-wasix/aot/$host_triple/manifest.json" +) +for artifact in "${required_artifacts[@]}"; do + [[ -f "$artifact" ]] || fail "missing release-shaped WASIX artifact: $artifact" +done + +run bash src/bindings/wasix-rust/tools/check-package.sh + +run env OLIPHAUNT_WASM_AOT_VERIFY=full \ + cargo test -p oliphaunt-wasix --locked --no-default-features --features extensions,tools \ + --lib preflight_wasix_tools_loads_split_artifacts -- --nocapture diff --git a/tools/policy/check-moon-product-graph.mjs b/tools/policy/check-moon-product-graph.mjs index b6af5107..79967245 100755 --- a/tools/policy/check-moon-product-graph.mjs +++ b/tools/policy/check-moon-product-graph.mjs @@ -785,6 +785,7 @@ for (const projectId of exactExtensionProducts) { } assertTaskCommand(tasks, 'oliphaunt-wasix-rust', 'test', 'src/bindings/wasix-rust/tools/check-unit.sh'); assertTaskCommand(tasks, 'oliphaunt-wasix-rust', 'example-check', 'src/bindings/wasix-rust/tools/check-examples.sh'); +assertTaskCommand(tasks, 'oliphaunt-wasix-rust', 'release-check', 'src/bindings/wasix-rust/tools/check-release.sh'); assertTaskDependency(tasks, 'oliphaunt-broker', 'package', 'oliphaunt-broker:check'); assertTaskDependency(tasks, 'oliphaunt-broker', 'package', 'oliphaunt-broker:test'); assertTaskCommand(tasks, 'oliphaunt-broker', 'release-check', 'true'); @@ -1163,6 +1164,10 @@ assertTaskCommand(tasks, 'oliphaunt-wasix-rust', 'package-artifacts', 'tools/rel assertTaskDependency(tasks, 'oliphaunt-wasix-rust', 'package', 'oliphaunt-wasix-rust:check'); assertTaskDependency(tasks, 'oliphaunt-wasix-rust', 'package', 'oliphaunt-wasix-rust:test'); assertTaskDependency(tasks, 'oliphaunt-wasix-rust', 'package-artifacts', 'oliphaunt-wasix-rust:package'); +assertTaskDependency(tasks, 'oliphaunt-wasix-rust', 'release-check', 'liboliphaunt-wasix:runtime-aot'); +assertTaskInput(tasks, 'oliphaunt-wasix-rust', 'release-check', '/src/bindings/wasix-rust/tools/check-release.sh'); +assertTaskInput(tasks, 'oliphaunt-wasix-rust', 'release-check', '/target/oliphaunt-wasix/assets/**/*'); +assertTaskInput(tasks, 'oliphaunt-wasix-rust', 'release-check', '/target/oliphaunt-wasix/aot/**/*'); assertTaskOutput(tasks, 'oliphaunt-wasix-rust', 'package-artifacts', 'target/sdk-artifacts/oliphaunt-wasix-rust/**/*'); for (const projectId of [ 'oliphaunt-rust', diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 649cee2a..6da69d38 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -1546,6 +1546,25 @@ def check_wasm(findings: list[Finding]) -> None: ], severity="P0", ) + release_check_source = read_text("src/bindings/wasix-rust/tools/check-release.sh") + wasix_rust_moon_source = read_text("src/bindings/wasix-rust/moon.yml") + require( + findings, + product, + "wasm-tools-release-preflight", + "OLIPHAUNT_WASM_AOT_VERIFY=full" in release_check_source + and "preflight_wasix_tools_loads_split_artifacts" in release_check_source + and "--no-run" not in release_check_source + and 'command: "bash src/bindings/wasix-rust/tools/check-release.sh"' in wasix_rust_moon_source + and "liboliphaunt-wasix:runtime-aot" in wasix_rust_moon_source + and '"/target/oliphaunt-wasix/aot/**/*"' in wasix_rust_moon_source, + "WASM Rust release-check must execute the split pg_dump/psql tools preflight against release-shaped WASIX AOT artifacts.", + [ + "src/bindings/wasix-rust/tools/check-release.sh", + "src/bindings/wasix-rust/moon.yml", + ], + severity="P0", + ) runtime_version = product_metadata.read_current_version("liboliphaunt-wasix") dependencies = manifest.get("dependencies", {}) target_tables = manifest.get("target", {}) diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 9e55edca..518cc0d8 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1403,6 +1403,17 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None or "load_psql_module(&engine)" not in sdk_pg_dump_source ): fail("oliphaunt-wasix must expose an explicit split pg_dump/psql tools preflight that validates payload and AOT artifacts") + release_check_source = read_text("src/bindings/wasix-rust/tools/check-release.sh") + wasix_rust_moon_source = read_text("src/bindings/wasix-rust/moon.yml") + if ( + "OLIPHAUNT_WASM_AOT_VERIFY=full" not in release_check_source + or "preflight_wasix_tools_loads_split_artifacts" not in release_check_source + or "--no-run" in release_check_source + or 'command: "bash src/bindings/wasix-rust/tools/check-release.sh"' not in wasix_rust_moon_source + or 'liboliphaunt-wasix:runtime-aot' not in wasix_rust_moon_source + or '"/target/oliphaunt-wasix/aot/**/*"' not in wasix_rust_moon_source + ): + fail("oliphaunt-wasix-rust release-check must run the split tools preflight against release-shaped WASIX AOT artifacts") sdk_aot_source = read_text("src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs") if "missing package-manager-resolved AOT manifest for selected extension" not in sdk_aot_source: fail("oliphaunt-wasix must fail when a selected extension AOT manifest is missing for the target") From 6ced470ab4379df39bfee1eecad941cb7c1efc54 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 15:57:04 +0000 Subject: [PATCH 097/137] fix: keep wasix tools out of root crates --- ...2026-06-07-transitional-catalog-smoke.json | 2 +- .../generated/docs/extension-evidence.json | 80 +++++++++---------- .../assets/generated/asset-inputs.sha256 | 2 +- .../liboliphaunt/wasix/crates/assets/build.rs | 9 ++- .../wasix/crates/assets/src/lib.rs | 4 - tools/release/check_consumer_shape.py | 13 +-- tools/release/check_release_metadata.py | 12 ++- ...kage_liboliphaunt_wasix_cargo_artifacts.py | 4 +- tools/release/release.py | 4 +- tools/xtask/src/release_workspace.rs | 42 ++++++++-- 10 files changed, 104 insertions(+), 68 deletions(-) diff --git a/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json b/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json index ab42ac7a..20f7549a 100644 --- a/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json +++ b/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json @@ -514,7 +514,7 @@ } ], "schema": "oliphaunt-extension-evidence-v1", - "sourceDigest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f", + "sourceDigest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67", "sourceDigestInputs": [ "src/postgres/versions/18/source.toml", "src/extensions/catalog/extensions.promoted.toml", diff --git a/src/extensions/generated/docs/extension-evidence.json b/src/extensions/generated/docs/extension-evidence.json index 2f23ecd6..9777420e 100644 --- a/src/extensions/generated/docs/extension-evidence.json +++ b/src/extensions/generated/docs/extension-evidence.json @@ -20,7 +20,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -56,7 +56,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -92,7 +92,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -128,7 +128,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -164,7 +164,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -200,7 +200,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -236,7 +236,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -272,7 +272,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -308,7 +308,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -344,7 +344,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -380,7 +380,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -416,7 +416,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -452,7 +452,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -488,7 +488,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -524,7 +524,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -560,7 +560,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -596,7 +596,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -632,7 +632,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -668,7 +668,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -704,7 +704,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -740,7 +740,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -776,7 +776,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -812,7 +812,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -848,7 +848,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -884,7 +884,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -920,7 +920,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -956,7 +956,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -992,7 +992,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -1028,7 +1028,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -1064,7 +1064,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -1100,7 +1100,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -1136,7 +1136,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -1172,7 +1172,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -1208,7 +1208,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -1244,7 +1244,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -1280,7 +1280,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -1316,7 +1316,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -1352,7 +1352,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -1388,7 +1388,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f" + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" } ], "platform-targets": [ @@ -1420,7 +1420,7 @@ "path": "src/extensions/evidence/runs" } ], - "source-digest": "sha256:cd7c479a1b88c4d61213f8b856b33026f016d2598a1a761d8666b2db28e22a9f", + "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67", "source-digest-inputs": [ "src/postgres/versions/18/source.toml", "src/extensions/catalog/extensions.promoted.toml", diff --git a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 index 8bab979e..2d96c62e 100644 --- a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 +++ b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 @@ -1 +1 @@ -791b1fa125476447c37e6dca2836e760700efbf922bc320c754bdc752063d279 +8ce51e356a666dcebe4be6fba8c685b6e76f7b0c2c3ed49862df2a2df7adf33a diff --git a/src/runtimes/liboliphaunt/wasix/crates/assets/build.rs b/src/runtimes/liboliphaunt/wasix/crates/assets/build.rs index a3199788..dfacbd90 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/assets/build.rs +++ b/src/runtimes/liboliphaunt/wasix/crates/assets/build.rs @@ -534,7 +534,7 @@ fn write_source_only_assets(out: &Path, selected_extensions: &[SelectedExtension pub const SELECTED_EXTENSION_SQL_NAMES: &[&str] = {extension_sql_names};\n" ); text.push_str( - r##"pub const MANIFEST_JSON: &str = r#"{"format-version":1,"runtime":{"archive":"","sha256":"","module-sha256":"","postgres-version":"","runtime-kind":"source-only-template"},"runtime-support":[],"pg-dump":null,"psql":null,"extensions":[],"sources":[]}"#; + r##"pub const MANIFEST_JSON: &str = r#"{"format-version":1,"runtime":{"archive":"","sha256":"","module-sha256":"","postgres-version":"","runtime-kind":"source-only-template"},"runtime-support":[],"extensions":[],"sources":[]}"#; pub fn runtime_archive() -> Option<&'static [u8]> { None } pub fn pgdata_template_archive() -> Option<&'static [u8]> { None } pub fn pgdata_template_manifest() -> Option<&'static [u8]> { None } @@ -580,8 +580,11 @@ fn write_core_manifest( .filter_map(extension_manifest_entry) .collect(), ); - manifest["pg-dump"] = serde_json::Value::Null; - manifest["psql"] = serde_json::Value::Null; + let object = manifest + .as_object_mut() + .expect("generated WASIX asset manifest is an object"); + object.remove("pg-dump"); + object.remove("psql"); let rendered = serde_json::to_string_pretty(&manifest).expect("serialize core WASIX asset manifest"); fs::write(destination, format!("{rendered}\n")).expect("write core WASIX asset manifest"); diff --git a/src/runtimes/liboliphaunt/wasix/crates/assets/src/lib.rs b/src/runtimes/liboliphaunt/wasix/crates/assets/src/lib.rs index 2602e568..25e9d3cc 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/assets/src/lib.rs +++ b/src/runtimes/liboliphaunt/wasix/crates/assets/src/lib.rs @@ -16,10 +16,6 @@ pub struct AssetManifest { #[serde(default)] pub runtime_support: Vec, #[serde(default)] - pub pg_dump: Option, - #[serde(default)] - pub psql: Option, - #[serde(default)] pub initdb: Option, #[serde(default)] pub pgdata_template: Option, diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 6da69d38..21504d1a 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -1776,16 +1776,19 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: findings, product, "wasix-root-tools-split", - 'manifest["pg-dump"] = serde_json::Value::Null;' in assets_build_source - and 'manifest["psql"] = serde_json::Value::Null;' in assets_build_source - and 'manifest["pg-dump"] = serde_json::Value::Null;' in release_workspace_source - and 'manifest["psql"] = serde_json::Value::Null;' in release_workspace_source + 'object.remove("pg-dump");' in assets_build_source + and 'object.remove("psql");' in assets_build_source + and 'object.remove("pg-dump");' in release_workspace_source + and 'object.remove("psql");' in release_workspace_source + and '"pg-dump":null' not in assets_build_source + and '"psql":null' not in assets_build_source and "remove_split_wasix_tool_payload" in release_workspace_source and "retain_split_tools" in release_workspace_source + and "SPLIT_WASIX_TOOL_AOT_ARTIFACTS" in release_workspace_source and '"bin/initdb.wasix.wasm"' in assets_build_source and '"bin/pg_dump.wasix.wasm"' not in assets_build_source and '"bin/psql.wasix.wasm"' not in assets_build_source, - "WASIX root runtime asset crate must keep postgres/initdb assets only and null split tool manifest entries.", + "WASIX root runtime asset crate must keep postgres/initdb assets only and omit split tool manifest entries.", [ "src/runtimes/liboliphaunt/wasix/crates/assets/build.rs", "tools/xtask/src/release_workspace.rs", diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 518cc0d8..2c8b11c8 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1337,14 +1337,20 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None if tools_manifest.get("package", {}).get("name") != "oliphaunt-wasix-tools": fail("WASIX split tools asset crate must be oliphaunt-wasix-tools") asset_build_source = read_text("src/runtimes/liboliphaunt/wasix/crates/assets/build.rs") + release_workspace_source = read_text("tools/xtask/src/release_workspace.rs") if ( '"bin/initdb.wasix.wasm"' not in asset_build_source or '"bin/pg_dump.wasix.wasm"' in asset_build_source or '"bin/psql.wasix.wasm"' in asset_build_source - or 'manifest["pg-dump"] = serde_json::Value::Null;' not in asset_build_source - or 'manifest["psql"] = serde_json::Value::Null;' not in asset_build_source + or 'object.remove("pg-dump");' not in asset_build_source + or 'object.remove("psql");' not in asset_build_source + or 'object.remove("pg-dump");' not in release_workspace_source + or 'object.remove("psql");' not in release_workspace_source + or "SPLIT_WASIX_TOOL_AOT_ARTIFACTS" not in release_workspace_source + or '"pg-dump":null' in asset_build_source + or '"psql":null' in asset_build_source ): - fail("WASIX root runtime asset crate must embed initdb only and null split pg_dump/psql manifest entries") + fail("WASIX root runtime asset crate must embed initdb only and omit split pg_dump/psql manifest entries") tools_build_source = read_text("src/runtimes/liboliphaunt/wasix/crates/tools/build.rs") if ( '"bin/pg_dump.wasix.wasm"' not in tools_build_source diff --git a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py index 2142cc7a..8af03fd5 100644 --- a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py +++ b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py @@ -256,8 +256,8 @@ def validate_runtime_payload(root: Path) -> None: if manifest.get("extensions") != []: fail(f"{rel(root / 'manifest.json')} must have an empty extensions array") for tool_key in ["pg-dump", "psql"]: - if manifest.get(tool_key) is not None: - fail(f"{rel(root / 'manifest.json')} must not advertise split WASIX tool {tool_key}") + if tool_key in manifest: + fail(f"{rel(root / 'manifest.json')} must not contain split WASIX tool entry {tool_key}") for required in [ "oliphaunt.wasix.tar.zst", "bin/initdb.wasix.wasm", diff --git a/tools/release/release.py b/tools/release/release.py index b44ce1f2..2611c0c7 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -985,9 +985,9 @@ def validate_wasix_portable_release_asset(archive: Path) -> None: if extensions != []: fail(f"{archive.relative_to(ROOT)} asset manifest must contain an empty extensions array") for tool_key in ["pg-dump", "psql"]: - if manifest.get(tool_key) is not None: + if tool_key in manifest: fail( - f"{archive.relative_to(ROOT)} asset manifest must not advertise split WASIX tool {tool_key}" + f"{archive.relative_to(ROOT)} asset manifest must not contain split WASIX tool entry {tool_key}" ) icu_sidecar_members = sorted( member diff --git a/tools/xtask/src/release_workspace.rs b/tools/xtask/src/release_workspace.rs index 95e97d2c..c5114f8d 100644 --- a/tools/xtask/src/release_workspace.rs +++ b/tools/xtask/src/release_workspace.rs @@ -16,6 +16,7 @@ const RELEASE_RELEVANT_UNTRACKED_PATHS: &[&str] = &[ "tools/xtask", ]; const SPLIT_WASIX_TOOL_PAYLOAD_FILES: &[&str] = &["bin/pg_dump.wasix.wasm", "bin/psql.wasix.wasm"]; +const SPLIT_WASIX_TOOL_AOT_ARTIFACTS: &[&str] = &["tool:pg_dump", "tool:psql"]; pub(super) fn stage_release_workspace() -> Result<()> { let stage_root = Path::new(RELEASE_STAGE_DIR); @@ -64,10 +65,12 @@ pub(super) fn stage_release_workspace() -> Result<()> { .join("src/runtimes/liboliphaunt/wasix/crates/aot") .join(target) .join("artifacts"), + false, )?; copy_core_wasix_aot_payload( &generated_aot, &workspace.join("target/oliphaunt-wasix/aot").join(target), + true, )?; } } @@ -141,8 +144,11 @@ fn strip_core_asset_manifest_extensions(manifest_path: &Path) -> Result<()> { ) })?; extensions.clear(); - manifest["pg-dump"] = serde_json::Value::Null; - manifest["psql"] = serde_json::Value::Null; + let object = manifest + .as_object_mut() + .ok_or_else(|| anyhow!("{} must contain a JSON object", manifest_path.display()))?; + object.remove("pg-dump"); + object.remove("psql"); let rendered = serde_json::to_string_pretty(&manifest).context("serialize core WASIX asset manifest")?; fs::write(manifest_path, format!("{rendered}\n")) @@ -183,7 +189,11 @@ fn ensure_core_wasix_asset_payload(root: &Path, retain_split_tools: bool) -> Res Ok(()) } -fn copy_core_wasix_aot_payload(source: &Path, destination: &Path) -> Result<()> { +fn copy_core_wasix_aot_payload( + source: &Path, + destination: &Path, + retain_split_tools: bool, +) -> Result<()> { copy_dir_all(source, destination)?; let manifest_path = destination.join("manifest.json"); let text = fs::read_to_string(&manifest_path) @@ -221,7 +231,9 @@ fn copy_core_wasix_aot_payload(source: &Path, destination: &Path) -> Result<()> ) })?; let relative_path = validated_aot_artifact_path(path, &manifest_path, name)?; - if name.starts_with("extension:") { + if name.starts_with("extension:") + || (!retain_split_tools && SPLIT_WASIX_TOOL_AOT_ARTIFACTS.contains(&name)) + { let artifact_path = destination.join(&relative_path); if artifact_path.exists() { fs::remove_file(&artifact_path) @@ -245,7 +257,7 @@ fn copy_core_wasix_aot_payload(source: &Path, destination: &Path) -> Result<()> serde_json::to_string_pretty(&manifest).context("serialize core WASIX AOT manifest")?; fs::write(&manifest_path, format!("{rendered}\n")) .with_context(|| format!("write {}", manifest_path.display()))?; - ensure_core_wasix_aot_payload(destination) + ensure_core_wasix_aot_payload(destination, retain_split_tools) } fn validated_aot_artifact_path(path: &str, manifest_path: &Path, name: &str) -> Result { @@ -277,13 +289,14 @@ fn remove_unretained_aot_payload_files( Ok(()) } -fn ensure_core_wasix_aot_payload(root: &Path) -> Result<()> { +fn ensure_core_wasix_aot_payload(root: &Path, retain_split_tools: bool) -> Result<()> { ensure_file(&root.join("manifest.json"))?; let text = fs::read_to_string(root.join("manifest.json")) .with_context(|| format!("read {}", root.join("manifest.json").display()))?; let manifest: serde_json::Value = serde_json::from_str(&text) .with_context(|| format!("parse {}", root.join("manifest.json").display()))?; let mut retained_paths = BTreeSet::new(); + let mut retained_split_tools = BTreeSet::new(); for artifact in manifest .get("artifacts") .and_then(|value| value.as_array()) @@ -298,6 +311,13 @@ fn ensure_core_wasix_aot_payload(root: &Path) -> Result<()> { .get("name") .and_then(|value| value.as_str()) .ok_or_else(|| anyhow!("{} contains an artifact without a name", root.display()))?; + if SPLIT_WASIX_TOOL_AOT_ARTIFACTS.contains(&name) { + ensure!( + retain_split_tools, + "core WASIX AOT payload must not contain split tool artifact {name}" + ); + retained_split_tools.insert(name.to_owned()); + } ensure!( !name.starts_with("extension:"), "core WASIX AOT payload must not contain extension artifact {name}" @@ -310,6 +330,14 @@ fn ensure_core_wasix_aot_payload(root: &Path) -> Result<()> { ensure_file(&root.join(&relative_path))?; retained_paths.insert(relative_path); } + if retain_split_tools { + for required in SPLIT_WASIX_TOOL_AOT_ARTIFACTS { + ensure!( + retained_split_tools.contains(*required), + "WASIX AOT payload retained for tools must contain split tool artifact {required}" + ); + } + } for file in sorted_files(root)? { let relative = file .strip_prefix(root) @@ -482,7 +510,7 @@ fn package_release_aot_assets(output_dir: &Path, target: &str, version: &str) -> if staging.exists() { fs::remove_dir_all(&staging).with_context(|| format!("remove {}", staging.display()))?; } - copy_core_wasix_aot_payload(&generated_aot, &staging)?; + copy_core_wasix_aot_payload(&generated_aot, &staging, true)?; deterministic_tar_zst( &staging, &Path::new("target/oliphaunt-wasix/aot").join(target), From d41d97a9d2cbd0e94d92a344a114dde6094c8ddd Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 16:25:17 +0000 Subject: [PATCH 098/137] fix: validate explicit mobile runtime extensions --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 54 ++++++++- .../OliphauntAndroidRuntimeAssets.kt | 105 +++++++++++++++++- .../OliphauntAndroidRuntimeAssetsTest.kt | 99 +++++++++++++++++ .../react-native/src/__tests__/client.test.ts | 3 +- .../Oliphaunt/OliphauntNativeDirect.swift | 54 ++++++++- .../Oliphaunt/OliphauntRuntimeResources.swift | 48 ++++++++ .../Tests/OliphauntTests/OliphauntTests.swift | 54 ++++++++- .../check-sdk-mobile-extension-surface.sh | 24 ++++ tools/release/check_release_metadata.py | 60 ++++++++++ 9 files changed, 490 insertions(+), 11 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 3f2179c1..a05b1fa4 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -48,7 +48,7 @@ until the current-state gates here are checked with fresh local evidence. - [x] Align React Native package-size reports with Kotlin and Swift by carrying `runtimeFeatures` through the native spec, Android bridge, iOS bridge, and JS normalization. -- [ ] Fix mobile explicit `runtimeDirectory` extension validation so Kotlin, +- [x] Fix mobile explicit `runtimeDirectory` extension validation so Kotlin, Swift, and React Native reject selected extensions unless release-shaped runtime resources prove extension files, static registry readiness, and shared preload metadata. @@ -69,7 +69,51 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence - 2026-06-26: `git status --short --branch` was clean on - `f0rr0/reduce-oliphaunt-icu-crate-size` after pushing commit `a20f25f`. + `f0rr0/reduce-oliphaunt-icu-crate-size` at commit `6ced470`. +- 2026-06-26: Current-state example e2e re-run passed against the staged local + registries: `examples/tools/run-electron-driver-smoke.sh examples/electron`, + `examples/tools/run-electron-driver-smoke.sh examples/electron-wasix`, + `examples/tools/run-tauri-webdriver-smoke.sh examples/tauri`, and + `examples/tools/run-tauri-webdriver-smoke.sh examples/tauri-wasix`. + Native Electron verified `@oliphaunt/ts`, + `@oliphaunt/liboliphaunt-linux-x64-gnu`, + `@oliphaunt/tools-linux-x64-gnu`, and `@oliphaunt/extension-hstore` from + installed `node_modules`; WASIX Electron and Tauri exercised + `preflight_tools`, `pg_dump --schema-only`, and noninteractive `psql SELECT + 1` through the split `oliphaunt-wasix-tools` registry packages. +- 2026-06-26: `bash examples/tools/check-examples.sh` passed, and + `bash src/bindings/wasix-rust/tools/check-examples.sh` passed with its copied + workspace locked Cargo check plus frontend build. The nested WASIX SQLx + profiler also passed through `examples/tools/with-local-registries.sh cargo + run --manifest-path + src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml + --locked --bin profile_queries -- --fresh --rows 10 --json-out + target/oliphaunt-wasix-rust/examples/tauri-sqlx-vanilla/profile-e2e-2026-06-26.json`; + the generated report included startup phase `validate split WASIX tools`. +- 2026-06-26: Split root/tools package-shape checks passed with + `python3 tools/release/check_release_metadata.py`, + `python3 tools/release/check_consumer_shape.py`, + `bash tools/policy/check-native-boundaries.sh`, and + `bun tools/policy/check-wasix-release-dependency-invariants.mjs`. Local crate + payload inspection found native root crates carrying only `initdb`, `pg_ctl`, + and `postgres`; native `oliphaunt-tools-*` carrying `pg_dump` and `psql`; + WASIX root carrying only `initdb` plus runtime/template payloads; and + `oliphaunt-wasix-tools` carrying `pg_dump.wasix.wasm` and `psql.wasix.wasm`. +- 2026-06-26: Mobile explicit runtime-directory validation now requires + release-shaped `oliphaunt/runtime/files` proof before selected extensions are + accepted on Kotlin Android and Swift native-direct; React Native forwards the + same `extensions`, `runtimeDirectory`, and `resourceRoot` controls into those + SDKs. Fresh checks passed: + `bash tools/policy/check-sdk-mobile-extension-surface.sh`, + `python3 tools/release/check_release_metadata.py`, + `python3 tools/release/check_consumer_shape.py`, + `pnpm --dir src/sdks/react-native test`, + `pnpm --dir src/sdks/react-native typecheck`, + `ANDROID_HOME=$PWD/target/android-sdk ANDROID_SDK_ROOT=$PWD/target/android-sdk bash src/sdks/kotlin/tools/check-sdk.sh test-unit`, + and + `ANDROID_HOME=$PWD/target/android-sdk ANDROID_SDK_ROOT=$PWD/target/android-sdk bash src/sdks/kotlin/tools/check-sdk.sh check-static`. + `bash src/sdks/swift/tools/check-sdk.sh test-unit` remains unrun because + this Linux host does not have `swift` installed. - 2026-06-26: Web research confirmed `nektos/act` remains the primary local GitHub Actions runner; use it selectively for Linux workflow smoke because complex hosted-runner parity is limited. Pair it with static workflow checks @@ -144,6 +188,12 @@ until the current-state gates here are checked with fresh local evidence. `runtimeDirectory` paths can bypass release-shaped exact-extension validation in Kotlin/Swift and therefore React Native. Fixing it requires a coordinated runtime-resource contract change, not a one-line report mapping. +- 2026-06-26: The explicit `runtimeDirectory` mobile P1 is now fixed for + Kotlin Android and Swift native-direct. Both paths require release-shaped + runtime resources for selected extensions, validate extension install files + and static-registry readiness through the manifest path, and return shared + preload libraries from the proved runtime resources. React Native inherits + those checks through its Kotlin/Swift SDK delegation. ## Priority 0: Current Acceptance Gates diff --git a/src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssets.kt b/src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssets.kt index c3408fa9..8e5ef5b4 100644 --- a/src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssets.kt +++ b/src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssets.kt @@ -81,6 +81,7 @@ internal object OliphauntAndroidRuntimeAssets { resourceRoot: File? = null, ): OliphauntAndroidResolvedRuntime { val requestedExtensionSet = validateExtensionIds(requestedExtensions) + val explicitRuntime = explicitRuntimeDirectory?.takeIf(String::isNotEmpty) val templatePgdata = if (resourceRoot == null) { packageManifestOrNull(context.assets, TEMPLATE_PGDATA_ASSET_ROOT) @@ -93,17 +94,51 @@ internal object OliphauntAndroidRuntimeAssets { } else { filePackageManifestOrNull(resourceRoot, RUNTIME_ASSET_ROOT) } - val usePackagedRuntime = explicitRuntimeDirectory?.takeIf(String::isNotEmpty) == null - val runtimeDirectory = - explicitRuntimeDirectory?.takeIf(String::isNotEmpty) - ?: materializePackagedRuntime(context, requestedExtensionSet, packagedRuntime) + if (explicitRuntime != null) { + val sharedPreloadLibraries = + validateExplicitRuntimeDirectory( + explicitRuntime, + requestedExtensionSet, + ) + return OliphauntAndroidResolvedRuntime( + runtimeDirectory = explicitRuntime, + templatePgdata = templatePgdata, + sharedPreloadLibraries = sharedPreloadLibraries, + ) + } + + val runtimeDirectory = materializePackagedRuntime(context, requestedExtensionSet, packagedRuntime) return OliphauntAndroidResolvedRuntime( runtimeDirectory = runtimeDirectory, templatePgdata = templatePgdata, - sharedPreloadLibraries = if (usePackagedRuntime) packagedRuntime?.sharedPreloadLibraries.orEmpty() else emptySet(), + sharedPreloadLibraries = packagedRuntime?.sharedPreloadLibraries.orEmpty(), ) } + internal fun validateExplicitRuntimeDirectory( + runtimeDirectory: String, + requestedExtensions: Collection, + ): Set { + val requestedExtensionSet = validateExtensionIds(requestedExtensions) + val runtimePackage = releaseShapedRuntimePackageForDirectory(runtimeDirectory) + if (runtimePackage == null) { + if (requestedExtensionSet.isEmpty()) { + return emptySet() + } + throw OliphauntException( + "Kotlin Android Oliphaunt extensions with explicit runtimeDirectory require " + + "release-shaped runtime resources at oliphaunt/runtime/files so selected extension " + + "files, mobile static registry metadata, and shared preload libraries can be validated.", + ) + } + requirePackagedExtensions( + runtimePackage = runtimePackage, + requestedExtensions = requestedExtensionSet, + runtimeFiles = File(runtimeDirectory), + ) + return runtimePackage.sharedPreloadLibraries + } + fun packageSizeReport(assetManager: AssetManager): OliphauntPackageSizeReport? = try { assetManager.open(PACKAGE_SIZE_REPORT_ASSET).bufferedReader().use { reader -> parsePackageSizeReport(reader.readText(), PACKAGE_SIZE_REPORT_ASSET) @@ -202,6 +237,7 @@ internal object OliphauntAndroidRuntimeAssets { "oliphaunt/runtime/${runtimePackage.cacheKey}", ) materializeAssetPackage(context.assets, runtimePackage, runtimeRoot) + requireExtensionInstallFiles(runtimePackage, requestedExtensions, runtimeRoot) return runtimeRoot.absolutePath } @@ -556,6 +592,7 @@ internal object OliphauntAndroidRuntimeAssets { private fun requirePackagedExtensions( runtimePackage: OliphauntAndroidAssetPackage, requestedExtensions: Set, + runtimeFiles: File? = null, ) { val missing = requestedExtensions @@ -585,6 +622,58 @@ internal object OliphauntAndroidRuntimeAssets { ) } } + requireExtensionInstallFiles(runtimePackage, requestedExtensions, runtimeFiles) + } + + private fun requireExtensionInstallFiles( + runtimePackage: OliphauntAndroidAssetPackage, + requestedExtensions: Set, + runtimeFiles: File?, + ) { + if (requestedExtensions.isEmpty() || runtimeFiles == null) { + return + } + val extensionDirectory = File(runtimeFiles, "share/postgresql/extension") + requestedExtensions.sorted().forEach { extension -> + val control = File(extensionDirectory, "$extension.control") + if (!control.isFile) { + throw OliphauntException( + "Kotlin Android Oliphaunt runtime resources ${runtimePackage.assetRoot} " + + "declare extension $extension but are missing $extension.control", + ) + } + val installScripts = + extensionDirectory + .listFiles { file -> file.isFile && file.name.startsWith("$extension--") && file.name.endsWith(".sql") } + .orEmpty() + if (installScripts.isEmpty()) { + throw OliphauntException( + "Kotlin Android Oliphaunt runtime resources ${runtimePackage.assetRoot} " + + "declare extension $extension but are missing $extension--*.sql", + ) + } + } + } + + private fun releaseShapedRuntimePackageForDirectory(runtimeDirectory: String): OliphauntAndroidAssetPackage? { + val filesDir = File(runtimeDirectory) + if (filesDir.name != FILES_DIR_NAME) { + return null + } + val runtimeRoot = filesDir.parentFile ?: return null + if (runtimeRoot.name != "runtime") { + return null + } + val oliphauntRoot = runtimeRoot.parentFile ?: return null + if (oliphauntRoot.name != "oliphaunt") { + return null + } + val resourceRoot = oliphauntRoot.parentFile ?: return null + val expectedFiles = File(resourceRoot, "$RUNTIME_ASSET_ROOT/$FILES_DIR_NAME") + if (filesDir.canonicalPathOrAbsolute() != expectedFiles.canonicalPathOrAbsolute()) { + return null + } + return filePackageManifestOrNull(resourceRoot, RUNTIME_ASSET_ROOT) } private fun validateExtensionIds(values: Collection): Set = validatePortableIds(values, label = "extension id") @@ -791,4 +880,10 @@ internal object OliphauntAndroidRuntimeAssets { } catch (_: IOException) { null } + + private fun File.canonicalPathOrAbsolute(): String = try { + canonicalPath + } catch (_: IOException) { + absolutePath + } } diff --git a/src/sdks/kotlin/oliphaunt/src/androidUnitTest/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssetsTest.kt b/src/sdks/kotlin/oliphaunt/src/androidUnitTest/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssetsTest.kt index a8cb94f5..b36d72bc 100644 --- a/src/sdks/kotlin/oliphaunt/src/androidUnitTest/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssetsTest.kt +++ b/src/sdks/kotlin/oliphaunt/src/androidUnitTest/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssetsTest.kt @@ -139,6 +139,72 @@ class OliphauntAndroidRuntimeAssetsTest { } } + @Test + fun validatesExplicitRuntimeDirectoryAgainstReleaseShapedResources() { + val resourceRoot = Files.createTempDirectory("liboliphaunt-explicit-runtime").toFile() + try { + val runtimeFiles = + writeReleaseShapedRuntime( + resourceRoot, + extensions = "vector", + sharedPreloadLibraries = "pg_search", + ) + + val sharedPreloadLibraries = + OliphauntAndroidRuntimeAssets.validateExplicitRuntimeDirectory( + runtimeFiles.absolutePath, + listOf("vector"), + ) + + assertEquals(setOf("pg_search"), sharedPreloadLibraries) + } finally { + resourceRoot.deleteRecursively() + } + } + + @Test + fun rejectsExplicitRuntimeDirectoryWithoutReleaseShapedProofForExtensions() { + val runtimeDirectory = Files.createTempDirectory("liboliphaunt-unproved-runtime").toFile() + try { + val error = + assertFailsWith { + OliphauntAndroidRuntimeAssets.validateExplicitRuntimeDirectory( + runtimeDirectory.absolutePath, + listOf("vector"), + ) + } + + assertTrue(error.message.orEmpty().contains("release-shaped runtime resources")) + } finally { + runtimeDirectory.deleteRecursively() + } + } + + @Test + fun rejectsExplicitRuntimeDirectoryWithMissingExtensionInstallFiles() { + val resourceRoot = Files.createTempDirectory("liboliphaunt-explicit-runtime-missing-extension").toFile() + try { + val runtimeFiles = + writeReleaseShapedRuntime( + resourceRoot, + extensions = "vector", + includeSql = false, + ) + + val error = + assertFailsWith { + OliphauntAndroidRuntimeAssets.validateExplicitRuntimeDirectory( + runtimeFiles.absolutePath, + listOf("vector"), + ) + } + + assertTrue(error.message.orEmpty().contains("missing vector--*.sql")) + } finally { + resourceRoot.deleteRecursively() + } + } + @Test fun returnsNullWhenPackageSizeReportIsAbsentFromResourceRoot() { val resourceRoot = Files.createTempDirectory("liboliphaunt-resource-report-absent").toFile() @@ -604,3 +670,36 @@ private fun validPackageSizeReport(vararg extensionRows: String): String { ) + extensionRows return rows.joinToString("\n") } + +private fun writeReleaseShapedRuntime( + resourceRoot: java.io.File, + extensions: String, + sharedPreloadLibraries: String = "", + includeControl: Boolean = true, + includeSql: Boolean = true, +): java.io.File { + val runtimeRoot = resourceRoot.resolve("oliphaunt/runtime") + runtimeRoot.mkdirs() + runtimeRoot.resolve("manifest.properties").writeText( + """ + schema=oliphaunt-runtime-resources-v1 + layout=postgres-runtime-files-v1 + cacheKey=runtime-smoke + extensions=$extensions + sharedPreloadLibraries=$sharedPreloadLibraries + mobileStaticRegistryState=complete + mobileStaticRegistryRegistered=$extensions + mobileStaticRegistryPending= + nativeModuleStems=$extensions + """.trimIndent(), + ) + val extensionDirectory = runtimeRoot.resolve("files/share/postgresql/extension") + extensionDirectory.mkdirs() + if (includeControl) { + extensionDirectory.resolve("vector.control").writeText("comment = 'vector smoke control'\n") + } + if (includeSql) { + extensionDirectory.resolve("vector--1.0.sql").writeText("select 'vector smoke sql';\n") + } + return runtimeRoot.resolve("files") +} diff --git a/src/sdks/react-native/src/__tests__/client.test.ts b/src/sdks/react-native/src/__tests__/client.test.ts index 22e4441a..1bd15cd5 100644 --- a/src/sdks/react-native/src/__tests__/client.test.ts +++ b/src/sdks/react-native/src/__tests__/client.test.ts @@ -987,6 +987,7 @@ async function testOpenForwardsNativeRuntimeOverrides(): Promise { startupGUCs: [{ name: 'shared_buffers', value: '16MB' }, 'wal_buffers=256kB'], username: 'app_user', database: 'app_db', + extensions: ['hstore', 'unaccent'], libraryPath: '/tmp/oliphaunt.dylib', runtimeDirectory: '/tmp/postgres-install', resourceRoot: '/tmp/oliphaunt-resources', @@ -1001,7 +1002,7 @@ async function testOpenForwardsNativeRuntimeOverrides(): Promise { startupGUCs: ['shared_buffers=16MB', 'wal_buffers=256kB'], username: 'app_user', database: 'app_db', - extensions: undefined, + extensions: ['hstore', 'unaccent'], libraryPath: '/tmp/oliphaunt.dylib', runtimeDirectory: '/tmp/postgres-install', resourceRoot: '/tmp/oliphaunt-resources', diff --git a/src/sdks/swift/Sources/Oliphaunt/OliphauntNativeDirect.swift b/src/sdks/swift/Sources/Oliphaunt/OliphauntNativeDirect.swift index 5f8afce6..312cc6ee 100644 --- a/src/sdks/swift/Sources/Oliphaunt/OliphauntNativeDirect.swift +++ b/src/sdks/swift/Sources/Oliphaunt/OliphauntNativeDirect.swift @@ -147,7 +147,11 @@ public struct OliphauntNativeDirectEngine: OliphauntEngine, OliphauntEngineSuppo runtimeResources: OliphauntRuntimeResources? ) throws -> ResolvedNativeRuntime { if let runtimeDirectory { - return ResolvedNativeRuntime(directory: runtimeDirectory) + return try resolveExplicitRuntimeDirectory( + runtimeDirectory, + extensions: extensions, + runtimeResources: runtimeResources + ) } if let runtimeResources { return ResolvedNativeRuntime( @@ -156,7 +160,11 @@ public struct OliphauntNativeDirectEngine: OliphauntEngine, OliphauntEngineSuppo ) } if let environmentRuntimeDirectory = Self.environmentRuntimeDirectory() { - return ResolvedNativeRuntime(directory: environmentRuntimeDirectory) + return try resolveExplicitRuntimeDirectory( + environmentRuntimeDirectory, + extensions: extensions, + runtimeResources: nil + ) } if !extensions.isEmpty { throw OliphauntError.engine( @@ -166,6 +174,48 @@ public struct OliphauntNativeDirectEngine: OliphauntEngine, OliphauntEngineSuppo return ResolvedNativeRuntime() } + private func resolveExplicitRuntimeDirectory( + _ directory: URL, + extensions: [String], + runtimeResources: OliphauntRuntimeResources? + ) throws -> ResolvedNativeRuntime { + let resources = + try matchingRuntimeResources( + directory: directory, + runtimeResources: runtimeResources + ) + if let resources { + return ResolvedNativeRuntime( + directory: directory, + sharedPreloadLibraries: try resources.sharedPreloadLibraries( + forRuntimeDirectory: directory, + requestedExtensions: extensions + ) + ) + } + if !extensions.isEmpty { + throw OliphauntError.engine( + "Swift native-direct extensions with explicit runtimeDirectory require release-shaped OliphauntRuntimeResources at oliphaunt/runtime/files so selected extension files, mobile static registry metadata, and shared preload libraries can be validated" + ) + } + return ResolvedNativeRuntime(directory: directory) + } + + private func matchingRuntimeResources( + directory: URL, + runtimeResources: OliphauntRuntimeResources? + ) throws -> OliphauntRuntimeResources? { + if let runtimeResources, + (try? runtimeResources.sharedPreloadLibraries(forRuntimeDirectory: directory)) != nil + { + return runtimeResources + } + return try OliphauntRuntimeResources.releaseShapedResources( + forRuntimeDirectory: directory, + cacheRoot: runtimeResources?.cacheRoot ?? OliphauntRuntimeResources.defaultCacheRoot() + ) + } + private struct ResolvedNativeRuntime { var directory: URL? = nil var sharedPreloadLibraries: [String] = [] diff --git a/src/sdks/swift/Sources/Oliphaunt/OliphauntRuntimeResources.swift b/src/sdks/swift/Sources/Oliphaunt/OliphauntRuntimeResources.swift index 4cdd1351..22016ea1 100644 --- a/src/sdks/swift/Sources/Oliphaunt/OliphauntRuntimeResources.swift +++ b/src/sdks/swift/Sources/Oliphaunt/OliphauntRuntimeResources.swift @@ -517,6 +517,49 @@ public struct OliphauntRuntimeResources: Sendable { return runtime.sharedPreloadLibraries.sorted() } + func sharedPreloadLibraries( + forRuntimeDirectory runtimeDirectory: URL, + requestedExtensions: [String] = [] + ) throws -> [String] { + let requested = try Self.validateExtensionIds(requestedExtensions) + let runtime = try assetPackage(kind: .runtime) + guard Self.sameFileURL(runtime.filesURL, runtimeDirectory) else { + throw OliphauntError.engine( + "Swift Oliphaunt runtimeDirectory \(runtimeDirectory.path) is not the files directory for runtime resources \(runtime.rootURL.path)" + ) + } + try require(runtime: runtime, contains: requested) + return runtime.sharedPreloadLibraries.sorted() + } + + static func releaseShapedResources( + forRuntimeDirectory runtimeDirectory: URL, + cacheRoot: URL = Self.defaultCacheRoot() + ) throws -> OliphauntRuntimeResources? { + let filesURL = runtimeDirectory.standardizedFileURL + guard filesURL.lastPathComponent == "files" else { + return nil + } + let runtimeRoot = filesURL.deletingLastPathComponent() + guard runtimeRoot.lastPathComponent == "runtime" else { + return nil + } + let resourceRoot = runtimeRoot.deletingLastPathComponent() + guard resourceRoot.lastPathComponent == "oliphaunt" else { + return nil + } + let resources = OliphauntRuntimeResources( + resourceRoot: resourceRoot, + cacheRoot: cacheRoot + ) + guard let runtime = try resources.optionalAssetPackage(kind: .runtime), + Self.sameFileURL(runtime.filesURL, runtimeDirectory) + else { + return nil + } + return resources + } + func hasPackagedResources(containing requestedExtensions: Set = []) throws -> Bool { guard FileManager.default.fileExists( atPath: resourceRoot.appendingPathComponent("runtime/manifest.properties").path @@ -700,6 +743,11 @@ public struct OliphauntRuntimeResources: Sendable { } } + private static func sameFileURL(_ left: URL, _ right: URL) -> Bool { + left.standardizedFileURL.resolvingSymlinksInPath().path == + right.standardizedFileURL.resolvingSymlinksInPath().path + } + private func assetPackage(kind: AssetPackageKind) throws -> AssetPackage { guard let package = try optionalAssetPackage(kind: kind) else { throw OliphauntError.engine("missing packaged liboliphaunt \(kind.label) resources at \(kind.root(in: resourceRoot).path)") diff --git a/src/sdks/swift/Tests/OliphauntTests/OliphauntTests.swift b/src/sdks/swift/Tests/OliphauntTests/OliphauntTests.swift index 9ae8ae84..7d08d2cd 100644 --- a/src/sdks/swift/Tests/OliphauntTests/OliphauntTests.swift +++ b/src/sdks/swift/Tests/OliphauntTests/OliphauntTests.swift @@ -1088,7 +1088,7 @@ func nativeDirectExtensionIdsArePortable() async throws { } @Test -func nativeDirectExtensionsUseExplicitRuntimeDirectory() async throws { +func nativeDirectExtensionsRejectUnprovedExplicitRuntimeDirectory() async throws { let root = try makeExistingPgdataRoot() defer { try? FileManager.default.removeItem(at: root) @@ -1098,6 +1098,34 @@ func nativeDirectExtensionsUseExplicitRuntimeDirectory() async throws { runtimeDirectory: URL(fileURLWithPath: "/tmp/oliphaunt-swift-runtime") ) + do { + _ = try await OliphauntDatabase.open( + configuration: OliphauntConfiguration( + mode: .nativeDirect, + root: root, + extensions: ["vector"] + ), + engine: engine + ) + Issue.record("explicit runtimeDirectory with extensions should require release-shaped proof") + } catch OliphauntError.engine(let message) { + #expect(message.contains("release-shaped OliphauntRuntimeResources")) + } +} + +@Test +func nativeDirectExtensionsUseExplicitRuntimeDirectory() async throws { + let fixture = try makeRuntimeResourceFixture() + let root = try makeExistingPgdataRoot() + defer { + try? FileManager.default.removeItem(at: fixture.root) + try? FileManager.default.removeItem(at: root) + } + let engine = OliphauntNativeDirectEngine( + libraryURL: URL(fileURLWithPath: "/tmp/oliphaunt-swift-missing.dylib"), + runtimeDirectory: fixture.resourceRoot.appendingPathComponent("runtime/files", isDirectory: true) + ) + do { _ = try await OliphauntDatabase.open( configuration: OliphauntConfiguration( @@ -1165,6 +1193,30 @@ func runtimeResourcesExposeManifestSharedPreloadLibraries() throws { ]) } +@Test +func runtimeResourcesValidateExplicitRuntimeDirectory() throws { + let fixture = try makeRuntimeResourceFixture(sharedPreloadLibraries: "pg_search") + defer { + try? FileManager.default.removeItem(at: fixture.root) + } + let resources = OliphauntRuntimeResources( + resourceRoot: fixture.resourceRoot, + cacheRoot: fixture.cacheRoot + ) + let runtimeDirectory = fixture.resourceRoot + .appendingPathComponent("runtime/files", isDirectory: true) + + #expect(try resources.sharedPreloadLibraries( + forRuntimeDirectory: runtimeDirectory, + requestedExtensions: ["vector"] + ) == ["pg_search"]) + let inferred = try #require(try OliphauntRuntimeResources.releaseShapedResources( + forRuntimeDirectory: runtimeDirectory, + cacheRoot: fixture.cacheRoot + )) + #expect(inferred.resourceRoot.standardizedFileURL == fixture.resourceRoot.standardizedFileURL) +} + @Test func runtimeResourcesDiscoverBundledResourceDirectoryCandidates() throws { let fixture = try makeRuntimeResourceFixture() diff --git a/tools/policy/check-sdk-mobile-extension-surface.sh b/tools/policy/check-sdk-mobile-extension-surface.sh index a6db4d2f..5744d21f 100755 --- a/tools/policy/check-sdk-mobile-extension-surface.sh +++ b/tools/policy/check-sdk-mobile-extension-surface.sh @@ -74,6 +74,16 @@ require_text src/sdks/kotlin/README.md "Maven Central artifact is the Android SD "Kotlin docs must state that Maven does not implicitly ship liboliphaunt/runtime/extension assets" require_text src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssets.kt "Available extensions" \ "Kotlin Android resource parser must validate exact extension availability" +require_text src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssets.kt "validateExplicitRuntimeDirectory" \ + "Kotlin Android explicit runtimeDirectory must validate selected extensions against release-shaped runtime resources" +require_text src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssets.kt "releaseShapedRuntimePackageForDirectory" \ + "Kotlin Android explicit runtimeDirectory validation must infer only oliphaunt/runtime/files resource trees" +require_text src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssets.kt "requireExtensionInstallFiles(runtimePackage, requestedExtensions, runtimeRoot)" \ + "Kotlin Android packaged runtime materialization must validate selected extension control and SQL files after copy" +require_text src/sdks/kotlin/oliphaunt/src/androidUnitTest/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssetsTest.kt "rejectsExplicitRuntimeDirectoryWithoutReleaseShapedProofForExtensions" \ + "Kotlin Android tests must reject explicit runtimeDirectory extensions without release-shaped proof" +require_text src/sdks/kotlin/oliphaunt/src/androidUnitTest/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssetsTest.kt "rejectsExplicitRuntimeDirectoryWithMissingExtensionInstallFiles" \ + "Kotlin Android tests must reject explicit runtimeDirectory extension manifests missing install files" require_text src/sdks/react-native/android/build.gradle "schema=oliphaunt-runtime-resources-v1" \ "React Native Android Gradle packaging must emit the shared runtime-resource schema for the Kotlin SDK" require_text src/sdks/react-native/android/build.gradle "validateRuntimeResourcesSchema" \ @@ -90,6 +100,8 @@ require_text src/sdks/react-native/android/src/main/java/dev/oliphaunt/reactnati "React Native Android open must forward resourceRoot to the Kotlin Android runtime resolver" require_text src/sdks/react-native/android/src/main/java/dev/oliphaunt/reactnative/OliphauntModule.kt "resourceRoot.orEmpty()" \ "React Native Android reopen keys must include resourceRoot so different resource sets are not aliased" +require_text src/sdks/react-native/src/__tests__/client.test.ts "extensions: ['hstore', 'unaccent']" \ + "React Native JS tests must forward selected extensions together with explicit native runtime/resource overrides" require_text src/sdks/react-native/android/build.gradle "nativeModuleStems=" \ "React Native Android Gradle packaging must emit expected native module stems" require_text src/sdks/react-native/android/build.gradle "generatedExtensionMetadata.from(file(\"../src/generated/extensions.json\"))" \ @@ -170,6 +182,18 @@ require_text src/sdks/swift/Sources/Oliphaunt/OliphauntRuntimeResources.swift "a "Swift resource parser must validate exact extension availability" require_text src/sdks/swift/Sources/Oliphaunt/OliphauntNativeDirect.swift "sharedPreloadLibraries: resolvedRuntime.sharedPreloadLibraries" \ "Swift native-direct startup must pass packaged shared-preload libraries to liboliphaunt" +require_text src/sdks/swift/Sources/Oliphaunt/OliphauntNativeDirect.swift "resolveExplicitRuntimeDirectory" \ + "Swift native-direct explicit runtimeDirectory must validate selected extensions against release-shaped runtime resources" +require_text src/sdks/swift/Sources/Oliphaunt/OliphauntNativeDirect.swift "release-shaped OliphauntRuntimeResources" \ + "Swift native-direct explicit runtimeDirectory errors must require release-shaped resource proof for selected extensions" +require_text src/sdks/swift/Sources/Oliphaunt/OliphauntRuntimeResources.swift "forRuntimeDirectory runtimeDirectory: URL" \ + "Swift runtime resources must validate explicit runtimeDirectory and return shared-preload metadata from the manifest" +require_text src/sdks/swift/Sources/Oliphaunt/OliphauntRuntimeResources.swift "releaseShapedResources" \ + "Swift runtime resources must infer only oliphaunt/runtime/files resource trees for explicit runtimeDirectory validation" +require_text src/sdks/swift/Tests/OliphauntTests/OliphauntTests.swift "nativeDirectExtensionsRejectUnprovedExplicitRuntimeDirectory" \ + "Swift tests must reject explicit runtimeDirectory extensions without release-shaped proof" +require_text src/sdks/swift/Tests/OliphauntTests/OliphauntTests.swift "runtimeResourcesValidateExplicitRuntimeDirectory" \ + "Swift tests must validate explicit runtimeDirectory extension files and shared-preload metadata" require_text src/sdks/swift/Sources/COliphaunt/bridge.c "liboliphaunt_selected_static_extensions" \ "Swift native bridge must register generated static extension rows before open" require_text src/sdks/rust/src/runtime_resources.rs "oliphaunt-static-registry-v1" \ diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 2c8b11c8..0dbeb41d 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -625,6 +625,36 @@ def validate_swift(swift_version: str, liboliphaunt_version: str) -> None: "@Test\nfunc runtimeResourcesRejectUnsupportedPackageKindLayout() throws", "Swift runtime-resource layout rejection must be an executable test, not an unannotated helper", ) + require_text( + "src/sdks/swift/Sources/Oliphaunt/OliphauntNativeDirect.swift", + "resolveExplicitRuntimeDirectory", + "Swift native-direct explicit runtimeDirectory must validate selected extensions against release-shaped runtime resources", + ) + require_text( + "src/sdks/swift/Sources/Oliphaunt/OliphauntNativeDirect.swift", + "release-shaped OliphauntRuntimeResources", + "Swift native-direct explicit runtimeDirectory errors must require release-shaped resource proof for selected extensions", + ) + require_text( + "src/sdks/swift/Sources/Oliphaunt/OliphauntRuntimeResources.swift", + "forRuntimeDirectory runtimeDirectory: URL", + "Swift runtime resources must validate explicit runtimeDirectory and return shared-preload metadata from the manifest", + ) + require_text( + "src/sdks/swift/Sources/Oliphaunt/OliphauntRuntimeResources.swift", + "releaseShapedResources", + "Swift runtime resources must infer only oliphaunt/runtime/files resource trees for explicit runtimeDirectory validation", + ) + require_text( + "src/sdks/swift/Tests/OliphauntTests/OliphauntTests.swift", + "nativeDirectExtensionsRejectUnprovedExplicitRuntimeDirectory", + "Swift tests must reject explicit runtimeDirectory extensions without release-shaped proof", + ) + require_text( + "src/sdks/swift/Tests/OliphauntTests/OliphauntTests.swift", + "runtimeResourcesValidateExplicitRuntimeDirectory", + "Swift tests must validate explicit runtimeDirectory extension files and shared-preload metadata", + ) swift_readme = read_text("src/sdks/swift/README.md") allowed_extension_api_symbols = { "OliphauntExtensionArtifactResolution", @@ -698,6 +728,31 @@ def validate_kotlin(kotlin_version: str, liboliphaunt_version: str) -> None: "resourceRoot = resourceRoot", "Kotlin Android native-direct engine must pass explicit resourceRoot into runtime resolution", ) + require_text( + "src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssets.kt", + "validateExplicitRuntimeDirectory", + "Kotlin Android explicit runtimeDirectory must validate selected extensions against release-shaped runtime resources", + ) + require_text( + "src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssets.kt", + "releaseShapedRuntimePackageForDirectory", + "Kotlin Android explicit runtimeDirectory validation must infer only oliphaunt/runtime/files resource trees", + ) + require_text( + "src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssets.kt", + "requireExtensionInstallFiles(runtimePackage, requestedExtensions, runtimeRoot)", + "Kotlin Android packaged runtime materialization must validate selected extension control and SQL files after copy", + ) + require_text( + "src/sdks/kotlin/oliphaunt/src/androidUnitTest/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssetsTest.kt", + "rejectsExplicitRuntimeDirectoryWithoutReleaseShapedProofForExtensions", + "Kotlin Android tests must reject explicit runtimeDirectory extensions without release-shaped proof", + ) + require_text( + "src/sdks/kotlin/oliphaunt/src/androidUnitTest/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssetsTest.kt", + "rejectsExplicitRuntimeDirectoryWithMissingExtensionInstallFiles", + "Kotlin Android tests must reject explicit runtimeDirectory extension manifests missing install files", + ) require_text( "src/sdks/kotlin/oliphaunt/build.gradle.kts", "fun oliphauntProperty(name: String)", @@ -833,6 +888,11 @@ def validate_react_native(rn_version: str, swift_version: str, kotlin_version: s "resourceRoot.orEmpty()", "React Native Android reopen keys must include resourceRoot", ) + require_text( + "src/sdks/react-native/src/__tests__/client.test.ts", + "extensions: ['hstore', 'unaccent']", + "React Native JS tests must forward selected extensions together with explicit native runtime/resource overrides", + ) require_text( "src/sdks/react-native/android/build.gradle", "def oliphauntProperty = { String name ->", From 3b1bf3847b2cd07f9576c212d14ef5a5a5670bd6 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 16:40:00 +0000 Subject: [PATCH 099/137] docs: record split tools validation --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index a05b1fa4..c1e188dd 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -36,7 +36,7 @@ until the current-state gates here are checked with fresh local evidence. - [x] Use subagent reviews for independent codebase audits: examples/local-registry flows, CI/release package production, and SDK runtime resolution parity. -- [ ] Check CI/release workflows produce exactly the current package surfaces +- [x] Check CI/release workflows produce exactly the current package surfaces declared by release metadata, without duplicated target lists or hidden registry package synthesis. - [x] Derive WASIX runtime/tools Cargo package expectations from the canonical @@ -99,6 +99,15 @@ until the current-state gates here are checked with fresh local evidence. and `postgres`; native `oliphaunt-tools-*` carrying `pg_dump` and `psql`; WASIX root carrying only `initdb` plus runtime/template payloads; and `oliphaunt-wasix-tools` carrying `pg_dump.wasix.wasm` and `psql.wasix.wasm`. +- 2026-06-26: Rechecked the split tools model against current local-registry + artifacts. Native `liboliphaunt-0.1.0-linux-x64-gnu.tar.gz` contains + `runtime/bin/initdb`, `runtime/bin/pg_ctl`, and `runtime/bin/postgres`; + native `oliphaunt-tools-0.1.0-linux-x64-gnu.tar.gz` contains only + `runtime/bin/pg_dump` and `runtime/bin/psql`; `liboliphaunt-wasix-portable` + contains `payload/bin/initdb.wasix.wasm` and no split tools; and + `oliphaunt-wasix-tools` contains `payload/bin/pg_dump.wasix.wasm` and + `payload/bin/psql.wasix.wasm`, with no `pg_ctl`. A sweep of 286 local + registry crate files found every crate at or below the 10 MiB limit. - 2026-06-26: Mobile explicit runtime-directory validation now requires release-shaped `oliphaunt/runtime/files` proof before selected extensions are accepted on Kotlin Android and Swift native-direct; React Native forwards the @@ -114,6 +123,13 @@ until the current-state gates here are checked with fresh local evidence. `ANDROID_HOME=$PWD/target/android-sdk ANDROID_SDK_ROOT=$PWD/target/android-sdk bash src/sdks/kotlin/tools/check-sdk.sh check-static`. `bash src/sdks/swift/tools/check-sdk.sh test-unit` remains unrun because this Linux host does not have `swift` installed. +- 2026-06-26: Current CI/release package-surface gates passed: + `tools/release/release.py check`, `python3 tools/release/check_artifact_targets.py`, + and explicit publish-target/workflow audits over `release.toml`, + `release.py publish_step_target_coverage`, and `.github/workflows/release.yml`. + The release check covered release policy, release-please config, artifact + targets, derived release PR sync, release metadata, and ready consumer-shape + gates across all products. - 2026-06-26: Web research confirmed `nektos/act` remains the primary local GitHub Actions runner; use it selectively for Linux workflow smoke because complex hosted-runner parity is limited. Pair it with static workflow checks From bf63ee5de44874ca894439ddd8888ebce13095bf Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 17:08:32 +0000 Subject: [PATCH 100/137] fix: derive sdk artifact handoff from release metadata --- .github/workflows/release.yml | 16 ++------ .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 16 ++++++++ docs/maintainers/sdk-api-surface.md | 1 + docs/maintainers/sdk-parity-policy.md | 31 +++++++++++++-- tools/policy/check-release-policy.py | 16 ++++++-- tools/policy/check-sdk-parity.sh | 38 ++++++++++++++++++- tools/policy/sdk-manifest.toml | 17 +++++++++ tools/release/check_artifact_targets.py | 18 +++++++++ tools/release/check_staged_artifacts.py | 10 +---- tools/release/release.py | 27 ++++++++++++- 10 files changed, 161 insertions(+), 29 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4379f492..51576768 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -356,12 +356,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} - PRODUCT_OLIPHAUNT_RUST: ${{ steps.release_plan.outputs.product_oliphaunt_rust }} - PRODUCT_OLIPHAUNT_SWIFT: ${{ steps.release_plan.outputs.product_oliphaunt_swift }} - PRODUCT_OLIPHAUNT_KOTLIN: ${{ steps.release_plan.outputs.product_oliphaunt_kotlin }} - PRODUCT_OLIPHAUNT_REACT_NATIVE: ${{ steps.release_plan.outputs.product_oliphaunt_react_native }} - PRODUCT_OLIPHAUNT_JS: ${{ steps.release_plan.outputs.product_oliphaunt_js }} - PRODUCT_OLIPHAUNT_WASIX_RUST: ${{ steps.release_plan.outputs.product_oliphaunt_wasix_rust }} + PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} CI_RUN_ID: ${{ steps.ci_build_gate.outputs.run_id }} run: | download_sdk_artifact() { @@ -378,12 +373,9 @@ jobs: --job Builds \ "${artifact_args[@]}" } - [ "$PRODUCT_OLIPHAUNT_RUST" != "true" ] || download_sdk_artifact oliphaunt-rust - [ "$PRODUCT_OLIPHAUNT_SWIFT" != "true" ] || download_sdk_artifact oliphaunt-swift - [ "$PRODUCT_OLIPHAUNT_KOTLIN" != "true" ] || download_sdk_artifact oliphaunt-kotlin - [ "$PRODUCT_OLIPHAUNT_REACT_NATIVE" != "true" ] || download_sdk_artifact oliphaunt-react-native - [ "$PRODUCT_OLIPHAUNT_JS" != "true" ] || download_sdk_artifact oliphaunt-js - [ "$PRODUCT_OLIPHAUNT_WASIX_RUST" != "true" ] || download_sdk_artifact oliphaunt-wasix-rust + while IFS= read -r product; do + download_sdk_artifact "$product" + done < <(tools/release/release.py ci-products --family sdk-package --products-json "$PRODUCTS_JSON") - name: Download liboliphaunt release assets if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && steps.release_plan.outputs.product_liboliphaunt_native == 'true' }} diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index c1e188dd..add567f7 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -130,6 +130,22 @@ until the current-state gates here are checked with fresh local evidence. The release check covered release policy, release-please config, artifact targets, derived release PR sync, release metadata, and ready consumer-shape gates across all products. +- 2026-06-26: Release SDK artifact downloads now derive selected SDK products + from release metadata via `tools/release/release.py ci-products --family + sdk-package --products-json "$PRODUCTS_JSON"` instead of hard-coded + per-SDK workflow booleans. `tools/release/check_staged_artifacts.py` also + derives SDK products from `artifact_targets.sdk_package_products()`. Fresh + checks passed: direct `ci-products` smoke, `python3 + tools/release/check_artifact_targets.py`, `python3 + tools/release/check_staged_artifacts.py --inspect-present`, `python3 + tools/policy/check-release-policy.py`, and `tools/release/release.py check`. +- 2026-06-26: SDK parity guard passed after regenerating + `docs/maintainers/sdk-api-surface.md` for React Native + `PackageSizeReport.runtimeFeatures` and adding WASIX Rust to the + machine-checked SDK parity registry/docs matrix. `bash + tools/policy/check-sdk-parity.sh` now asserts WASIX Rust manifest fields, + Cargo artifact/runtime/tool/extension resolution, the `tools` feature split, + and the intentional absence of `pg_ctl`. - 2026-06-26: Web research confirmed `nektos/act` remains the primary local GitHub Actions runner; use it selectively for Linux workflow smoke because complex hosted-runner parity is limited. Pair it with static workflow checks diff --git a/docs/maintainers/sdk-api-surface.md b/docs/maintainers/sdk-api-surface.md index 2ebde324..6862bda3 100644 --- a/docs/maintainers/sdk-api-surface.md +++ b/docs/maintainers/sdk-api-surface.md @@ -617,6 +617,7 @@ node tools/policy/generate-sdk-api-surface.mjs --write - `PackageSizeReport.nativeModuleStems` - `PackageSizeReport.packageBytes` - `PackageSizeReport.runtimeBytes` +- `PackageSizeReport.runtimeFeatures` - `PackageSizeReport.selectedExtensionBytes` - `PackageSizeReport.staticRegistryBytes` - `PackageSizeReport.templatePgdataBytes` diff --git a/docs/maintainers/sdk-parity-policy.md b/docs/maintainers/sdk-parity-policy.md index 4d6a3d39..edc20934 100644 --- a/docs/maintainers/sdk-parity-policy.md +++ b/docs/maintainers/sdk-parity-policy.md @@ -8,6 +8,7 @@ - React Native: TypeScript/TurboModule SDK over Swift and Kotlin. - TypeScript: desktop JavaScript SDK for Node.js, Bun, Deno, and Tauri JavaScript apps. +- WASIX Rust: Rust SDK for the WASIX/WASM runtime product. The machine-checked SDK registry is `tools/policy/sdk-manifest.toml`. It is the compact source @@ -18,7 +19,9 @@ the parity check guards the registry and the docs together. The generated public surface inventory is [`sdk-api-surface.md`](sdk-api-surface.md). It is intentionally no-build so normal iteration stays fast, but it still makes public Rust, Swift, Kotlin, -React Native, and TypeScript symbol drift visible in review. +React Native, and TypeScript symbol drift visible in review. WASIX Rust is +tracked through its product test/release gates because its runtime surface is +generated from WASIX asset crates rather than the native C ABI wrappers. Shared semantics use product-native tests fed by shared fixture corpora, not a fake universal harness. `src/shared/fixtures/protocol/query-response-cases.json` is the @@ -34,8 +37,10 @@ sandbox. The common product concepts are defined by `liboliphaunt`, the shared fixture contracts, the public parity matrix, and the release metadata. Rust, Swift, -Kotlin, TypeScript, React Native, and WASM are peer products with ecosystem -contracts. Any deviation needs an explicit reason, not silent drift. +Kotlin, TypeScript, React Native, and WASIX Rust are peer products with +ecosystem contracts. WASIX Rust is the parallel WASIX runtime SDK, with its own +asset and AOT artifact contract. Any deviation needs an explicit reason, not +silent drift. ## SDK Taxonomy @@ -51,6 +56,9 @@ SDK ownership is product ownership, not just source layout: - TypeScript owns desktop JavaScript runtime behavior for Node.js, Bun, Deno, and Tauri JavaScript apps. Its broker mode consumes the published `oliphaunt-broker` runtime and the shared `PGOB` protocol. +- WASIX Rust owns the Rust API over the WASIX/WASM runtime. It is not a native + liboliphaunt mode, and its split tools, AOT artifacts, and extension assets + resolve through Cargo artifact crates. The SDKs are peers over the same `liboliphaunt` C ABI and runtime-resource model. React Native is not a fifth runtime. Its native modules are adapters over the @@ -73,6 +81,7 @@ those overrides are not the consumer install path. | SDK | Runtime/library artifacts | Standalone tools | Extension artifacts | Explicit local override | | --- | --- | --- | --- | --- | | Rust | Cargo-resolved `liboliphaunt-native-*` artifact crates staged by `oliphaunt-build` | split `oliphaunt-tools-*` Cargo artifact crates copied into the runtime cache | exact `oliphaunt-extension-*` Cargo artifact crates | `OLIPHAUNT_RESOURCES_DIR` | +| WASIX Rust | Cargo-resolved `liboliphaunt-wasix-portable`, `oliphaunt-icu`, and target AOT artifact crates | optional `oliphaunt-wasix-tools` plus target tools-AOT artifact crates behind the `tools` feature | exact `oliphaunt-extension-*-wasix` and extension AOT Cargo artifact crates selected by feature | `OLIPHAUNT_WASM_GENERATED_ASSETS_DIR` | | TypeScript | npm optional platform packages such as `@oliphaunt/liboliphaunt-*` and `@oliphaunt/node-direct-*` | split `@oliphaunt/tools-*` npm packages | Node/Bun exact extension npm packages; Deno requires an explicit prepared `runtimeDirectory` for extension materialization | `libraryPath` and `runtimeDirectory` | | Swift | SwiftPM release assets and packaged runtime resources | not exposed in mobile native-direct mode | exact extension XCFramework artifacts selected by SQL extension name | `runtimeDirectory` or `resourceRoot` | | Kotlin | Maven runtime artifacts applied through the Android Gradle plugin | not exposed in Android native-direct mode | exact extension Maven artifacts selected by SQL extension name | `runtimeDirectory` or `resourceRoot` | @@ -160,11 +169,27 @@ table above: packages. Deno requires an explicit prepared `runtimeDirectory` for extension materialization. +### WASIX Rust Deltas + +`oliphaunt-wasix` is the Rust SDK for the WASIX runtime product. It does not +share the native liboliphaunt process model; its runtime, ICU data, root AOT, +split tools, tools-AOT, and extension artifacts are all Cargo-resolved WASIX +artifact crates. `pg_dump` and `psql` are available only when the `tools` +feature selects `oliphaunt-wasix-tools` and the matching tools-AOT crate for +the host target. `pg_ctl` is intentionally absent because there is no external +WASIX postmaster lifecycle to control. + +Release checks, consumer-shape checks, and the WASIX Rust product +`release-check` own the semantic proof for this lane: the split tools preflight +must load both `pg_dump` and `psql` artifacts before tool APIs run, and AOT +manifests must reject missing, duplicate, or non-tool entries. + ## Current Platform Stance | SDK | Primary app target | Runtime owner | Current native mode | Non-parity that is allowed today | | --- | --- | --- | --- | --- | | Rust | Tauri and Rust desktop apps | `oliphaunt` | direct, broker, server | none for the core SDK contract | +| WASIX Rust | WASIX/WASM runtime apps | `oliphaunt-wasix` | not native; WASIX direct/server APIs | native direct/broker/server modes do not apply; split WASIX tools require the explicit `tools` feature | | Swift | iOS and macOS apps | `Oliphaunt` | direct | broker/server are explicit unsupported errors until platform runtimes exist; they must not be faked through direct mode | | Kotlin | Android apps | `oliphaunt` | Android direct plus Kotlin/Native direct | Android common defaults require the `OliphauntAndroid` Context facade; JVM runtime is explicitly unavailable; Android broker/server must be separate platform adapters, not direct-mode aliases | | React Native | React Native apps | Swift on Apple, Kotlin on Android | delegated direct | New Architecture JSI ArrayBuffer transport is required for protocol, backup, and restore bytes | diff --git a/tools/policy/check-release-policy.py b/tools/policy/check-release-policy.py index cbdfe19d..fd8411ce 100644 --- a/tools/policy/check-release-policy.py +++ b/tools/policy/check-release-policy.py @@ -722,6 +722,8 @@ def check_release_workflow_policy() -> None: "--artifact oliphaunt-extension-package-artifacts", "--artifact liboliphaunt-native-release-assets", "--artifact \"$artifact\"", + "PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }}", + "tools/release/release.py ci-products --family sdk-package --products-json \"$PRODUCTS_JSON\"", "tools/release/release.py ci-artifacts --product \"$product\" --family sdk-package", "tools/release/release.py ci-artifacts --product \"$product\" --kind \"$kind\" --family release-assets", "tools/release/release.py ci-artifacts --product oliphaunt-node-direct --kind node-direct-addon --family npm-package", @@ -733,10 +735,16 @@ def check_release_workflow_policy() -> None: ): if snippet not in publish_block: fail(f"Release workflow dry-run handoff is missing {snippet!r}") - for product in artifact_targets.sdk_package_products(): - snippet = f"download_sdk_artifact {product}" - if snippet not in publish_block: - fail(f"Release workflow dry-run handoff is missing {snippet!r}") + for legacy_env in ( + "PRODUCT_OLIPHAUNT_RUST", + "PRODUCT_OLIPHAUNT_SWIFT", + "PRODUCT_OLIPHAUNT_KOTLIN", + "PRODUCT_OLIPHAUNT_REACT_NATIVE", + "PRODUCT_OLIPHAUNT_JS", + "PRODUCT_OLIPHAUNT_WASIX_RUST", + ): + if legacy_env in publish_block: + fail(f"Release workflow must not hard-code SDK product selection with {legacy_env}") if "target/release-assets/native" in publish_block: fail("Release workflow must download native helper artifacts into product-owned release asset roots") diff --git a/tools/policy/check-sdk-parity.sh b/tools/policy/check-sdk-parity.sh index 5899d97f..8896b15f 100755 --- a/tools/policy/check-sdk-parity.sh +++ b/tools/policy/check-sdk-parity.sh @@ -128,6 +128,30 @@ require_text src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs "WASIX SDK must reject non-tool artifacts from split tools AOT manifests" require_text src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/aot.rs "tools AOT manifest is missing required artifact" \ "WASIX SDK must reject split tools AOT manifests that omit pg_dump or psql" +require_manifest_text wasix-rust 'classification = "sdk"' \ + "SDK manifest must classify WASIX Rust as a product SDK" +require_manifest_text wasix-rust 'package_name = "oliphaunt-wasix"' \ + "SDK manifest must name the WASIX Rust registry package" +require_manifest_text wasix-rust 'implementation_path = "src/bindings/wasix-rust/crates/oliphaunt-wasix"' \ + "SDK manifest must point WASIX Rust ownership at the WASIX binding crate" +require_manifest_text wasix-rust 'primary_targets = ["wasix", "wasm"]' \ + "SDK manifest must classify WASIX Rust as the WASIX/WASM SDK" +require_manifest_text wasix-rust 'runtime_boundary = "oliphaunt-wasix"' \ + "SDK manifest must classify the WASIX Rust runtime boundary" +require_manifest_text wasix-rust 'parity_role = "wasm-peer"' \ + "SDK manifest must classify WASIX Rust as a WASM peer SDK" +require_manifest_text wasix-rust 'available_modes = ["wasix-direct", "wasix-server"]' \ + "SDK manifest must declare WASIX Rust mode availability" +require_manifest_text wasix-rust 'unsupported_modes = ["native-direct", "native-broker", "native-server"]' \ + "SDK manifest must declare native liboliphaunt modes as unsupported for WASIX Rust" +require_manifest_text wasix-rust 'artifact_resolution = "liboliphaunt-wasix-cargo-artifact-crates"' \ + "SDK manifest must declare WASIX Rust runtime artifact resolution" +require_manifest_text wasix-rust 'tool_resolution = "optional-oliphaunt-wasix-tools-cargo-crates"' \ + "SDK manifest must declare WASIX Rust split tools resolution" +require_manifest_text wasix-rust 'extension_resolution = "exact-extension-wasix-cargo-crates"' \ + "SDK manifest must declare WASIX Rust exact-extension Cargo resolution" +require_manifest_text wasix-rust 'resource_override = "OLIPHAUNT_WASM_GENERATED_ASSETS_DIR"' \ + "SDK manifest must declare WASIX Rust generated-asset override" require_manifest_text swift 'classification = "sdk"' \ "SDK manifest must classify Swift as a product SDK" require_manifest_text swift 'primary_targets = ["ios", "macos"]' \ @@ -316,8 +340,10 @@ require_text docs/maintainers/sdk-parity-policy.md '`tools/policy/sdk-manifest.t "SDK parity docs must link the machine-checked SDK registry" require_text docs/maintainers/sdk-parity-policy.md '[`sdk-api-surface.md`](sdk-api-surface.md)' \ "SDK parity docs must link the generated SDK API surface inventory" -require_text docs/maintainers/sdk-parity-policy.md "WASM are peer products with ecosystem" \ +require_text docs/maintainers/sdk-parity-policy.md "WASIX Rust are peer products with" \ "SDK parity docs must classify SDKs as peer products" +require_text docs/maintainers/sdk-parity-policy.md "WASIX Rust: Rust SDK for the WASIX/WASM runtime product." \ + "SDK parity docs must define WASIX Rust ownership" require_text docs/maintainers/sdk-parity-policy.md 'src/shared/fixtures/protocol/query-response-cases.json' \ "SDK parity docs must document the shared protocol fixture corpus" require_text docs/maintainers/sdk-parity-policy.md "React Native is not a fifth runtime." \ @@ -330,6 +356,12 @@ require_text docs/maintainers/sdk-parity-policy.md "split \`oliphaunt-tools-*\` "SDK parity docs must describe Rust split tools Cargo artifact resolution" require_text docs/maintainers/sdk-parity-policy.md "\`OLIPHAUNT_RESOURCES_DIR\`" \ "SDK parity docs must document Rust's explicit local runtime-resource override" +require_text docs/maintainers/sdk-parity-policy.md "Cargo-resolved \`liboliphaunt-wasix-portable\`, \`oliphaunt-icu\`, and target AOT artifact crates" \ + "SDK parity docs must describe WASIX Rust runtime artifact resolution" +require_text docs/maintainers/sdk-parity-policy.md "optional \`oliphaunt-wasix-tools\` plus target tools-AOT artifact crates behind the \`tools\` feature" \ + "SDK parity docs must describe WASIX Rust split tools Cargo artifact resolution" +require_text docs/maintainers/sdk-parity-policy.md "\`OLIPHAUNT_WASM_GENERATED_ASSETS_DIR\`" \ + "SDK parity docs must document WASIX Rust's generated-asset override" require_text docs/maintainers/sdk-parity-policy.md "split \`@oliphaunt/tools-*\` npm packages" \ "SDK parity docs must describe TypeScript split tools npm resolution" require_text docs/maintainers/sdk-parity-policy.md "\`libraryPath\` and \`runtimeDirectory\`" \ @@ -340,8 +372,12 @@ require_text docs/maintainers/sdk-parity-policy.md "\`runtimeDirectory\` or \`re "SDK parity docs must document mobile SDK explicit local runtime-resource overrides" require_text docs/maintainers/sdk-parity-policy.md "### Desktop TypeScript Deltas" \ "SDK parity docs must describe desktop TypeScript deltas explicitly" +require_text docs/maintainers/sdk-parity-policy.md "### WASIX Rust Deltas" \ + "SDK parity docs must describe WASIX Rust deltas explicitly" require_text docs/maintainers/sdk-parity-policy.md "The default open profile is \`runtimeFootprint: 'throughput'\` with" \ "SDK parity docs must document the desktop TypeScript default profile" +require_text docs/maintainers/sdk-parity-policy.md "\`pg_ctl\` is intentionally absent because there is no external" \ + "SDK parity docs must document why WASIX Rust has no pg_ctl" require_text docs/maintainers/sdk-parity-policy.md "Node.js direct mode resolves the prebuilt \`@oliphaunt/node-direct-*\`" \ "SDK parity docs must document Node direct optional adapter resolution" require_text docs/maintainers/sdk-parity-policy.md "not exposed in Android native-direct mode" \ diff --git a/tools/policy/sdk-manifest.toml b/tools/policy/sdk-manifest.toml index cbb018d5..8878e0e0 100644 --- a/tools/policy/sdk-manifest.toml +++ b/tools/policy/sdk-manifest.toml @@ -23,6 +23,23 @@ tool_resolution = "split-oliphaunt-tools-cargo-crates" extension_resolution = "exact-extension-cargo-crates" resource_override = "OLIPHAUNT_RESOURCES_DIR" +[sdks.wasix-rust] +classification = "sdk" +package_name = "oliphaunt-wasix" +implementation_path = "src/bindings/wasix-rust/crates/oliphaunt-wasix" +documentation_path = "src/docs/content/sdk/wasm" +primary_targets = ["wasix", "wasm"] +runtime_owner = true +runtime_boundary = "oliphaunt-wasix" +parity_role = "wasm-peer" +available_modes = ["wasix-direct", "wasix-server"] +unsupported_modes = ["native-direct", "native-broker", "native-server"] +unsupported_mode_reason = "WASIX embeds PostgreSQL as WebAssembly modules; native liboliphaunt process modes do not apply" +artifact_resolution = "liboliphaunt-wasix-cargo-artifact-crates" +tool_resolution = "optional-oliphaunt-wasix-tools-cargo-crates" +extension_resolution = "exact-extension-wasix-cargo-crates" +resource_override = "OLIPHAUNT_WASM_GENERATED_ASSETS_DIR" + [sdks.swift] classification = "sdk" package_name = "Oliphaunt" diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py index ba8ebdf7..f8138505 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -412,6 +412,24 @@ def validate_ci_release_artifacts() -> None: 'tools/release/release.py ci-artifacts --product "$product" --family sdk-package', "release workflow must derive SDK package artifact names from release metadata", ) + require_text( + ".github/workflows/release.yml", + 'tools/release/release.py ci-products --family sdk-package --products-json "$PRODUCTS_JSON"', + "release workflow must derive selected SDK package products from release metadata", + ) + for legacy_env in ( + "PRODUCT_OLIPHAUNT_RUST", + "PRODUCT_OLIPHAUNT_SWIFT", + "PRODUCT_OLIPHAUNT_KOTLIN", + "PRODUCT_OLIPHAUNT_REACT_NATIVE", + "PRODUCT_OLIPHAUNT_JS", + "PRODUCT_OLIPHAUNT_WASIX_RUST", + ): + reject_text( + ".github/workflows/release.yml", + legacy_env, + f"release workflow must not hard-code SDK product selection with {legacy_env}", + ) require_text( "src/runtimes/broker/moon.yml", 'tags: ["release", "artifact", "ci-broker-runtime"]', diff --git a/tools/release/check_staged_artifacts.py b/tools/release/check_staged_artifacts.py index 26df50e9..b5b9ee49 100755 --- a/tools/release/check_staged_artifacts.py +++ b/tools/release/check_staged_artifacts.py @@ -25,6 +25,7 @@ from pathlib import Path from typing import NoReturn +import artifact_targets import extension_artifact_targets import package_liboliphaunt_wasix_cargo_artifacts import product_metadata @@ -35,14 +36,7 @@ EXTENSION_ROOT = ROOT / "target" / "extension-artifacts" MOBILE_ROOT = ROOT / "target" / "mobile-build" / "react-native" -SDK_PRODUCTS = { - "oliphaunt-rust", - "oliphaunt-swift", - "oliphaunt-kotlin", - "oliphaunt-js", - "oliphaunt-react-native", - "oliphaunt-wasix-rust", -} +SDK_PRODUCTS = frozenset(artifact_targets.sdk_package_products()) SDK_RUNTIME_PAYLOAD_PATTERNS = [ re.compile(pattern) diff --git a/tools/release/release.py b/tools/release/release.py index 2611c0c7..96650a67 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -1729,6 +1729,21 @@ def command_ci_artifacts(args: list[str]) -> None: print(name) +def command_ci_products(args: list[str]) -> None: + parser = argparse.ArgumentParser(description="Emit selected CI products derived from release metadata.") + parser.add_argument("--family", choices=["sdk-package"], required=True) + parser.add_argument("--products-json") + parsed = parser.parse_args(args) + sdk_products = set(artifact_targets.sdk_package_products()) + if parsed.products_json is None: + products = list(artifact_targets.sdk_package_products()) + else: + products = selected_products_from_passthrough(["--products-json", parsed.products_json]) + for product in products: + if product in sdk_products: + print(product) + + def consumer_shape_scope_args(args: list[str]) -> list[str]: scoped: list[str] = [] index = 0 @@ -3190,7 +3205,15 @@ def main(argv: list[str]) -> int: parser = argparse.ArgumentParser(description=__doc__) subparsers = parser.add_subparsers(dest="command", required=True) - for name in ["plan", "check", "check-registries", "consumer-shape", "ci-artifacts", "verify-release"]: + for name in [ + "plan", + "check", + "check-registries", + "consumer-shape", + "ci-artifacts", + "ci-products", + "verify-release", + ]: subparsers.add_parser(name, add_help=False) dry_run = subparsers.add_parser("publish-dry-run") @@ -3217,6 +3240,8 @@ def main(argv: list[str]) -> int: command_consumer_shape(passthrough) elif command == "ci-artifacts": command_ci_artifacts(passthrough) + elif command == "ci-products": + command_ci_products(passthrough) elif command == "verify-release": command_verify_release(passthrough) elif command == "publish-dry-run": From 88cffc74d53b4d5c9faf71b3c50b547373b4e1ab Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 17:22:40 +0000 Subject: [PATCH 101/137] fix: tighten wasix tools artifact guards --- tools/release/check_consumer_shape.py | 25 +++++++++++++++---------- tools/release/check_release_metadata.py | 2 +- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 21504d1a..c8aac7d5 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -1848,20 +1848,25 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: findings, product, "wasix-portable-runtime-tool-contract", - "oliphaunt/bin/initdb" in release_source - and "oliphaunt/bin/postgres" in release_source - and "oliphaunt/bin/pg_ctl" in release_source - and "oliphaunt/bin/pg_dump" in release_source - and "oliphaunt/bin/psql" in release_source + package_liboliphaunt_wasix_cargo_artifacts.CORE_RUNTIME_ARCHIVE_FILES + == ("oliphaunt/bin/initdb", "oliphaunt/bin/postgres") + and package_liboliphaunt_wasix_cargo_artifacts.TOOLS_PAYLOAD_FILES + == ("bin/pg_dump.wasix.wasm", "bin/psql.wasix.wasm") + and package_liboliphaunt_wasix_cargo_artifacts.FORBIDDEN_RUNTIME_ARCHIVE_TOOL_FILES + == ("oliphaunt/bin/pg_ctl", "oliphaunt/bin/pg_dump", "oliphaunt/bin/psql") + and package_liboliphaunt_wasix_cargo_artifacts.TOOLS_AOT_ARTIFACTS + == {"tool:pg_dump", "tool:psql"} + and '"oliphaunt/bin/initdb", "oliphaunt/bin/postgres"' in release_source + and '"oliphaunt/bin/pg_ctl", "oliphaunt/bin/pg_dump", "oliphaunt/bin/psql"' in release_source and "CORE_RUNTIME_ARCHIVE_FILES" in wasix_packager_source and "TOOLS_PAYLOAD_FILES" in wasix_packager_source and "TOOLS_AOT_ARTIFACTS" in wasix_packager_source and "FORBIDDEN_RUNTIME_ARCHIVE_TOOL_FILES" in wasix_packager_source - and "oliphaunt/bin/initdb" in wasix_packager_source - and "oliphaunt/bin/postgres" in wasix_packager_source - and "oliphaunt/bin/pg_ctl" in wasix_packager_source - and "oliphaunt/bin/pg_dump" in wasix_packager_source - and "oliphaunt/bin/psql" in wasix_packager_source, + and '"oliphaunt/bin/initdb",' in wasix_packager_source + and '"oliphaunt/bin/postgres",' in wasix_packager_source + and '"oliphaunt/bin/pg_ctl",' in wasix_packager_source + and '"oliphaunt/bin/pg_dump",' in wasix_packager_source + and '"oliphaunt/bin/psql",' in wasix_packager_source, "Release validation must require postgres/initdb in the WASIX runtime archive, reject pg_ctl/pg_dump/psql there, and publish pg_dump/psql through WASIX tools payload/AOT crates.", [ "tools/release/release.py", diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 0dbeb41d..e3696228 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1410,7 +1410,7 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None or '"pg-dump":null' in asset_build_source or '"psql":null' in asset_build_source ): - fail("WASIX root runtime asset crate must embed initdb only and omit split pg_dump/psql manifest entries") + fail("WASIX root runtime asset crate must carry postgres/initdb runtime assets and omit split pg_dump/psql manifest entries") tools_build_source = read_text("src/runtimes/liboliphaunt/wasix/crates/tools/build.rs") if ( '"bin/pg_dump.wasix.wasm"' not in tools_build_source From 0acb1c7548f56f9c8d37da340a74eadf2de2c45e Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 17:29:43 +0000 Subject: [PATCH 102/137] fix: validate js extensions against catalog --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 15 ++++++++++++ src/sdks/js/src/__tests__/client.test.ts | 4 ++++ src/sdks/js/src/__tests__/config.test.ts | 23 +++++++++++++++++-- src/sdks/js/src/config.ts | 11 +++++++-- src/sdks/js/src/native/assets-node.ts | 5 +++- src/sdks/js/tools/check-sdk.sh | 6 +++++ 6 files changed, 59 insertions(+), 5 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index add567f7..3d181fe5 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -108,6 +108,21 @@ until the current-state gates here are checked with fresh local evidence. `oliphaunt-wasix-tools` contains `payload/bin/pg_dump.wasix.wasm` and `payload/bin/psql.wasix.wasm`, with no `pg_ctl`. A sweep of 286 local registry crate files found every crate at or below the 10 MiB limit. +- 2026-06-26: Tightened the current WASIX split-tools release guards after + commit `88cffc7`; `check_consumer_shape.py` now asserts exact WASIX root + runtime archive, tools payload, forbidden root tool, and tools-AOT payload + constants. Fresh package generation and payload inspection found native + root/tool and WASIX root/tool crates below the 10 MiB crate limit with + `pg_dump` and `psql` only in the split tools packages. +- 2026-06-26: TypeScript extension selection now validates requested extension + IDs against the generated extension catalog before startup argument + construction, and Node/Bun extension package materialization uses only + generated package-materialization dependencies. Fresh checks passed: + `pnpm --dir src/sdks/js test`, `pnpm --dir src/sdks/js typecheck`, + `bash src/sdks/js/tools/check-sdk.sh check-static`, + `python3 tools/release/check_consumer_shape.py`, + `python3 tools/release/check_release_metadata.py`, + `bash tools/policy/check-sdk-parity.sh`, and `git diff --check`. - 2026-06-26: Mobile explicit runtime-directory validation now requires release-shaped `oliphaunt/runtime/files` proof before selected extensions are accepted on Kotlin Android and Swift native-direct; React Native forwards the diff --git a/src/sdks/js/src/__tests__/client.test.ts b/src/sdks/js/src/__tests__/client.test.ts index 790efcc1..ad94856f 100644 --- a/src/sdks/js/src/__tests__/client.test.ts +++ b/src/sdks/js/src/__tests__/client.test.ts @@ -175,6 +175,10 @@ async function testOpenRejectsUnsupportedModesAndInvalidInputs(): Promise async () => client.open({ root: '/tmp/root', extensions: ['bad/value'] }), /extension id/, ); + await assert.rejects( + async () => client.open({ root: '/tmp/root', extensions: ['pg_search'] }), + /unknown Oliphaunt extension id 'pg_search'/, + ); await assert.rejects( async () => client.open({ temporary: false }), /database root is not configured/, diff --git a/src/sdks/js/src/__tests__/config.test.ts b/src/sdks/js/src/__tests__/config.test.ts index 0af1a82e..4fc7f78d 100644 --- a/src/sdks/js/src/__tests__/config.test.ts +++ b/src/sdks/js/src/__tests__/config.test.ts @@ -10,6 +10,7 @@ import { validateBrokerTransport, validateMaxClientSessions, validateOptionalPathOverride, + validateExtensionIds, validateRootPath, validateServerPort, validateStartupGUCs, @@ -145,6 +146,15 @@ test('validates config error surfaces deterministically', () => { () => validateStartupGUCs([{ name: 'ok', value: 'bad\0' }]), /must not contain NUL/, ); + assert.deepEqual(validateExtensionIds([' earthdistance ', '', 'cube']), [ + 'earthdistance', + 'cube', + ]); + throwsMessage(() => validateExtensionIds(['bad/value']), /extension id/); + throwsMessage( + () => validateExtensionIds(['pg_search']), + /unknown Oliphaunt extension id 'pg_search'/, + ); }); test('uses generated extension metadata for startup requirements', () => { @@ -178,12 +188,21 @@ test('uses generated extension metadata for startup requirements', () => { durability: 'safe', runtimeFootprint: 'throughput', startupGUCs: [{ name: 'app.setting', value: 'enabled' }], - extensions: ['hstore', 'pg_search'], + extensions: ['hstore'], }); assert.ok(args.includes('app.setting=enabled')); assert.equal( args.some((value) => value.startsWith('shared_preload_libraries=')), false, - 'candidate-only extensions must not create startup preload rules unless generated metadata marks them public', + 'extensions without generated preload rules must not create startup preload rules', + ); + throwsMessage( + () => + buildStartupArgs({ + durability: 'safe', + runtimeFootprint: 'throughput', + extensions: ['hstore', 'pg_search'], + }), + /unknown Oliphaunt extension id 'pg_search'/, ); }); diff --git a/src/sdks/js/src/config.ts b/src/sdks/js/src/config.ts index cb9f821f..35678882 100644 --- a/src/sdks/js/src/config.ts +++ b/src/sdks/js/src/config.ts @@ -1,6 +1,9 @@ import { join } from 'node:path'; -import { generatedSharedPreloadLibraries } from './generated/extensions.js'; +import { + generatedExtensionBySqlName, + generatedSharedPreloadLibraries, +} from './generated/extensions.js'; import type { BrokerTransport, DurabilityProfile, @@ -106,12 +109,13 @@ export function buildStartupArgs(options: { startupGUCs?: ReadonlyArray; extensions?: ReadonlyArray; }): string[] { + const extensions = validateExtensionIds(options.extensions ?? []); const assignments = [ ...runtimeFootprintAssignments(options.runtimeFootprint), ...durabilityAssignments(options.durability), ...validateStartupGUCs(options.startupGUCs ?? []), ]; - const preloadLibraries = requiredSharedPreloadLibraries(options.extensions ?? []); + const preloadLibraries = requiredSharedPreloadLibraries(extensions); if (preloadLibraries.length > 0) { assignments.push(`shared_preload_libraries=${preloadLibraries.join(',')}`); } @@ -220,6 +224,9 @@ export function validateExtensionIds(extensions: ReadonlyArray): string[ `Oliphaunt extension id '${trimmed}' must contain 1 to 128 ASCII letters, digits, '.', '_' or '-'`, ); } + if (generatedExtensionBySqlName(trimmed) === undefined) { + throw new Error(`unknown Oliphaunt extension id '${trimmed}'`); + } normalized.push(trimmed); } return normalized; diff --git a/src/sdks/js/src/native/assets-node.ts b/src/sdks/js/src/native/assets-node.ts index 02f6ebf5..7726bca2 100644 --- a/src/sdks/js/src/native/assets-node.ts +++ b/src/sdks/js/src/native/assets-node.ts @@ -717,7 +717,10 @@ function selectedExtensionClosure(extensions: ReadonlyArray): string[] { } seen.add(sqlName); const metadata = generatedExtensionBySqlName(sqlName); - for (const dependency of metadata?.selectedExtensionDependencies ?? metadata?.dependencies ?? []) { + if (metadata === undefined) { + throw new Error(`unknown Oliphaunt extension id '${sqlName}'`); + } + for (const dependency of metadata.selectedExtensionDependencies) { queue.push(dependency); } } diff --git a/src/sdks/js/tools/check-sdk.sh b/src/sdks/js/tools/check-sdk.sh index b45f86a4..598a3ce3 100755 --- a/src/sdks/js/tools/check-sdk.sh +++ b/src/sdks/js/tools/check-sdk.sh @@ -418,6 +418,12 @@ require_source_text "$package_dir/src/client.ts" "async checkpoint(): Promise Date: Fri, 26 Jun 2026 17:37:05 +0000 Subject: [PATCH 103/137] fix: validate react native extensions against catalog --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 14 ++++++++++++++ .../react-native/src/__tests__/client.test.ts | 4 ++++ src/sdks/react-native/src/client.ts | 4 ++++ src/sdks/react-native/tools/check-sdk.sh | 17 +++++++++++------ tools/policy/check-sdk-parity.sh | 8 ++++++++ 5 files changed, 41 insertions(+), 6 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 3d181fe5..381cd176 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -123,6 +123,20 @@ until the current-state gates here are checked with fresh local evidence. `python3 tools/release/check_consumer_shape.py`, `python3 tools/release/check_release_metadata.py`, `bash tools/policy/check-sdk-parity.sh`, and `git diff --check`. +- 2026-06-26: React Native JS extension selection now rejects unknown + generated-catalog extension IDs before crossing the TurboModule bridge, + matching the TypeScript preflight behavior while Kotlin and Swift continue to + validate exact mobile runtime resources. The React Native scratch package + check now generates a package-scoped pnpm lockfile instead of copying the + monorepo lockfile, so unpublished local-registry example dependencies do not + break SDK static checks. Fresh checks passed: + `pnpm --dir src/sdks/react-native test`, + `pnpm --dir src/sdks/react-native typecheck`, + `bash src/sdks/react-native/tools/check-sdk.sh check-static`, + `python3 tools/release/check_release_metadata.py`, + `python3 tools/release/check_consumer_shape.py`, + `bash tools/policy/check-sdk-parity.sh`, + `bash tools/policy/check-tooling-stack.sh`, and `git diff --check`. - 2026-06-26: Mobile explicit runtime-directory validation now requires release-shaped `oliphaunt/runtime/files` proof before selected extensions are accepted on Kotlin Android and Swift native-direct; React Native forwards the diff --git a/src/sdks/react-native/src/__tests__/client.test.ts b/src/sdks/react-native/src/__tests__/client.test.ts index 1bd15cd5..034ae044 100644 --- a/src/sdks/react-native/src/__tests__/client.test.ts +++ b/src/sdks/react-native/src/__tests__/client.test.ts @@ -1068,6 +1068,10 @@ async function testOpenValidatesExtensionIdsBeforeNativeCall(): Promise { await client.open({ extensions: ['mobile/vector'] }); }, /extension id 'mobile\/vector' must contain 1 to 128 ASCII/); assert.equal(native.openCalls.length, 0); + await assert.rejects(async () => { + await client.open({ extensions: ['pg_search'] }); + }, /unknown React Native Oliphaunt extension id 'pg_search'/); + assert.equal(native.openCalls.length, 0); await client.open({ extensions: [' pg_trgm ', '', 'vector', 'hstore'], diff --git a/src/sdks/react-native/src/client.ts b/src/sdks/react-native/src/client.ts index 1b8f4dfc..b35af17a 100644 --- a/src/sdks/react-native/src/client.ts +++ b/src/sdks/react-native/src/client.ts @@ -16,6 +16,7 @@ import { type QueryParam, type QueryResult, } from './query'; +import { generatedExtensionBySqlName } from './generated/extensions'; import type { NativeCapabilities, NativeEngineModeSupport, @@ -704,6 +705,9 @@ function validateExtensionIds(extensions: ReadonlyArray): string[] { `React Native Oliphaunt extension id '${trimmed}' must contain 1 to 128 ASCII letters, digits, '.', '_' or '-'`, ); } + if (generatedExtensionBySqlName(trimmed) === undefined) { + throw new Error(`unknown React Native Oliphaunt extension id '${trimmed}'`); + } normalized.push(trimmed); } return normalized; diff --git a/src/sdks/react-native/tools/check-sdk.sh b/src/sdks/react-native/tools/check-sdk.sh index 4f9e62e9..728054c6 100755 --- a/src/sdks/react-native/tools/check-sdk.sh +++ b/src/sdks/react-native/tools/check-sdk.sh @@ -148,7 +148,10 @@ allowBuilds: sharp: true unrs-resolver: true YAML - cp pnpm-lock.yaml "$scratch_root/pnpm-lock.yaml" + # Generate a package-scoped scratch lockfile. The root lockfile includes + # example importers that intentionally resolve unpublished local-registry + # @oliphaunt/* packages and should not be fetched by the SDK package check. + rm -f "$scratch_root/pnpm-lock.yaml" mkdir -p "$scratch_root/fixtures" mkdir -p "$scratch_root/tools/test" rsync -a --delete src/shared/fixtures/ "$scratch_root/fixtures/" @@ -163,11 +166,9 @@ YAML --exclude ios/vendor \ "$source_package_dir/" "$package_dir/" rm -rf "$scratch_root/node_modules" "$package_dir/node_modules" - if [ "${PNPM_CONFIG_LOCKFILE:-}" = "false" ]; then - run pnpm --dir "$scratch_root" install --no-frozen-lockfile - else - run pnpm --dir "$scratch_root" install --frozen-lockfile - fi + # PNPM_CONFIG_LOCKFILE=false remains honored by pnpm for callers that need to + # disable scratch lockfile writes, but the normal path records one. + run pnpm --dir "$scratch_root" install --no-frozen-lockfile --trust-lockfile if [ ! -e "$package_dir/node_modules" ]; then ln -s "$scratch_root/node_modules" "$package_dir/node_modules" fi @@ -321,6 +322,10 @@ require_source_text "$package_dir/android/settings.gradle" "if (configuredKotlin "React Native Android local Kotlin SDK composite builds must be explicit development overrides" require_source_text "$package_dir/tools/expo-android-runner.sh" "kotlin_sdk_dependency_from_maven_repo" \ "React Native Android mobile runner must derive the Kotlin SDK dependency from staged Maven artifacts" +require_source_text "$package_dir/src/client.ts" "generatedExtensionBySqlName(trimmed)" \ + "React Native JS boundary must validate selected extensions against the generated extension catalog before crossing the bridge" +require_source_text "$package_dir/src/client.ts" "unknown React Native Oliphaunt extension id" \ + "React Native JS boundary must fail clearly for unknown selected extensions" if grep -Fq "dev.oliphaunt:oliphaunt-android:0.1.0" "$package_dir/tools/expo-android-runner.sh"; then echo "React Native Android mobile runner must not hardcode the Kotlin SDK version" >&2 exit 1 diff --git a/tools/policy/check-sdk-parity.sh b/tools/policy/check-sdk-parity.sh index 8896b15f..5bebd1de 100755 --- a/tools/policy/check-sdk-parity.sh +++ b/tools/policy/check-sdk-parity.sh @@ -1277,8 +1277,16 @@ require_text src/sdks/react-native/src/index.ts "PostgresError" \ "React Native SDK must re-export structured PostgreSQL errors" require_text src/sdks/react-native/src/client.ts "validateExtensionIds" \ "React Native SDK must validate extension identifiers before crossing the bridge" +require_text src/sdks/react-native/src/client.ts "generatedExtensionBySqlName(trimmed)" \ + "React Native SDK must validate selected extension identifiers against the generated catalog before crossing the bridge" require_text src/sdks/react-native/src/__tests__/client.test.ts "mobile/vector" \ "React Native SDK must test malformed extension identifiers before native open" +require_text src/sdks/react-native/src/__tests__/client.test.ts "pg_search" \ + "React Native SDK must test unknown generated-catalog extension identifiers before native open" +require_text src/sdks/js/src/config.ts "generatedExtensionBySqlName(trimmed)" \ + "TypeScript SDK must validate selected extension identifiers against the generated catalog before runtime startup" +require_text src/sdks/js/src/__tests__/config.test.ts "pg_search" \ + "TypeScript SDK must test unknown generated-catalog extension identifiers before startup" require_text src/sdks/react-native/ios/OliphauntAdapter.swift "extensions must be an array of strings" \ "React Native iOS adapter must reject malformed extension arrays before Swift SDK open" reject_text src/sdks/react-native/ios/OliphauntAdapter.swift 'compactMap { $0 as? String }' \ From 91e2d41219b0471dd71f14528e4740ac8a8b3032 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 17:42:12 +0000 Subject: [PATCH 104/137] chore: move react native extension path lookup to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 14 ++ .../tools/mobile-extension-artifact-paths.mjs | 127 ++++++++++++++++++ .../tools/mobile-extension-runtime.sh | 75 +---------- tools/policy/check-tooling-stack.sh | 6 + 4 files changed, 154 insertions(+), 68 deletions(-) create mode 100644 src/sdks/react-native/tools/mobile-extension-artifact-paths.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 381cd176..b1e73741 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -137,6 +137,20 @@ until the current-state gates here are checked with fresh local evidence. `python3 tools/release/check_consumer_shape.py`, `bash tools/policy/check-sdk-parity.sh`, `bash tools/policy/check-tooling-stack.sh`, and `git diff --check`. +- 2026-06-26: React Native mobile exact-extension artifact path resolution now + uses `src/sdks/react-native/tools/mobile-extension-artifact-paths.mjs` + through the pinned Bun launcher instead of an inline Python heredoc in + `mobile-extension-runtime.sh`. A fixture check covered the matching runtime + asset path and optional-missing exit code, and fresh checks passed: + `bash -n src/sdks/react-native/tools/mobile-extension-runtime.sh + src/sdks/react-native/tools/expo-android-runner.sh + src/sdks/react-native/tools/expo-ios-runner.sh`, + `bash tools/policy/check-tooling-stack.sh`, + `bash tools/policy/check-sdk-mobile-extension-surface.sh`, + `bun tools/policy/check-test-strategy.mjs`, + `bash src/sdks/react-native/tools/check-sdk.sh check-static`, + `python3 tools/release/check_release_metadata.py`, + `python3 tools/release/check_consumer_shape.py`, and `git diff --check`. - 2026-06-26: Mobile explicit runtime-directory validation now requires release-shaped `oliphaunt/runtime/files` proof before selected extensions are accepted on Kotlin Android and Swift native-direct; React Native forwards the diff --git a/src/sdks/react-native/tools/mobile-extension-artifact-paths.mjs b/src/sdks/react-native/tools/mobile-extension-artifact-paths.mjs new file mode 100644 index 00000000..73533eda --- /dev/null +++ b/src/sdks/react-native/tools/mobile-extension-artifact-paths.mjs @@ -0,0 +1,127 @@ +#!/usr/bin/env bun +import { existsSync, statSync } from "node:fs"; +import { readdir, readFile } from "node:fs/promises"; +import { isAbsolute, join } from "node:path"; + +function fail(message, code = 1) { + console.error(message); + process.exit(code); +} + +function usage() { + fail( + "usage: mobile-extension-artifact-paths.mjs --root PATH --artifact-root PATH --extensions CSV --asset-kind runtime|ios-xcframework --asset-target TARGET|* --required 0|1", + 2, + ); +} + +function optionValue(args, name) { + const index = args.indexOf(name); + if (index === -1) { + usage(); + } + const value = args[index + 1]; + if (value === undefined || value.startsWith("--")) { + usage(); + } + return value; +} + +function isFile(path) { + try { + return statSync(path).isFile(); + } catch { + return false; + } +} + +async function manifestPaths(artifactRoot) { + const entries = await readdir(artifactRoot, { withFileTypes: true }); + return entries + .filter((entry) => entry.isDirectory()) + .map((entry) => join(artifactRoot, entry.name, "extension-artifacts.json")) + .filter((path) => existsSync(path)) + .sort(); +} + +function assetMatches(asset, assetKind, assetTarget) { + if (asset.family !== "native") { + return false; + } + if (assetTarget !== "*" && asset.target !== assetTarget) { + return false; + } + if (assetKind === "runtime") { + return asset.kind === "runtime"; + } + if (assetKind === "ios-xcframework") { + return asset.kind === "ios-xcframework"; + } + fail(`unknown extension asset kind: ${assetKind}`); +} + +const args = Bun.argv.slice(2); +const root = optionValue(args, "--root"); +const artifactRoot = optionValue(args, "--artifact-root"); +const selected = optionValue(args, "--extensions") + .split(",") + .map((item) => item.trim()) + .filter(Boolean); +const assetKind = optionValue(args, "--asset-kind"); +const assetTarget = optionValue(args, "--asset-target"); +const required = optionValue(args, "--required") === "1"; + +const bySqlName = new Map(); +for (const manifestPath of await manifestPaths(artifactRoot)) { + const manifest = JSON.parse(await readFile(manifestPath, "utf8")); + const sqlName = manifest.sqlName; + if (typeof sqlName !== "string" || sqlName.length === 0) { + fail(`${manifestPath} does not declare sqlName`); + } + if (bySqlName.has(sqlName)) { + fail(`duplicate exact-extension artifact package for SQL extension ${sqlName}`); + } + bySqlName.set(sqlName, { manifestPath, manifest }); +} + +const paths = []; +const missing = []; +for (const sqlName of selected) { + const entry = bySqlName.get(sqlName); + if (entry === undefined) { + missing.push(`${sqlName}: package`); + continue; + } + const assets = Array.isArray(entry.manifest.assets) ? entry.manifest.assets : []; + const matches = assets.filter( + (asset) => asset !== null && typeof asset === "object" && assetMatches(asset, assetKind, assetTarget), + ); + if (matches.length === 0) { + missing.push(`${sqlName}: ${assetKind} asset`); + continue; + } + if (matches.length !== 1) { + fail( + `${entry.manifestPath} must contain exactly one ${assetKind} asset for ${sqlName}, got ${matches.length}`, + ); + } + const rawPath = matches[0].path; + if (typeof rawPath !== "string" || rawPath.length === 0) { + fail(`${entry.manifestPath} ${assetKind} asset for ${sqlName} does not declare path`); + } + const path = isAbsolute(rawPath) ? rawPath : join(root, rawPath); + if (!isFile(path)) { + missing.push(`${sqlName}: ${path}`); + continue; + } + paths.push(path); +} + +if (missing.length > 0) { + const message = `missing exact-extension artifact(s): ${missing.join(", ")}`; + fail(message, required ? 1 : 3); +} + +for (const path of paths) { + console.log(path); +} diff --git a/src/sdks/react-native/tools/mobile-extension-runtime.sh b/src/sdks/react-native/tools/mobile-extension-runtime.sh index 4ffad019..344ad223 100644 --- a/src/sdks/react-native/tools/mobile-extension-runtime.sh +++ b/src/sdks/react-native/tools/mobile-extension-runtime.sh @@ -142,74 +142,13 @@ oliphaunt_dev_prebuilt_extension_asset_paths_for_selection() { return 1 fi - python3 - "$root" "$artifact_root" "$selected_extensions" "$asset_kind" "$asset_target" "${OLIPHAUNT_EXPO_REQUIRE_PREBUILT_EXTENSIONS:-0}" <<'PY' -import json -import sys -from pathlib import Path - -root = Path(sys.argv[1]) -artifact_root = Path(sys.argv[2]) -selected = [item.strip() for item in sys.argv[3].split(",") if item.strip()] -asset_kind = sys.argv[4] -asset_target = sys.argv[5] -required = sys.argv[6] == "1" - -manifests = sorted(artifact_root.glob("*/extension-artifacts.json")) -by_sql = {} -for manifest_path in manifests: - with manifest_path.open("r", encoding="utf-8") as handle: - manifest = json.load(handle) - sql_name = manifest.get("sqlName") - if not isinstance(sql_name, str) or not sql_name: - raise SystemExit(f"{manifest_path} does not declare sqlName") - if sql_name in by_sql: - raise SystemExit(f"duplicate exact-extension artifact package for SQL extension {sql_name}") - by_sql[sql_name] = (manifest_path, manifest) - -def asset_matches(asset): - if asset.get("family") != "native": - return False - if asset_target != "*" and asset.get("target") != asset_target: - return False - kind = asset.get("kind") - if asset_kind == "runtime": - return kind == "runtime" - if asset_kind == "ios-xcframework": - return kind == "ios-xcframework" - raise SystemExit(f"unknown extension asset kind: {asset_kind}") - -paths = [] -missing = [] -for sql_name in selected: - entry = by_sql.get(sql_name) - if entry is None: - missing.append(f"{sql_name}: package") - continue - manifest_path, manifest = entry - matches = [asset for asset in manifest.get("assets", []) if isinstance(asset, dict) and asset_matches(asset)] - if not matches: - missing.append(f"{sql_name}: {asset_kind} asset") - continue - if len(matches) != 1: - raise SystemExit(f"{manifest_path} must contain exactly one {asset_kind} asset for {sql_name}, got {len(matches)}") - raw_path = matches[0].get("path") - if not isinstance(raw_path, str) or not raw_path: - raise SystemExit(f"{manifest_path} {asset_kind} asset for {sql_name} does not declare path") - path = root / raw_path - if not path.is_file(): - missing.append(f"{sql_name}: {path}") - continue - paths.append(path) - -if missing: - message = "missing exact-extension artifact(s): " + ", ".join(missing) - if required: - raise SystemExit(message) - raise SystemExit(3) - -for path in paths: - print(path) -PY + "$root/tools/dev/bun.sh" "$root/src/sdks/react-native/tools/mobile-extension-artifact-paths.mjs" \ + --root "$root" \ + --artifact-root "$artifact_root" \ + --extensions "$selected_extensions" \ + --asset-kind "$asset_kind" \ + --asset-target "$asset_target" \ + --required "${OLIPHAUNT_EXPO_REQUIRE_PREBUILT_EXTENSIONS:-0}" } oliphaunt_dev_prebuilt_extension_runtime_artifacts_for_selection() { diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index eac31e80..8b56d9fb 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -43,6 +43,7 @@ require_file tools/policy/check-native-boundaries.mjs require_file tools/policy/python-entrypoints.allowlist require_file tools/runtime/preflight.sh require_file src/sdks/rust/tools/cargo-artifact-patches.mjs +require_file src/sdks/react-native/tools/mobile-extension-artifact-paths.mjs require_file tools/release/cargo-crate-filename.mjs require_file tools/dev/bun.sh require_file tools/dev/deno.sh @@ -251,6 +252,11 @@ fi if grep -Eq "python3[[:space:]]+(-[[:space:]]+)?<<'PY'" tools/runtime/preflight.sh; then fail "runtime preflight must use Bun instead of inline Python" fi +grep -Fq 'mobile-extension-artifact-paths.mjs' src/sdks/react-native/tools/mobile-extension-runtime.sh || + fail "React Native mobile extension runtime helper must use the Bun artifact path resolver" +if grep -Eq "python3[[:space:]]+(-[[:space:]]+)?<<'PY'" src/sdks/react-native/tools/mobile-extension-runtime.sh; then + fail "React Native mobile extension runtime helper must use Bun instead of inline Python" +fi grep -Fq 'bun src/sdks/rust/tools/cargo-artifact-patches.mjs' src/sdks/rust/tools/check-sdk.sh || fail "Rust SDK Cargo artifact patch generation must use the Bun helper" if grep -Fq 'python3 - "$root" "$liboliphaunt_cargo_artifacts/packages.json"' src/sdks/rust/tools/check-sdk.sh; then From 33d33479dc6e06b51ec3c876b61ee2a2e52debcf Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 17:45:54 +0000 Subject: [PATCH 105/137] chore: expose rust release source preparation cli --- docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 10 ++++++++++ src/sdks/rust/tools/check-sdk.sh | 9 +-------- tools/policy/check-tooling-stack.sh | 5 +++++ tools/release/release.py | 12 ++++++++++++ 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index b1e73741..ce0303c3 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -151,6 +151,16 @@ until the current-state gates here are checked with fresh local evidence. `bash src/sdks/react-native/tools/check-sdk.sh check-static`, `python3 tools/release/check_release_metadata.py`, `python3 tools/release/check_consumer_shape.py`, and `git diff --check`. +- 2026-06-26: Rust SDK broker Cargo relay smoke setup now prepares the generated + publish source through `python3 tools/release/release.py + prepare-rust-release-source` instead of an inline Python heredoc that imports + release internals. The release CLI command validates generated Rust SDK + artifact dependency coverage and prints the staged manifest path. Fresh + checks passed: `python3 tools/release/release.py prepare-rust-release-source`, + `bash src/sdks/rust/tools/check-sdk.sh package-shape`, + `bash tools/policy/check-tooling-stack.sh`, + `python3 tools/release/check_release_metadata.py`, + `python3 tools/release/check_consumer_shape.py`, and `git diff --check`. - 2026-06-26: Mobile explicit runtime-directory validation now requires release-shaped `oliphaunt/runtime/files` proof before selected extensions are accepted on Kotlin Android and Swift native-direct; React Native forwards the diff --git a/src/sdks/rust/tools/check-sdk.sh b/src/sdks/rust/tools/check-sdk.sh index 7ba882fe..fb55f4ea 100755 --- a/src/sdks/rust/tools/check-sdk.sh +++ b/src/sdks/rust/tools/check-sdk.sh @@ -178,14 +178,7 @@ check_broker_cargo_relay_fixture() { --output-dir "$cargo_artifacts" \ --version "$broker_version" - printf '\n==> prepare generated oliphaunt release Cargo source\n' - PYTHONPATH=tools/release python3 - <<'PY' -import release - -release.prepare_oliphaunt_release_source( - release.current_product_version("oliphaunt-rust") -) -PY + run python3 tools/release/release.py prepare-rust-release-source smoke="$(prepare_scratch_dir broker-cargo-relay-smoke)" mkdir -p "$smoke/src" diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 8b56d9fb..2b05d77a 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -259,6 +259,11 @@ if grep -Eq "python3[[:space:]]+(-[[:space:]]+)?<<'PY'" src/sdks/react-native/to fi grep -Fq 'bun src/sdks/rust/tools/cargo-artifact-patches.mjs' src/sdks/rust/tools/check-sdk.sh || fail "Rust SDK Cargo artifact patch generation must use the Bun helper" +grep -Fq 'python3 tools/release/release.py prepare-rust-release-source' src/sdks/rust/tools/check-sdk.sh || + fail "Rust SDK check must prepare generated publish source through the release CLI" +if grep -Eq "python3[[:space:]]+(-[[:space:]]+)?<<'PY'" src/sdks/rust/tools/check-sdk.sh; then + fail "Rust SDK check must not use inline Python heredocs" +fi if grep -Fq 'python3 - "$root" "$liboliphaunt_cargo_artifacts/packages.json"' src/sdks/rust/tools/check-sdk.sh; then fail "Rust SDK Cargo artifact patch generation must not use inline Python" fi diff --git a/tools/release/release.py b/tools/release/release.py index 96650a67..be2063bd 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -1574,6 +1574,15 @@ def validate_staged_sdk_package(product: str) -> None: run(["python3", "tools/release/check_staged_artifacts.py", "--require-sdk-product", product]) +def command_prepare_rust_release_source(passthrough: list[str]) -> None: + if passthrough: + fail("prepare-rust-release-source does not accept extra arguments: " + " ".join(passthrough)) + version = current_product_version("oliphaunt-rust") + release_manifest = prepare_oliphaunt_release_source(version) + validate_generated_oliphaunt_release_artifact_coverage(release_manifest) + print(release_manifest.relative_to(ROOT)) + + def run_rust_sdk_dry_run(allow_dirty: bool, head_ref: str) -> None: version = current_product_version("oliphaunt-rust") validate_staged_sdk_package("oliphaunt-rust") @@ -3212,6 +3221,7 @@ def main(argv: list[str]) -> int: "consumer-shape", "ci-artifacts", "ci-products", + "prepare-rust-release-source", "verify-release", ]: subparsers.add_parser(name, add_help=False) @@ -3242,6 +3252,8 @@ def main(argv: list[str]) -> int: command_ci_artifacts(passthrough) elif command == "ci-products": command_ci_products(passthrough) + elif command == "prepare-rust-release-source": + command_prepare_rust_release_source(passthrough) elif command == "verify-release": command_verify_release(passthrough) elif command == "publish-dry-run": From 960c8b5658f76ae74c2feb0e40aef202488ed22f Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 17:50:38 +0000 Subject: [PATCH 106/137] chore: move wasix third-party toml reads to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 9 ++++ .../wasix/assets/build/wasix-toml-value.mjs | 46 +++++++++++++++++++ .../wasix/assets/build/wasix_third_party.sh | 43 ++++------------- tools/policy/check-tooling-stack.sh | 6 +++ 4 files changed, 71 insertions(+), 33 deletions(-) create mode 100644 src/runtimes/liboliphaunt/wasix/assets/build/wasix-toml-value.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index ce0303c3..c9cec805 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -161,6 +161,15 @@ until the current-state gates here are checked with fresh local evidence. `bash tools/policy/check-tooling-stack.sh`, `python3 tools/release/check_release_metadata.py`, `python3 tools/release/check_consumer_shape.py`, and `git diff --check`. +- 2026-06-26: WASIX third-party extension build metadata reads now use + `src/runtimes/liboliphaunt/wasix/assets/build/wasix-toml-value.mjs` through + the pinned Bun launcher instead of inline Python heredocs in + `wasix_third_party.sh`. Direct probes covered recipe string reads, dependency + list reads, and the previous missing-list-as-empty behavior; sourced shell + function probes returned `postgis` and the expected PostGIS dependency list. + Fresh checks passed: `tools/dev/bun.sh --version`, + `bash -n src/runtimes/liboliphaunt/wasix/assets/build/wasix_third_party.sh`, + `bash tools/policy/check-tooling-stack.sh`, and `git diff --check`. - 2026-06-26: Mobile explicit runtime-directory validation now requires release-shaped `oliphaunt/runtime/files` proof before selected extensions are accepted on Kotlin Android and Swift native-direct; React Native forwards the diff --git a/src/runtimes/liboliphaunt/wasix/assets/build/wasix-toml-value.mjs b/src/runtimes/liboliphaunt/wasix/assets/build/wasix-toml-value.mjs new file mode 100644 index 00000000..40b5aafc --- /dev/null +++ b/src/runtimes/liboliphaunt/wasix/assets/build/wasix-toml-value.mjs @@ -0,0 +1,46 @@ +#!/usr/bin/env bun + +function fail(message) { + console.error(message); + process.exit(2); +} + +function usage() { + fail("usage: wasix-toml-value.mjs string|string-list "); +} + +function isObject(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +const [mode, file, key] = Bun.argv.slice(2); +if ((mode !== "string" && mode !== "string-list") || !file || !key) { + usage(); +} + +let data; +try { + data = Bun.TOML.parse(await Bun.file(file).text()); +} catch (error) { + fail(`could not read TOML file ${file}: ${error.message}`); +} + +if (!isObject(data)) { + fail(`${file} must contain a TOML table`); +} + +if (mode === "string-list") { + const values = Object.hasOwn(data, key) ? data[key] : []; + if (!Array.isArray(values) || !values.every((value) => typeof value === "string")) { + fail(`${file} field ${key} must be an array of strings`); + } + for (const value of values) { + console.log(value); + } +} else { + const value = data[key]; + if (typeof value !== "string" || value.length === 0) { + fail(`${file} field ${key} must be a non-empty string`); + } + console.log(value); +} diff --git a/src/runtimes/liboliphaunt/wasix/assets/build/wasix_third_party.sh b/src/runtimes/liboliphaunt/wasix/assets/build/wasix_third_party.sh index 9cd94d5a..9c11727b 100755 --- a/src/runtimes/liboliphaunt/wasix/assets/build/wasix_third_party.sh +++ b/src/runtimes/liboliphaunt/wasix/assets/build/wasix_third_party.sh @@ -111,23 +111,11 @@ oliphaunt_wasix_extension_wasix_target_values() { local extension="$2" local key="$3" local target="$repo_root/src/extensions/external/$extension/targets/wasix.toml" - python3 - "$target" "$key" <<'PY' -from __future__ import annotations - -import sys -import tomllib -from pathlib import Path - -target = Path(sys.argv[1]) -key = sys.argv[2] -with target.open("rb") as handle: - data = tomllib.load(handle) -values = data.get(key, []) -if not isinstance(values, list) or not all(isinstance(value, str) for value in values): - raise SystemExit(f"{target} field {key} must be an array of strings") -for value in values: - print(value) -PY + "$repo_root/tools/dev/bun.sh" \ + "$repo_root/src/runtimes/liboliphaunt/wasix/assets/build/wasix-toml-value.mjs" \ + string-list \ + "$target" \ + "$key" } oliphaunt_wasix_extension_recipe_value() { @@ -135,22 +123,11 @@ oliphaunt_wasix_extension_recipe_value() { local extension="$2" local key="$3" local recipe="$repo_root/src/extensions/external/$extension/recipe.toml" - python3 - "$recipe" "$key" <<'PY' -from __future__ import annotations - -import sys -import tomllib -from pathlib import Path - -recipe = Path(sys.argv[1]) -key = sys.argv[2] -with recipe.open("rb") as handle: - data = tomllib.load(handle) -value = data.get(key) -if not isinstance(value, str) or not value: - raise SystemExit(f"{recipe} field {key} must be a non-empty string") -print(value) -PY + "$repo_root/tools/dev/bun.sh" \ + "$repo_root/src/runtimes/liboliphaunt/wasix/assets/build/wasix-toml-value.mjs" \ + string \ + "$recipe" \ + "$key" } oliphaunt_wasix_extension_source_dir() { diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 2b05d77a..73b9dc18 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -44,6 +44,7 @@ require_file tools/policy/python-entrypoints.allowlist require_file tools/runtime/preflight.sh require_file src/sdks/rust/tools/cargo-artifact-patches.mjs require_file src/sdks/react-native/tools/mobile-extension-artifact-paths.mjs +require_file src/runtimes/liboliphaunt/wasix/assets/build/wasix-toml-value.mjs require_file tools/release/cargo-crate-filename.mjs require_file tools/dev/bun.sh require_file tools/dev/deno.sh @@ -257,6 +258,11 @@ grep -Fq 'mobile-extension-artifact-paths.mjs' src/sdks/react-native/tools/mobil if grep -Eq "python3[[:space:]]+(-[[:space:]]+)?<<'PY'" src/sdks/react-native/tools/mobile-extension-runtime.sh; then fail "React Native mobile extension runtime helper must use Bun instead of inline Python" fi +grep -Fq 'wasix-toml-value.mjs' src/runtimes/liboliphaunt/wasix/assets/build/wasix_third_party.sh || + fail "WASIX third-party build helper must use the Bun TOML reader" +if grep -Eq "python3[[:space:]]+(-[[:space:]]+)?<<'PY'" src/runtimes/liboliphaunt/wasix/assets/build/wasix_third_party.sh; then + fail "WASIX third-party build helper must use Bun instead of inline Python" +fi grep -Fq 'bun src/sdks/rust/tools/cargo-artifact-patches.mjs' src/sdks/rust/tools/check-sdk.sh || fail "Rust SDK Cargo artifact patch generation must use the Bun helper" grep -Fq 'python3 tools/release/release.py prepare-rust-release-source' src/sdks/rust/tools/check-sdk.sh || From 38bfb8dc311941feeb291f40a59e7e0f040675d8 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 17:58:28 +0000 Subject: [PATCH 107/137] chore: move wasix extension asset packaging to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 13 + .../wasix/tools/package-release-assets.mjs | 240 ++++++++++++++++++ .../wasix/tools/package-release-assets.sh | 115 +-------- tools/policy/check-tooling-stack.sh | 6 + 4 files changed, 267 insertions(+), 107 deletions(-) create mode 100644 src/extensions/artifacts/wasix/tools/package-release-assets.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index c9cec805..e9c5c110 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -170,6 +170,19 @@ until the current-state gates here are checked with fresh local evidence. Fresh checks passed: `tools/dev/bun.sh --version`, `bash -n src/runtimes/liboliphaunt/wasix/assets/build/wasix_third_party.sh`, `bash tools/policy/check-tooling-stack.sh`, and `git diff --check`. +- 2026-06-26: WASIX exact-extension release asset packaging now uses + `src/extensions/artifacts/wasix/tools/package-release-assets.mjs` through the + pinned Bun launcher instead of shell-embedded Python/product_metadata calls. + Product-scoped PostGIS packaging passed through both direct helper and shell + wrapper paths, and an all-extension smoke staged 39 WASIX exact-extension + artifacts plus TSV index rows from the generated runtime asset directory. + Fresh checks passed: `bash -n + src/extensions/artifacts/wasix/tools/package-release-assets.sh`, + `bash tools/policy/check-tooling-stack.sh`, + `python3 tools/release/check_artifact_targets.py`, + `python3 tools/policy/check-release-policy.py`, + `python3 tools/release/check_release_metadata.py`, + `python3 tools/release/check_consumer_shape.py`, and `git diff --check`. - 2026-06-26: Mobile explicit runtime-directory validation now requires release-shaped `oliphaunt/runtime/files` proof before selected extensions are accepted on Kotlin Android and Swift native-direct; React Native forwards the diff --git a/src/extensions/artifacts/wasix/tools/package-release-assets.mjs b/src/extensions/artifacts/wasix/tools/package-release-assets.mjs new file mode 100644 index 00000000..db78aa31 --- /dev/null +++ b/src/extensions/artifacts/wasix/tools/package-release-assets.mjs @@ -0,0 +1,240 @@ +#!/usr/bin/env bun +import { copyFile, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises"; +import path from "node:path"; + +const PREFIX = "package-wasix-extension-assets.sh"; +const WASIX_PRODUCT_PATH = "src/runtimes/liboliphaunt/wasix"; +const EXTENSION_CLASSES = ["contrib", "external", "first-party"]; + +function fail(message) { + console.error(`${PREFIX}: ${message}`); + process.exit(2); +} + +function usage() { + fail( + "usage: package-release-assets.mjs --root PATH --asset-root PATH --metadata PATH --out-dir PATH --target TARGET --extension-products CSV", + ); +} + +function optionValue(args, name) { + const index = args.indexOf(name); + if (index === -1) { + usage(); + } + const value = args[index + 1]; + if (value === undefined || value.startsWith("--")) { + usage(); + } + return value; +} + +function parseCsv(value) { + return [...new Set(value.split(",").map((item) => item.trim()).filter(Boolean))].sort(); +} + +function isObject(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +async function readJson(file) { + let value; + try { + value = JSON.parse(await readFile(file, "utf8")); + } catch (error) { + fail(`could not read JSON file ${file}: ${error.message}`); + } + if (!isObject(value)) { + fail(`${file} must contain a JSON object`); + } + return value; +} + +async function readToml(file) { + let value; + try { + value = Bun.TOML.parse(await readFile(file, "utf8")); + } catch (error) { + fail(`could not read TOML file ${file}: ${error.message}`); + } + if (!isObject(value)) { + fail(`${file} must contain a TOML table`); + } + return value; +} + +function relativeToRoot(root, file) { + return path.relative(root, file).split(path.sep).join("/"); +} + +async function releaseVersion(root) { + const manifestPath = path.join(root, ".release-please-manifest.json"); + const manifest = await readJson(manifestPath); + const version = manifest[WASIX_PRODUCT_PATH]; + if (typeof version !== "string" || version.length === 0) { + fail(`.release-please-manifest.json is missing ${WASIX_PRODUCT_PATH}`); + } + return version; +} + +async function extensionReleaseTomls(root) { + const files = []; + for (const extensionClass of EXTENSION_CLASSES) { + const classRoot = path.join(root, "src/extensions", extensionClass); + let entries; + try { + entries = await readdir(classRoot, { withFileTypes: true }); + } catch { + continue; + } + for (const entry of entries) { + if (entry.isDirectory()) { + const releasePath = path.join(classRoot, entry.name, "release.toml"); + if ((await fileSize(releasePath)) !== undefined) { + files.push(releasePath); + } + } + } + } + return files.sort(); +} + +async function selectedSqlNames(root, extensionProductsCsv) { + const products = parseCsv(extensionProductsCsv); + if (products.length === 0) { + return new Set(); + } + + const byProduct = new Map(); + for (const releasePath of await extensionReleaseTomls(root)) { + const metadata = await readToml(releasePath); + const product = metadata.id; + if (typeof product === "string" && product.length > 0) { + byProduct.set(product, { metadata, releasePath }); + } + } + + const sqlNames = new Set(); + for (const product of products) { + const entry = byProduct.get(product); + if (entry === undefined) { + fail(`unknown exact-extension artifact product ${product}`); + } + const { metadata, releasePath } = entry; + if (metadata.kind !== "exact-extension-artifact") { + fail(`${product} is not an exact-extension artifact product`); + } + const sqlName = metadata.extension_sql_name; + if (typeof sqlName !== "string" || sqlName.length === 0) { + fail(`${product} release metadata must declare extension_sql_name`); + } + const nestedSqlName = metadata.extension?.sql_name; + if (nestedSqlName !== undefined && nestedSqlName !== sqlName) { + fail( + `${relativeToRoot(root, releasePath)} extension.sql_name ${JSON.stringify( + nestedSqlName, + )} must match extension_sql_name ${JSON.stringify(sqlName)}`, + ); + } + sqlNames.add(sqlName); + } + return sqlNames; +} + +async function fileSize(file) { + try { + return (await stat(file)).size; + } catch { + return undefined; + } +} + +function tsvCell(value) { + const text = String(value); + if (text.includes("\t") || text.includes("\n") || text.includes("\r")) { + fail(`TSV field contains unsupported whitespace: ${JSON.stringify(text)}`); + } + return text; +} + +const args = Bun.argv.slice(2); +const root = path.resolve(optionValue(args, "--root")); +const assetRoot = path.resolve(optionValue(args, "--asset-root")); +const metadataPath = path.resolve(optionValue(args, "--metadata")); +const outDir = path.resolve(optionValue(args, "--out-dir")); +const targetId = optionValue(args, "--target"); +const extensionProductsCsv = optionValue(args, "--extension-products"); + +const [version, selected] = await Promise.all([ + releaseVersion(root), + selectedSqlNames(root, extensionProductsCsv), +]); + +const data = await readJson(metadataPath); +const extensions = data.extensions; +if (!Array.isArray(extensions) || extensions.length === 0) { + fail(`${relativeToRoot(root, metadataPath)} must contain a non-empty extensions array`); +} + +await rm(outDir, { recursive: true, force: true }); +await mkdir(outDir, { recursive: true }); + +const rows = []; +for (const item of extensions) { + if (!isObject(item)) { + fail(`${relativeToRoot(root, metadataPath)} contains a non-object extension row`); + } + const sqlName = item["sql-name"]; + const archive = item.archive; + if (typeof sqlName !== "string" || sqlName.length === 0) { + fail(`${relativeToRoot(root, metadataPath)} contains an extension row without sql-name`); + } + if (selected.size > 0 && !selected.has(sqlName)) { + continue; + } + if (typeof archive !== "string" || archive.length === 0) { + fail(`${relativeToRoot(root, metadataPath)} row for ${sqlName} is missing archive`); + } + + const source = path.join(assetRoot, archive); + const sourceBytes = await fileSize(source); + if (sourceBytes === undefined) { + fail(`missing WASIX extension archive for ${sqlName}: ${relativeToRoot(root, source)}`); + } + if (sourceBytes === 0) { + fail(`WASIX extension archive for ${sqlName} is empty: ${relativeToRoot(root, source)}`); + } + + const artifact = `liboliphaunt-wasix-${version}-extension-${sqlName}-${targetId}.tar.zst`; + const destination = path.join(outDir, artifact); + await copyFile(source, destination); + const artifactBytes = await fileSize(destination); + rows.push({ + sqlName, + target: targetId, + kind: "wasix-runtime", + artifact, + artifactBytes, + }); +} + +if (rows.length === 0) { + fail("no WASIX extension artifacts were staged"); +} + +const indexPath = path.join(outDir, `liboliphaunt-wasix-${version}-wasix-extension-assets.tsv`); +const lines = [["sql_name", "target", "kind", "artifact", "artifact_bytes"].join("\t")]; +for (const row of rows) { + lines.push( + [ + tsvCell(row.sqlName), + tsvCell(row.target), + tsvCell(row.kind), + tsvCell(row.artifact), + tsvCell(row.artifactBytes), + ].join("\t"), + ); +} +await writeFile(indexPath, `${lines.join("\n")}\n`, "utf8"); + +console.log(`staged ${rows.length} WASIX exact-extension artifact(s) in ${relativeToRoot(root, outDir)}`); diff --git a/src/extensions/artifacts/wasix/tools/package-release-assets.sh b/src/extensions/artifacts/wasix/tools/package-release-assets.sh index 98103068..25607e29 100755 --- a/src/extensions/artifacts/wasix/tools/package-release-assets.sh +++ b/src/extensions/artifacts/wasix/tools/package-release-assets.sh @@ -32,35 +32,6 @@ if [ -n "$extension_product" ]; then extension_products="$extension_product" fi fi -selected_sql_names="" -if [ -n "$extension_products" ]; then - selected_sql_names="$( - python3 - "$extension_products" <<'PY' -import sys -from pathlib import Path - -root = Path.cwd() -sys.path.insert(0, str(root / "tools" / "release")) -import product_metadata - -products = sorted({item.strip() for item in sys.argv[1].split(",") if item.strip()}) -if not products: - raise SystemExit("no exact-extension products were selected") -sql_names = [] -for product in products: - config = product_metadata.product_config(product) - if config.get("kind") != "exact-extension-artifact": - raise SystemExit(f"{product} is not an exact-extension artifact product") - sql_name = config.get("extension_sql_name") - if not isinstance(sql_name, str) or not sql_name: - raise SystemExit(f"{product} release metadata must declare extension_sql_name") - sql_names.append(sql_name) -print(",".join(sorted(set(sql_names)))) -PY - )" -fi - -version="$(python3 tools/release/product_metadata.py version liboliphaunt-wasix)" asset_root="$root/target/oliphaunt-wasix/assets" generated_metadata="$root/src/extensions/generated/wasix/extensions.json" default_out_dir="$root/target/extensions/wasix/release-assets/$target_id" @@ -68,87 +39,17 @@ if [ -n "$extension_product" ] && [ -z "${OLIPHAUNT_EXTENSION_PRODUCTS:-}" ]; th default_out_dir="$default_out_dir/$extension_product" fi out_dir="${OLIPHAUNT_WASIX_EXTENSION_RELEASE_ASSET_DIR:-$default_out_dir}" -asset_index="$out_dir/liboliphaunt-wasix-${version}-wasix-extension-assets.tsv" [ -f "$generated_metadata" ] || fail "missing generated WASIX extension metadata: ${generated_metadata#$root/}" [ -d "$asset_root/extensions" ] || fail "missing WASIX extension asset directory: ${asset_root#$root/}/extensions" -rm -rf "$out_dir" -mkdir -p "$out_dir" - -python3 - "$root" "$asset_root" "$generated_metadata" "$out_dir" "$version" "$target_id" "$asset_index" "$selected_sql_names" <<'PY' -from __future__ import annotations - -import csv -import json -import shutil -import sys -from pathlib import Path - - -root = Path(sys.argv[1]) -asset_root = Path(sys.argv[2]) -metadata_path = Path(sys.argv[3]) -out_dir = Path(sys.argv[4]) -version = sys.argv[5] -target_id = sys.argv[6] -asset_index = Path(sys.argv[7]) -selected_sql_names = {item.strip() for item in sys.argv[8].split(",") if item.strip()} - - -def fail(message: str) -> None: - raise SystemExit(f"package-wasix-extension-assets.sh: {message}") - - -data = json.loads(metadata_path.read_text(encoding="utf-8")) -extensions = data.get("extensions") -if not isinstance(extensions, list) or not extensions: - fail(f"{metadata_path.relative_to(root)} must contain a non-empty extensions array") - -rows: list[dict[str, object]] = [] -for item in extensions: - if not isinstance(item, dict): - fail(f"{metadata_path.relative_to(root)} contains a non-object extension row") - sql_name = item.get("sql-name") - archive = item.get("archive") - if not isinstance(sql_name, str) or not sql_name: - fail(f"{metadata_path.relative_to(root)} contains an extension row without sql-name") - if selected_sql_names and sql_name not in selected_sql_names: - continue - if not isinstance(archive, str) or not archive: - fail(f"{metadata_path.relative_to(root)} row for {sql_name} is missing archive") - source = asset_root / archive - if not source.is_file(): - fail(f"missing WASIX extension archive for {sql_name}: {source.relative_to(root)}") - if source.stat().st_size == 0: - fail(f"WASIX extension archive for {sql_name} is empty: {source.relative_to(root)}") - destination_name = f"liboliphaunt-wasix-{version}-extension-{sql_name}-{target_id}.tar.zst" - destination = out_dir / destination_name - shutil.copy2(source, destination) - rows.append( - { - "sql_name": sql_name, - "target": target_id, - "kind": "wasix-runtime", - "artifact": destination_name, - "artifact_bytes": destination.stat().st_size, - } - ) - -if not rows: - fail("no WASIX extension artifacts were staged") - -with asset_index.open("w", encoding="utf-8", newline="") as handle: - writer = csv.DictWriter( - handle, - delimiter="\t", - fieldnames=["sql_name", "target", "kind", "artifact", "artifact_bytes"], - lineterminator="\n", - ) - writer.writeheader() - writer.writerows(rows) - -print(f"staged {len(rows)} WASIX exact-extension artifact(s) in {out_dir.relative_to(root)}") -PY +"$root/tools/dev/bun.sh" \ + "$root/src/extensions/artifacts/wasix/tools/package-release-assets.mjs" \ + --root "$root" \ + --asset-root "$asset_root" \ + --metadata "$generated_metadata" \ + --out-dir "$out_dir" \ + --target "$target_id" \ + --extension-products "$extension_products" echo "wasixExtensionReleaseAssetDir=$out_dir" diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 73b9dc18..7f56db9f 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -45,6 +45,7 @@ require_file tools/runtime/preflight.sh require_file src/sdks/rust/tools/cargo-artifact-patches.mjs require_file src/sdks/react-native/tools/mobile-extension-artifact-paths.mjs require_file src/runtimes/liboliphaunt/wasix/assets/build/wasix-toml-value.mjs +require_file src/extensions/artifacts/wasix/tools/package-release-assets.mjs require_file tools/release/cargo-crate-filename.mjs require_file tools/dev/bun.sh require_file tools/dev/deno.sh @@ -263,6 +264,11 @@ grep -Fq 'wasix-toml-value.mjs' src/runtimes/liboliphaunt/wasix/assets/build/was if grep -Eq "python3[[:space:]]+(-[[:space:]]+)?<<'PY'" src/runtimes/liboliphaunt/wasix/assets/build/wasix_third_party.sh; then fail "WASIX third-party build helper must use Bun instead of inline Python" fi +grep -Fq 'package-release-assets.mjs' src/extensions/artifacts/wasix/tools/package-release-assets.sh || + fail "WASIX exact-extension release packager must use the Bun packager" +if grep -Fq 'python3' src/extensions/artifacts/wasix/tools/package-release-assets.sh; then + fail "WASIX exact-extension release packager shell must use Bun instead of Python" +fi grep -Fq 'bun src/sdks/rust/tools/cargo-artifact-patches.mjs' src/sdks/rust/tools/check-sdk.sh || fail "Rust SDK Cargo artifact patch generation must use the Bun helper" grep -Fq 'python3 tools/release/release.py prepare-rust-release-source' src/sdks/rust/tools/check-sdk.sh || From 76155debc906c9b3a1b8258951f7d463a9b22929 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 18:09:38 +0000 Subject: [PATCH 108/137] chore: move github release asset uploads to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 10 + tools/policy/check-release-policy.py | 6 +- tools/policy/python-entrypoints.allowlist | 1 - tools/release/release.py | 3 +- .../release/upload_github_release_assets.mjs | 262 ++++++++++++++++++ tools/release/upload_github_release_assets.py | 163 ----------- 6 files changed, 277 insertions(+), 168 deletions(-) create mode 100644 tools/release/upload_github_release_assets.mjs delete mode 100755 tools/release/upload_github_release_assets.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index e9c5c110..ef3c5051 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -183,6 +183,16 @@ until the current-state gates here are checked with fresh local evidence. `python3 tools/policy/check-release-policy.py`, `python3 tools/release/check_release_metadata.py`, `python3 tools/release/check_consumer_shape.py`, and `git diff --check`. +- 2026-06-26: GitHub release asset upload tooling now uses + `tools/release/upload_github_release_assets.mjs` through the pinned Bun + launcher from `release.py`; the retired Python uploader was removed from the + intentional Python inventory. Local CLI probes covered missing repository, + unknown product default-tag resolution, and missing asset rejection before any + GitHub upload call. Fresh checks passed: + `bash tools/policy/check-tooling-stack.sh`, + `python3 tools/policy/check-release-policy.py`, + `python3 tools/release/check_release_metadata.py`, + `python3 tools/release/check_consumer_shape.py`, and `git diff --check`. - 2026-06-26: Mobile explicit runtime-directory validation now requires release-shaped `oliphaunt/runtime/files` proof before selected extensions are accepted on Kotlin Android and Swift native-direct; React Native forwards the diff --git a/tools/policy/check-release-policy.py b/tools/policy/check-release-policy.py index fd8411ce..7b483e8d 100644 --- a/tools/policy/check-release-policy.py +++ b/tools/policy/check-release-policy.py @@ -636,7 +636,7 @@ def check_ci_policy() -> None: for path in ( ".github/workflows/release.yml", "tools/release/release.py", - "tools/release/upload_github_release_assets.py", + "tools/release/upload_github_release_assets.mjs", ): assert_not_contains( path, @@ -649,12 +649,12 @@ def check_ci_policy() -> None: "GitHub release asset replacement must stay a manual repair, not a release CLI switch", ) assert_not_contains( - "tools/release/upload_github_release_assets.py", + "tools/release/upload_github_release_assets.mjs", "--clobber", "GitHub release asset upload must not overwrite existing assets", ) assert_contains( - "tools/release/upload_github_release_assets.py", + "tools/release/upload_github_release_assets.mjs", "delete the conflicting GitHub release asset manually", "GitHub release asset byte conflicts must fail with manual repair guidance", ) diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index 8d348ade..f18e1c9d 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -33,6 +33,5 @@ tools/release/release_plan.py tools/release/render_swiftpm_release_package.py tools/release/strip_native_release_binaries.py tools/release/sync_release_pr.py -tools/release/upload_github_release_assets.py tools/release/verify_github_release_attestations.py tools/runtime/with-native-runtime-lock.py diff --git a/tools/release/release.py b/tools/release/release.py index be2063bd..40e50402 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -465,7 +465,8 @@ def glob_release_assets(asset_dir: Path, suffixes: tuple[str, ...]) -> list[str] def upload_github_release_assets(product: str, *, tag: str | None = None, assets: list[str] | None = None) -> None: command = [ - "tools/release/upload_github_release_assets.py", + "tools/dev/bun.sh", + "tools/release/upload_github_release_assets.mjs", product, "--tag", tag or product_tag(product), diff --git a/tools/release/upload_github_release_assets.mjs b/tools/release/upload_github_release_assets.mjs new file mode 100644 index 00000000..24680ce6 --- /dev/null +++ b/tools/release/upload_github_release_assets.mjs @@ -0,0 +1,262 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; +import { existsSync, mkdtempSync, rmSync } from "node:fs"; +import { stat } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { createHash } from "node:crypto"; + +const ROOT = path.resolve(import.meta.dir, "../.."); + +function fail(message) { + console.error(`upload_github_release_assets.mjs: ${message}`); + process.exit(1); +} + +function usage() { + fail("usage: upload_github_release_assets.mjs [--tag TAG] [--repo OWNER/NAME] [--asset PATH]..."); +} + +function parseArgs(argv) { + const args = { + product: undefined, + tag: undefined, + repo: process.env.GITHUB_REPOSITORY || "", + assets: [], + }; + let index = 0; + while (index < argv.length) { + const arg = argv[index]; + if (arg === "--tag") { + args.tag = valueArg(argv, index, arg); + index += 2; + } else if (arg === "--repo") { + args.repo = valueArg(argv, index, arg); + index += 2; + } else if (arg === "--asset") { + args.assets.push(valueArg(argv, index, arg)); + index += 2; + } else if (arg.startsWith("--")) { + usage(); + } else if (args.product === undefined) { + args.product = arg; + index += 1; + } else { + usage(); + } + } + if (!args.product) { + usage(); + } + return args; +} + +function valueArg(argv, index, name) { + const value = argv[index + 1]; + if (value === undefined || value.startsWith("--")) { + usage(); + } + return value; +} + +async function readJson(relativePath) { + const file = path.join(ROOT, relativePath); + let value; + try { + value = JSON.parse(await Bun.file(file).text()); + } catch (error) { + fail(`could not read ${relativePath}: ${error.message}`); + } + if (value === null || typeof value !== "object" || Array.isArray(value)) { + fail(`${relativePath} must contain a JSON object`); + } + return value; +} + +async function productPath(product) { + const config = await readJson("release-please-config.json"); + const packages = config.packages; + if (packages === null || typeof packages !== "object" || Array.isArray(packages)) { + fail("release-please-config.json must define packages"); + } + for (const [packagePath, packageConfig] of Object.entries(packages)) { + if ( + packageConfig !== null && + typeof packageConfig === "object" && + !Array.isArray(packageConfig) && + packageConfig.component === product + ) { + if (config["include-v-in-tag"] !== true) { + fail("release-please must include v in product tags"); + } + if (config["tag-separator"] !== "-") { + fail("release-please tag-separator must be '-'"); + } + return packagePath; + } + } + fail(`unknown release product ${JSON.stringify(product)}`); +} + +async function defaultTag(product) { + const manifest = await readJson(".release-please-manifest.json"); + const packagePath = await productPath(product); + const version = manifest[packagePath]; + if (typeof version !== "string" || version.length === 0) { + fail(`.release-please-manifest.json is missing ${packagePath}`); + } + return `${product}-v${version}`; +} + +function runGh(args, options = {}) { + const result = spawnSync("gh", args, { + cwd: ROOT, + encoding: "utf8", + stdio: options.capture ? ["ignore", "pipe", "pipe"] : "inherit", + }); + if (result.error !== undefined) { + fail(`gh failed to start: ${result.error.message}`); + } + if (result.status !== 0) { + if (options.capture) { + process.stderr.write(result.stderr); + } + fail(`gh ${args.join(" ")} failed with exit ${result.status}`); + } + return result.stdout ?? ""; +} + +function releaseExists(tag, repo) { + const result = spawnSync("gh", ["release", "view", tag, "--repo", repo], { + cwd: ROOT, + stdio: "ignore", + }); + if (result.error !== undefined) { + fail(`gh failed to start: ${result.error.message}`); + } + return result.status === 0; +} + +function ghJson(args) { + const output = runGh([...args, "--json", "assets"], { capture: true }); + try { + return JSON.parse(output); + } catch (error) { + fail(`gh ${args.join(" ")} returned malformed JSON: ${error.message}`); + } +} + +async function sha256(file) { + const digest = createHash("sha256"); + const input = Bun.file(file).stream(); + for await (const chunk of input) { + digest.update(chunk); + } + return digest.digest("hex"); +} + +function releaseAssetNames(tag, repo) { + const data = ghJson(["release", "view", tag, "--repo", repo]); + if ( + data === null || + typeof data !== "object" || + !Array.isArray(data.assets) + ) { + fail(`GitHub release ${tag} returned malformed asset metadata`); + } + return new Set( + data.assets + .filter((asset) => asset !== null && typeof asset === "object" && typeof asset.name === "string") + .map((asset) => asset.name), + ); +} + +function downloadReleaseAsset(tag, repo, assetName, destination) { + runGh(["release", "download", tag, "--pattern", assetName, "--dir", destination, "--repo", repo]); + const file = path.join(destination, assetName); + if (!existsSync(file)) { + fail(`failed to download existing GitHub release asset ${assetName}`); + } + return file; +} + +async function resolveAsset(asset) { + const relative = path.join(ROOT, asset); + if ((await isFile(relative))) { + return relative; + } + const direct = path.resolve(asset); + if ((await isFile(direct))) { + return direct; + } + fail(`release asset does not exist: ${asset}`); +} + +async function isFile(file) { + try { + return (await stat(file)).isFile(); + } catch { + return false; + } +} + +async function uploadReleaseAssets(product, tag, repo, assets) { + if (!releaseExists(tag, repo)) { + fail( + `${product} GitHub release ${tag} does not exist. ` + + "Run release-please before package-native publish steps.", + ); + } + + if (assets.length === 0) { + console.log(`${product} GitHub release ${tag} exists; no assets to upload.`); + return; + } + + const seenNames = new Set(); + const uploadAssets = []; + const existingNames = releaseAssetNames(tag, repo); + const tmp = mkdtempSync(path.join(tmpdir(), "oliphaunt-release-assets-")); + try { + for (const asset of assets) { + const assetPath = await resolveAsset(asset); + const assetName = path.basename(assetPath); + if (seenNames.has(assetName)) { + fail(`duplicate release asset name in upload set: ${assetName}`); + } + seenNames.add(assetName); + if (!existingNames.has(assetName)) { + uploadAssets.push(asset); + continue; + } + const existing = downloadReleaseAsset(tag, repo, assetName, tmp); + const [localSha, remoteSha] = await Promise.all([sha256(assetPath), sha256(existing)]); + if (localSha === remoteSha) { + console.log(`${product} GitHub release ${tag} already has identical asset ${assetName}; skipping.`); + continue; + } + fail( + `${product} GitHub release ${tag} already has different bytes for ${assetName}; ` + + "delete the conflicting GitHub release asset manually before rerunning an intentional repair", + ); + } + } finally { + rmSync(tmp, { recursive: true, force: true }); + } + + if (uploadAssets.length > 0) { + runGh(["release", "upload", tag, ...uploadAssets, "--repo", repo]); + } else { + console.log(`${product} GitHub release ${tag} already has all requested assets with matching checksums.`); + } +} + +const args = parseArgs(Bun.argv.slice(2)); +if (!args.repo) { + fail("--repo or GITHUB_REPOSITORY is required"); +} +const tag = args.tag || (await defaultTag(args.product)); +for (const asset of args.assets) { + await resolveAsset(asset); +} +await uploadReleaseAssets(args.product, tag, args.repo, args.assets); diff --git a/tools/release/upload_github_release_assets.py b/tools/release/upload_github_release_assets.py deleted file mode 100755 index b0a0aec1..00000000 --- a/tools/release/upload_github_release_assets.py +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env python3 -"""Upload assets to a product-scoped GitHub release created by release-please.""" - -from __future__ import annotations - -import argparse -import hashlib -import json -import os -import subprocess -import sys -from pathlib import Path -from tempfile import TemporaryDirectory -from typing import NoReturn - -import product_metadata - - -ROOT = Path(__file__).resolve().parents[2] - - -def fail(message: str) -> NoReturn: - print(f"upload_github_release_assets.py: {message}", file=sys.stderr) - raise SystemExit(1) - - -def default_tag(product: str) -> str: - prefix = product_metadata.tag_prefix(product) - return f"{prefix}{product_metadata.read_current_version(product)}" - - -def release_exists(tag: str, repo: str) -> bool: - result = subprocess.run( - ["gh", "release", "view", tag, "--repo", repo], - cwd=ROOT, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - check=False, - ) - return result.returncode == 0 - - -def run_gh(args: list[str]) -> None: - subprocess.run(["gh", *args], cwd=ROOT, check=True) - - -def gh_json(args: list[str]) -> object: - output = subprocess.check_output(["gh", *args, "--json", "assets"], cwd=ROOT, text=True) - return json.loads(output) - - -def sha256(path: Path) -> str: - digest = hashlib.sha256() - with path.open("rb") as handle: - for chunk in iter(lambda: handle.read(1024 * 1024), b""): - digest.update(chunk) - return digest.hexdigest() - - -def release_asset_names(tag: str, repo: str) -> set[str]: - data = gh_json(["release", "view", tag, "--repo", repo]) - if not isinstance(data, dict) or not isinstance(data.get("assets"), list): - fail(f"GitHub release {tag} returned malformed asset metadata") - return { - asset["name"] - for asset in data["assets"] - if isinstance(asset, dict) and isinstance(asset.get("name"), str) - } - - -def download_release_asset(tag: str, repo: str, asset_name: str, destination: Path) -> Path: - run_gh(["release", "download", tag, "--pattern", asset_name, "--dir", str(destination), "--repo", repo]) - path = destination / asset_name - if not path.is_file(): - fail(f"failed to download existing GitHub release asset {asset_name}") - return path - - -def upload_release_assets( - product: str, - tag: str, - repo: str, - assets: list[str], -) -> None: - if not release_exists(tag, repo): - fail( - f"{product} GitHub release {tag} does not exist. " - "Run release-please before package-native publish steps." - ) - if assets: - seen_names: set[str] = set() - upload_assets: list[str] = [] - existing_names = release_asset_names(tag, repo) - with TemporaryDirectory(prefix="oliphaunt-release-assets-") as tmp: - tmpdir = Path(tmp) - for asset in assets: - asset_path = ROOT / asset - if not asset_path.is_file(): - asset_path = Path(asset) - if not asset_path.is_file(): - fail(f"release asset does not exist: {asset}") - asset_name = asset_path.name - if asset_name in seen_names: - fail(f"duplicate release asset name in upload set: {asset_name}") - seen_names.add(asset_name) - if asset_name not in existing_names: - upload_assets.append(asset) - continue - existing = download_release_asset(tag, repo, asset_name, tmpdir) - local_sha = sha256(asset_path) - remote_sha = sha256(existing) - if local_sha == remote_sha: - print(f"{product} GitHub release {tag} already has identical asset {asset_name}; skipping.") - continue - fail( - f"{product} GitHub release {tag} already has different bytes for {asset_name}; " - "delete the conflicting GitHub release asset manually before rerunning an intentional repair" - ) - if upload_assets: - run_gh(["release", "upload", tag, *upload_assets, "--repo", repo]) - else: - print(f"{product} GitHub release {tag} already has all requested assets with matching checksums.") - else: - print(f"{product} GitHub release {tag} exists; no assets to upload.") - - -def parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("product", help="release product id") - parser.add_argument("--tag", help="release tag; defaults to the product tag prefix plus current version") - parser.add_argument( - "--repo", - default=os.environ.get("GITHUB_REPOSITORY", ""), - help="GitHub repository in owner/name form", - ) - parser.add_argument( - "--asset", - action="append", - default=[], - help="asset file to upload; may be passed more than once", - ) - return parser.parse_args(argv) - - -def main(argv: list[str]) -> int: - args = parse_args(argv) - if not args.repo: - fail("--repo or GITHUB_REPOSITORY is required") - assets = [str(Path(asset)) for asset in args.asset] - for asset in assets: - if not (ROOT / asset).is_file() and not Path(asset).is_file(): - fail(f"release asset does not exist: {asset}") - upload_release_assets( - product=args.product, - tag=args.tag or default_tag(args.product), - repo=args.repo, - assets=assets, - ) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) From 5ba51ecb64e92f07f3d8d46dc720ba4431cdd46a Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 18:18:06 +0000 Subject: [PATCH 109/137] chore: move native binary stripping to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 14 ++ .../tools/extension-artifact-packager.mjs | 8 +- .../node-direct/tools/build-node-addon.sh | 3 +- tools/policy/check-tooling-stack.sh | 17 ++ tools/policy/python-entrypoints.allowlist | 1 - .../optimize_native_runtime_payload.py | 14 +- tools/release/package-broker-assets.sh | 11 +- .../package-liboliphaunt-mobile-assets.sh | 4 +- .../release/strip_native_release_binaries.mjs | 222 ++++++++++++++++++ .../release/strip_native_release_binaries.py | 169 ------------- 10 files changed, 269 insertions(+), 194 deletions(-) create mode 100644 tools/release/strip_native_release_binaries.mjs delete mode 100644 tools/release/strip_native_release_binaries.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index ef3c5051..70046260 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -193,6 +193,20 @@ until the current-state gates here are checked with fresh local evidence. `python3 tools/policy/check-release-policy.py`, `python3 tools/release/check_release_metadata.py`, `python3 tools/release/check_consumer_shape.py`, and `git diff --check`. +- 2026-06-26: Native release binary stripping now uses + `tools/release/strip_native_release_binaries.mjs` from broker, mobile, + Node-direct, native extension, and runtime-payload optimization packaging + paths; the retired Python stripper was removed from the intentional Python + inventory, reducing it to 34 tracked files. A fake-strip smoke covered ELF + magic-byte classification, configured strip command invocation, changed-file + counting, empty-directory behavior, and missing-path failure. Fresh checks + passed: `bash tools/policy/check-tooling-stack.sh`, + `bash src/runtimes/node-direct/tools/check-package.sh check-static`, + `python3 tools/release/optimize_native_runtime_payload.py --help`, + `python3 tools/release/check_artifact_targets.py`, + `python3 tools/policy/check-release-policy.py`, + `python3 tools/release/check_release_metadata.py`, + `python3 tools/release/check_consumer_shape.py`, and `git diff --check`. - 2026-06-26: Mobile explicit runtime-directory validation now requires release-shaped `oliphaunt/runtime/files` proof before selected extensions are accepted on Kotlin Android and Swift native-direct; React Native forwards the diff --git a/src/extensions/artifacts/native/tools/extension-artifact-packager.mjs b/src/extensions/artifacts/native/tools/extension-artifact-packager.mjs index 6fc08dce..ad03224d 100755 --- a/src/extensions/artifacts/native/tools/extension-artifact-packager.mjs +++ b/src/extensions/artifacts/native/tools/extension-artifact-packager.mjs @@ -805,14 +805,10 @@ async function writeArtifactDirectory(artifactRoot, args) { await fs.writeFile(path.join(artifactRoot, 'manifest.properties'), manifest); } -function pythonCommand() { - return process.platform === 'win32' ? 'python' : 'python3'; -} - function stripNativeReleaseBinaries(artifactRoot) { const result = spawnSync( - pythonCommand(), - ['tools/release/strip_native_release_binaries.py', artifactRoot], + path.join(root, 'tools/dev/bun.sh'), + ['tools/release/strip_native_release_binaries.mjs', artifactRoot], { cwd: root, stdio: 'inherit' }, ); if (result.error !== undefined) { diff --git a/src/runtimes/node-direct/tools/build-node-addon.sh b/src/runtimes/node-direct/tools/build-node-addon.sh index 3f99dba1..8daa3893 100755 --- a/src/runtimes/node-direct/tools/build-node-addon.sh +++ b/src/runtimes/node-direct/tools/build-node-addon.sh @@ -17,7 +17,6 @@ require() { require node require npm require bun -require python3 require tar case "$(uname -s)" in @@ -169,7 +168,7 @@ case "$platform" in ;; esac -python3 tools/release/strip_native_release_binaries.py "$addon_file" +tools/dev/bun.sh tools/release/strip_native_release_binaries.mjs "$addon_file" node - "$addon" <<'JS' const addonPath = process.argv[2]; diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 7f56db9f..6f3f39bf 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -47,6 +47,7 @@ require_file src/sdks/react-native/tools/mobile-extension-artifact-paths.mjs require_file src/runtimes/liboliphaunt/wasix/assets/build/wasix-toml-value.mjs require_file src/extensions/artifacts/wasix/tools/package-release-assets.mjs require_file tools/release/cargo-crate-filename.mjs +require_file tools/release/strip_native_release_binaries.mjs require_file tools/dev/bun.sh require_file tools/dev/deno.sh require_file tools/dev/install-actionlint.sh @@ -269,6 +270,22 @@ grep -Fq 'package-release-assets.mjs' src/extensions/artifacts/wasix/tools/packa if grep -Fq 'python3' src/extensions/artifacts/wasix/tools/package-release-assets.sh; then fail "WASIX exact-extension release packager shell must use Bun instead of Python" fi +for native_strip_caller in \ + tools/release/package-broker-assets.sh \ + tools/release/package-liboliphaunt-mobile-assets.sh \ + src/runtimes/node-direct/tools/build-node-addon.sh \ + src/extensions/artifacts/native/tools/extension-artifact-packager.mjs \ + tools/release/optimize_native_runtime_payload.py +do + grep -Fq 'strip_native_release_binaries.mjs' "$native_strip_caller" || + fail "$native_strip_caller must use the Bun native binary stripper" +done +if git grep -n 'strip_native_release_binaries\.py' -- . ':!tools/policy/check-tooling-stack.sh' >/tmp/oliphaunt-native-strip-python-grep.$$ 2>/dev/null; then + cat /tmp/oliphaunt-native-strip-python-grep.$$ >&2 + rm -f /tmp/oliphaunt-native-strip-python-grep.$$ + fail "native release binary stripping must use the Bun helper" +fi +rm -f /tmp/oliphaunt-native-strip-python-grep.$$ grep -Fq 'bun src/sdks/rust/tools/cargo-artifact-patches.mjs' src/sdks/rust/tools/check-sdk.sh || fail "Rust SDK Cargo artifact patch generation must use the Bun helper" grep -Fq 'python3 tools/release/release.py prepare-rust-release-source' src/sdks/rust/tools/check-sdk.sh || diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index f18e1c9d..2f5c4f6f 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -31,7 +31,6 @@ tools/release/publish_swiftpm_source_tag.py tools/release/release.py tools/release/release_plan.py tools/release/render_swiftpm_release_package.py -tools/release/strip_native_release_binaries.py tools/release/sync_release_pr.py tools/release/verify_github_release_attestations.py tools/runtime/with-native-runtime-lock.py diff --git a/tools/release/optimize_native_runtime_payload.py b/tools/release/optimize_native_runtime_payload.py index 933aa35e..e3a16a40 100644 --- a/tools/release/optimize_native_runtime_payload.py +++ b/tools/release/optimize_native_runtime_payload.py @@ -13,8 +13,6 @@ from pathlib import Path, PurePosixPath from typing import Literal, NoReturn -import strip_native_release_binaries - ROOT = Path(__file__).resolve().parents[2] NATIVE_RUNTIME_TOOL_STEMS = ("initdb", "pg_ctl", "postgres") @@ -228,8 +226,16 @@ def strip_supported_for_target(target: str | None) -> bool: def strip_payload(root: Path) -> None: - result = strip_native_release_binaries.main([str(root)]) - if result != 0: + result = subprocess.run( + [ + str(ROOT / "tools/dev/bun.sh"), + "tools/release/strip_native_release_binaries.mjs", + str(root), + ], + cwd=ROOT, + check=False, + ) + if result.returncode != 0: fail(f"failed to strip native payload under {rel(root)}") diff --git a/tools/release/package-broker-assets.sh b/tools/release/package-broker-assets.sh index 42053389..2fb6af3a 100755 --- a/tools/release/package-broker-assets.sh +++ b/tools/release/package-broker-assets.sh @@ -20,15 +20,6 @@ fail() { command -v bun >/dev/null 2>&1 || fail "missing required command: bun" -python_bin="${PYTHON:-python3}" -if ! command -v "$python_bin" >/dev/null 2>&1; then - if command -v python >/dev/null 2>&1; then - python_bin=python - else - fail "missing required command: python3" - fi -fi - case "$host_os:$host_arch" in Darwin:arm64) target_id="macos-arm64" ;; Linux:x86_64|Linux:amd64) target_id="linux-x64-gnu" ;; @@ -63,7 +54,7 @@ cargo build -p oliphaunt-broker --release --locked cp "$broker_bin" "$stage/bin/$broker_stage_name" chmod 0755 "$stage/bin/$broker_stage_name" -"$python_bin" tools/release/strip_native_release_binaries.py "$stage" +tools/dev/bun.sh tools/release/strip_native_release_binaries.mjs "$stage" cat >"$stage/manifest.properties" < Stripping staged liboliphaunt Android $abi release binaries" - python3 tools/release/strip_native_release_binaries.py "$stage" + tools/dev/bun.sh tools/release/strip_native_release_binaries.mjs "$stage" archive_staged_dir "$stage" } @@ -115,7 +115,7 @@ package_ios() { mkdir -p "$stage_ios" rsync -a --delete "$ios_xcframework" "$stage_ios/" echo "==> Stripping staged liboliphaunt iOS release binaries" - python3 tools/release/strip_native_release_binaries.py "$stage_ios" + tools/dev/bun.sh tools/release/strip_native_release_binaries.mjs "$stage_ios" archive_staged_dir "$stage_ios" archive_swiftpm_xcframework \ diff --git a/tools/release/strip_native_release_binaries.mjs b/tools/release/strip_native_release_binaries.mjs new file mode 100644 index 00000000..543bdd8c --- /dev/null +++ b/tools/release/strip_native_release_binaries.mjs @@ -0,0 +1,222 @@ +#!/usr/bin/env bun +import { readdir, stat } from "node:fs/promises"; +import { accessSync, constants, existsSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import path from "node:path"; + +const MACHO_MAGICS = new Set([ + "feedface", + "cefaedfe", + "feedfacf", + "cffaedfe", + "cafebabe", + "bebafeca", +]); + +function fail(message) { + console.error(`strip_native_release_binaries.mjs: ${message}`); + process.exit(2); +} + +async function readPrefix(file, size = 8) { + try { + return Buffer.from(await Bun.file(file).slice(0, size).arrayBuffer()); + } catch (error) { + fail(`failed to read ${file}: ${error.message}`); + } +} + +async function classify(file) { + const prefix = await readPrefix(file); + if (prefix.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46]))) { + return { path: file, kind: "elf", archive: false }; + } + if (MACHO_MAGICS.has(prefix.subarray(0, 4).toString("hex"))) { + return { path: file, kind: "macho", archive: false }; + } + if (prefix.subarray(0, 2).toString("utf8") === "MZ") { + return { path: file, kind: "pe", archive: false }; + } + if (prefix.toString("utf8") === "!\n") { + return { path: file, kind: "archive", archive: true }; + } + return undefined; +} + +async function* iterFiles(roots) { + for (const root of roots) { + let info; + try { + info = await stat(root); + } catch { + fail(`input path does not exist: ${root}`); + } + if (info.isFile()) { + yield root; + continue; + } + if (!info.isDirectory()) { + fail(`input path does not exist: ${root}`); + } + yield* iterDirectory(root); + } +} + +async function* iterDirectory(root) { + const entries = (await readdir(root, { withFileTypes: true })).sort((left, right) => + left.name.localeCompare(right.name), + ); + for (const entry of entries) { + const entryPath = path.join(root, entry.name); + if (entry.isFile()) { + yield entryPath; + } else if (entry.isDirectory()) { + yield* iterDirectory(entryPath); + } + } +} + +function envTool(...names) { + for (const name of names) { + const value = process.env[name]; + if (value) { + return value; + } + } + return undefined; +} + +function isExecutable(file) { + try { + accessSync(file, constants.X_OK); + return true; + } catch { + return false; + } +} + +function findTool(...names) { + const paths = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean); + const extensions = + process.platform === "win32" + ? ["", ...(process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")] + : [""]; + for (const name of names) { + if (name.includes("/") || name.includes("\\")) { + if (isExecutable(name)) { + return name; + } + continue; + } + for (const directory of paths) { + for (const extension of extensions) { + const candidate = path.join(directory, `${name}${extension}`); + if (isExecutable(candidate)) { + return candidate; + } + } + } + } + return undefined; +} + +function darwinStripTool() { + const override = envTool("OLIPHAUNT_MACHO_STRIP", "OLIPHAUNT_STRIP"); + if (override) { + return override; + } + if (process.platform === "darwin") { + const result = spawnSync("xcrun", ["--find", "strip"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); + if (result.status === 0 && result.stdout.trim()) { + return result.stdout.trim(); + } + } + return findTool("strip"); +} + +function stripToolFor(native) { + if (native.kind === "macho") { + const tool = darwinStripTool(); + if (!tool) { + fail(`missing strip tool for Mach-O file ${native.path}`); + } + return { tool, flags: ["-S"] }; + } + if (native.kind === "pe") { + const tool = envTool("OLIPHAUNT_PE_STRIP", "OLIPHAUNT_STRIP") ?? findTool("llvm-strip", "strip"); + if (!tool) { + console.error(`skippedPeNativeFile=${native.path}`); + return undefined; + } + return { tool, flags: ["--strip-debug"] }; + } + if (native.archive && process.platform === "darwin") { + const tool = darwinStripTool(); + if (!tool) { + fail(`missing strip tool for archive ${native.path}`); + } + return { tool, flags: ["-S"] }; + } + if (native.archive && path.extname(native.path).toLowerCase() === ".lib") { + const tool = envTool("OLIPHAUNT_PE_STRIP", "OLIPHAUNT_STRIP") ?? findTool("llvm-strip", "strip"); + if (!tool) { + console.error(`skippedPeNativeFile=${native.path}`); + return undefined; + } + return { tool, flags: ["--strip-debug"] }; + } + const tool = envTool("OLIPHAUNT_ELF_STRIP", "OLIPHAUNT_STRIP") ?? findTool("llvm-strip", "strip"); + if (!tool) { + fail(`missing strip tool for ${native.kind} file ${native.path}`); + } + return { + tool, + flags: native.archive ? ["--strip-debug"] : ["--strip-unneeded"], + }; +} + +async function stripNative(native) { + const before = (await stat(native.path)).size; + const command = stripToolFor(native); + if (command === undefined) { + return false; + } + const result = spawnSync(command.tool, [...command.flags, native.path], { + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.error !== undefined) { + fail(`${command.tool} failed for ${native.path}: ${result.error.message}`); + } + if (result.status !== 0) { + const stderr = result.stderr.trim(); + fail(`${command.tool} failed for ${native.path}: ${stderr || `exit ${result.status}`}`); + } + return (await stat(native.path)).size !== before; +} + +const roots = Bun.argv.slice(2); +if (roots.length === 0) { + fail("usage: strip_native_release_binaries.mjs [path...]"); +} + +const nativeFiles = []; +for await (const file of iterFiles(roots)) { + const native = await classify(file); + if (native !== undefined) { + nativeFiles.push(native); + } +} + +let changed = 0; +for (const native of nativeFiles) { + if (await stripNative(native)) { + changed += 1; + } +} + +console.log(`strippedNativeFiles=${changed}`); +console.log(`checkedNativeFiles=${nativeFiles.length}`); diff --git a/tools/release/strip_native_release_binaries.py b/tools/release/strip_native_release_binaries.py deleted file mode 100644 index 13ddb47f..00000000 --- a/tools/release/strip_native_release_binaries.py +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/env python3 -"""Strip debug/symbol data from native release payloads before archiving.""" - -from __future__ import annotations - -import argparse -import os -import shutil -import subprocess -import sys -from dataclasses import dataclass -from pathlib import Path -from typing import Iterable, NoReturn - - -MACHO_MAGICS = { - b"\xfe\xed\xfa\xce", - b"\xce\xfa\xed\xfe", - b"\xfe\xed\xfa\xcf", - b"\xcf\xfa\xed\xfe", - b"\xca\xfe\xba\xbe", - b"\xbe\xba\xfe\xca", -} - - -@dataclass(frozen=True) -class NativeFile: - path: Path - kind: str - archive: bool = False - - -def fail(message: str) -> NoReturn: - print(f"strip_native_release_binaries.py: {message}", file=sys.stderr) - raise SystemExit(2) - - -def read_prefix(path: Path, size: int = 8) -> bytes: - try: - with path.open("rb") as handle: - return handle.read(size) - except OSError as error: - fail(f"failed to read {path}: {error}") - - -def classify(path: Path) -> NativeFile | None: - prefix = read_prefix(path) - if prefix.startswith(b"\x7fELF"): - return NativeFile(path, "elf") - if prefix[:4] in MACHO_MAGICS: - return NativeFile(path, "macho") - if prefix.startswith(b"MZ"): - return NativeFile(path, "pe") - if prefix.startswith(b"!\n"): - return NativeFile(path, "archive", archive=True) - return None - - -def iter_files(roots: Iterable[Path]) -> Iterable[Path]: - for root in roots: - if root.is_file(): - yield root - continue - if not root.is_dir(): - fail(f"input path does not exist: {root}") - for path in sorted(root.rglob("*")): - if path.is_file(): - yield path - - -def env_tool(*names: str) -> str | None: - for name in names: - value = os.environ.get(name) - if value: - return value - return None - - -def find_tool(*names: str) -> str | None: - for name in names: - resolved = shutil.which(name) - if resolved: - return resolved - return None - - -def darwin_strip_tool() -> str | None: - override = env_tool("OLIPHAUNT_MACHO_STRIP", "OLIPHAUNT_STRIP") - if override: - return override - if sys.platform == "darwin": - result = subprocess.run( - ["xcrun", "--find", "strip"], - check=False, - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - text=True, - ) - if result.returncode == 0 and result.stdout.strip(): - return result.stdout.strip() - return find_tool("strip") - - -def strip_tool_for(native: NativeFile) -> tuple[str | None, list[str]]: - if native.kind == "macho": - tool = darwin_strip_tool() - if not tool: - fail(f"missing strip tool for Mach-O file {native.path}") - return tool, ["-S"] - if native.kind == "pe": - tool = env_tool("OLIPHAUNT_PE_STRIP", "OLIPHAUNT_STRIP") or find_tool("llvm-strip", "strip") - if not tool: - print(f"skippedPeNativeFile={native.path}", file=sys.stderr) - return None, [] - return tool, ["--strip-debug"] - if native.archive and sys.platform == "darwin": - tool = darwin_strip_tool() - if not tool: - fail(f"missing strip tool for archive {native.path}") - return tool, ["-S"] - if native.archive and native.path.suffix.lower() == ".lib": - tool = env_tool("OLIPHAUNT_PE_STRIP", "OLIPHAUNT_STRIP") or find_tool("llvm-strip", "strip") - if not tool: - print(f"skippedPeNativeFile={native.path}", file=sys.stderr) - return None, [] - return tool, ["--strip-debug"] - tool = env_tool("OLIPHAUNT_ELF_STRIP", "OLIPHAUNT_STRIP") or find_tool("llvm-strip", "strip") - if not tool: - fail(f"missing strip tool for {native.kind} file {native.path}") - if native.archive: - return tool, ["--strip-debug"] - return tool, ["--strip-unneeded"] - - -def strip_native(native: NativeFile) -> bool: - before = native.path.stat().st_size - tool, flags = strip_tool_for(native) - if tool is None: - return False - result = subprocess.run( - [tool, *flags, str(native.path)], - check=False, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - if result.returncode != 0: - stderr = result.stderr.strip() - fail(f"{tool} failed for {native.path}: {stderr or f'exit {result.returncode}'}") - return native.path.stat().st_size != before - - -def main(argv: list[str]) -> int: - parser = argparse.ArgumentParser() - parser.add_argument("paths", nargs="+", type=Path) - args = parser.parse_args(argv) - - native_files = [native for path in iter_files(args.paths) if (native := classify(path)) is not None] - changed = 0 - for native in native_files: - if strip_native(native): - changed += 1 - print(f"strippedNativeFiles={changed}") - print(f"checkedNativeFiles={len(native_files)}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) From bd3717ad6aa037929fd823d1cbd623780fd4cbc3 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 18:32:34 +0000 Subject: [PATCH 110/137] chore: move broker cargo artifact packaging to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 11 + examples/README.md | 2 +- src/sdks/rust/tools/check-sdk.sh | 2 +- tools/policy/check-tooling-stack.sh | 15 + tools/policy/python-entrypoints.allowlist | 1 - tools/release/check_artifact_targets.py | 2 +- tools/release/check_release_metadata.py | 2 +- tools/release/local_registry_publish.py | 4 +- .../package_broker_cargo_artifacts.mjs | 324 ++++++++++++++++++ .../release/package_broker_cargo_artifacts.py | 261 -------------- tools/release/release.py | 15 +- 11 files changed, 365 insertions(+), 274 deletions(-) create mode 100644 tools/release/package_broker_cargo_artifacts.mjs delete mode 100755 tools/release/package_broker_cargo_artifacts.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 70046260..df5ebe03 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -621,6 +621,17 @@ until the current-state gates here are checked with fresh local evidence. `tools/release/cargo-crate-filename.mjs` instead of an inline Python TOML parser. The unused inline workspace-exclusion Python helper was removed, and `check-tooling-stack.sh` rejects drift back to either path. +- Broker Cargo artifact packaging now uses + `tools/release/package_broker_cargo_artifacts.mjs` through pinned Bun from + release orchestration, local registry publishing, and the Rust SDK + package-shape relay fixture. The retired Python packager was removed from the + explicit Python entrypoint inventory, which now contains 33 tracked files. + On 2026-06-26, focused validation passed with + `check-tooling-stack.sh`, `check_release_metadata.py`, + `check_artifact_targets.py`, `check_consumer_shape.py`, + `check-sdk.sh package-shape`, `check-release-policy.py`, and + `git diff --cached --check`; the package-shape lane generated and validated + broker Cargo crates for all four release targets through the Bun path. - Rust helper inventory is currently limited to `tools/xtask` and `tools/perf/runner`. Both remain Rust-owned for now: `xtask` owns WASIX asset parsing, archive/hash work, AOT/template feature-gated paths, and release diff --git a/examples/README.md b/examples/README.md index 308432ee..5d93fb84 100644 --- a/examples/README.md +++ b/examples/README.md @@ -24,7 +24,7 @@ python3 tools/release/package_liboliphaunt_cargo_artifacts.py \ --asset-dir target/local-registry-artifacts/liboliphaunt-native-release-assets-linux-x64-gnu \ --output-dir target/local-registry-generated/liboliphaunt-native-cargo \ --target linux-x64-gnu -python3 tools/release/package_broker_cargo_artifacts.py \ +tools/dev/bun.sh tools/release/package_broker_cargo_artifacts.mjs \ --asset-dir target/local-registry-artifacts/oliphaunt-broker-release-assets-linux-x64-gnu \ --output-dir target/local-registry-generated/broker-cargo \ --target linux-x64-gnu diff --git a/src/sdks/rust/tools/check-sdk.sh b/src/sdks/rust/tools/check-sdk.sh index fb55f4ea..8cb4d13f 100755 --- a/src/sdks/rust/tools/check-sdk.sh +++ b/src/sdks/rust/tools/check-sdk.sh @@ -173,7 +173,7 @@ check_broker_cargo_relay_fixture() { --part-bytes 1048576 cargo_artifacts="$(prepare_scratch_dir broker-cargo-artifacts)" - run python3 tools/release/package_broker_cargo_artifacts.py \ + run tools/dev/bun.sh tools/release/package_broker_cargo_artifacts.mjs \ --asset-dir "$fixture_assets" \ --output-dir "$cargo_artifacts" \ --version "$broker_version" diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 6f3f39bf..2f4021e1 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -48,6 +48,7 @@ require_file src/runtimes/liboliphaunt/wasix/assets/build/wasix-toml-value.mjs require_file src/extensions/artifacts/wasix/tools/package-release-assets.mjs require_file tools/release/cargo-crate-filename.mjs require_file tools/release/strip_native_release_binaries.mjs +require_file tools/release/package_broker_cargo_artifacts.mjs require_file tools/dev/bun.sh require_file tools/dev/deno.sh require_file tools/dev/install-actionlint.sh @@ -286,6 +287,20 @@ if git grep -n 'strip_native_release_binaries\.py' -- . ':!tools/policy/check-to fail "native release binary stripping must use the Bun helper" fi rm -f /tmp/oliphaunt-native-strip-python-grep.$$ +for broker_cargo_caller in \ + tools/release/release.py \ + tools/release/local_registry_publish.py \ + src/sdks/rust/tools/check-sdk.sh +do + grep -Fq 'package_broker_cargo_artifacts.mjs' "$broker_cargo_caller" || + fail "$broker_cargo_caller must use the Bun broker Cargo artifact packager" +done +if git grep -n 'package_broker_cargo_artifacts\.py' -- . ':!tools/policy/check-tooling-stack.sh' >/tmp/oliphaunt-broker-cargo-python-grep.$$ 2>/dev/null; then + cat /tmp/oliphaunt-broker-cargo-python-grep.$$ >&2 + rm -f /tmp/oliphaunt-broker-cargo-python-grep.$$ + fail "broker Cargo artifact packaging must use the Bun helper" +fi +rm -f /tmp/oliphaunt-broker-cargo-python-grep.$$ grep -Fq 'bun src/sdks/rust/tools/cargo-artifact-patches.mjs' src/sdks/rust/tools/check-sdk.sh || fail "Rust SDK Cargo artifact patch generation must use the Bun helper" grep -Fq 'python3 tools/release/release.py prepare-rust-release-source' src/sdks/rust/tools/check-sdk.sh || diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index 2f5c4f6f..fb7d6f0a 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -23,7 +23,6 @@ tools/release/check_staged_artifacts.py tools/release/extension_artifact_targets.py tools/release/local_registry_publish.py tools/release/optimize_native_runtime_payload.py -tools/release/package_broker_cargo_artifacts.py tools/release/package_liboliphaunt_cargo_artifacts.py tools/release/package_liboliphaunt_wasix_cargo_artifacts.py tools/release/product_metadata.py diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py index f8138505..a3c3d72d 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -884,7 +884,7 @@ def validate_ci_release_artifacts() -> None: ) require_text( "tools/release/release.py", - "package_broker_cargo_artifacts.py", + "package_broker_cargo_artifacts.mjs", "broker Cargo artifact packages must be generated from staged broker release assets", ) require_text( diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index e3696228..45c56d44 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -370,7 +370,7 @@ def validate_local_registry_publisher() -> None: if ( "def stage_release_asset_cargo_packages" not in publisher or "package_liboliphaunt_cargo_artifacts.py" not in publisher - or "package_broker_cargo_artifacts.py" not in publisher + or "package_broker_cargo_artifacts.mjs" not in publisher or "package_liboliphaunt_wasix_cargo_artifacts.py" not in publisher or "host_cargo_release_target()" not in publisher or "stage_release_asset_cargo_packages(roots, registry_root, dry_run, result)" not in publisher diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index 89eb666c..519a6f02 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -2418,8 +2418,8 @@ def stage_release_asset_cargo_packages( ) run( [ - "python3", - "tools/release/package_broker_cargo_artifacts.py", + str(ROOT / "tools/dev/bun.sh"), + "tools/release/package_broker_cargo_artifacts.mjs", "--version", broker_version, "--output-dir", diff --git a/tools/release/package_broker_cargo_artifacts.mjs b/tools/release/package_broker_cargo_artifacts.mjs new file mode 100644 index 00000000..5409e515 --- /dev/null +++ b/tools/release/package_broker_cargo_artifacts.mjs @@ -0,0 +1,324 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { chmod, copyFile, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises"; +import path from "node:path"; + +const ROOT = path.resolve(import.meta.dir, "../.."); +const PRODUCT = "oliphaunt-broker"; +const CRATES_IO_MAX_BYTES = 10 * 1024 * 1024; +const TARGETS = ["linux-arm64-gnu", "linux-x64-gnu", "macos-arm64", "windows-x64-msvc"]; + +function fail(message) { + console.error(`package_broker_cargo_artifacts.mjs: ${message}`); + process.exit(1); +} + +function rel(file) { + const relative = path.relative(ROOT, file); + return relative.startsWith("..") ? file : relative; +} + +function usage() { + fail( + "usage: package_broker_cargo_artifacts.mjs [--asset-dir DIR] [--output-dir DIR] [--target TARGET]... [--version VERSION]", + ); +} + +function optionValue(argv, index) { + const value = argv[index + 1]; + if (value === undefined || value.startsWith("--")) { + usage(); + } + return value; +} + +async function parseArgs(argv) { + const args = { + assetDir: "target/oliphaunt-broker/release-assets", + outputDir: "target/oliphaunt-broker/cargo-artifacts", + targets: [], + version: undefined, + }; + let index = 0; + while (index < argv.length) { + const arg = argv[index]; + if (arg === "--asset-dir") { + args.assetDir = optionValue(argv, index); + index += 2; + } else if (arg === "--output-dir") { + args.outputDir = optionValue(argv, index); + index += 2; + } else if (arg === "--target") { + args.targets.push(optionValue(argv, index)); + index += 2; + } else if (arg === "--version") { + args.version = optionValue(argv, index); + index += 2; + } else { + usage(); + } + } + return { + assetDir: repoPath(args.assetDir), + outputDir: repoPath(args.outputDir), + targets: args.targets, + version: args.version ?? (await currentVersion()), + }; +} + +function repoPath(value) { + return path.isAbsolute(value) ? value : path.join(ROOT, value); +} + +async function currentVersion() { + const manifest = JSON.parse(await readFile(path.join(ROOT, ".release-please-manifest.json"), "utf8")); + const version = manifest["src/runtimes/broker"]; + if (typeof version !== "string" || version.length === 0) { + fail(".release-please-manifest.json is missing src/runtimes/broker"); + } + return version; +} + +function cargoPackageName(targetId) { + return `${PRODUCT}-${targetId}`; +} + +function cargoLinksName(targetId) { + return `oliphaunt_artifact_broker_${targetId.replaceAll("-", "_")}`; +} + +function sourceCrateDir(targetId) { + return path.join(ROOT, "src/runtimes/broker/crates", targetId); +} + +async function isDirectory(file) { + try { + return (await stat(file)).isDirectory(); + } catch { + return false; + } +} + +async function isFile(file) { + try { + return (await stat(file)).isFile(); + } catch { + return false; + } +} + +function run(args, options = {}) { + console.log(`\n==> ${args.join(" ")}`); + const result = spawnSync(args[0], args.slice(1), { + cwd: options.cwd ?? ROOT, + env: options.env ?? process.env, + encoding: options.encoding ?? "utf8", + stdio: options.capture ? ["ignore", "pipe", "pipe"] : "inherit", + }); + if (result.error !== undefined) { + fail(`${args[0]} failed to start: ${result.error.message}`); + } + if (result.status !== 0) { + if (options.capture) { + process.stderr.write(result.stderr); + } + process.exit(result.status ?? 1); + } + return result.stdout ?? ""; +} + +async function extractMember(archivePath, memberName, destination) { + const candidates = [memberName, `./${memberName}`]; + let data; + for (const candidate of candidates) { + const command = archivePath.endsWith(".zip") + ? ["unzip", "-p", archivePath, candidate] + : ["tar", "-xOf", archivePath, candidate]; + const result = spawnSync(command[0], command.slice(1), { + cwd: ROOT, + encoding: "buffer", + stdio: ["ignore", "pipe", "pipe"], + maxBuffer: 32 * 1024 * 1024, + }); + if (result.error !== undefined) { + fail(`${command[0]} failed to start: ${result.error.message}`); + } + if (result.status === 0) { + data = result.stdout; + break; + } + } + if (data === undefined) { + fail(`${rel(archivePath)} is missing ${memberName}`); + } + await mkdir(path.dirname(destination), { recursive: true }); + await writeFile(destination, data); +} + +function targetFromSource(targetId, version) { + return { + target: targetId, + packageName: cargoPackageName(targetId), + sourceDir: sourceCrateDir(targetId), + archiveName: `${PRODUCT}-${version}-${targetId}.${targetId === "windows-x64-msvc" ? "zip" : "tar.gz"}`, + }; +} + +async function copySourceCrate(target, crateDir, version) { + if (!(await isDirectory(target.sourceDir))) { + fail(`${target.target} source Cargo artifact crate is missing: ${rel(target.sourceDir)}`); + } + await rm(crateDir, { recursive: true, force: true }); + run(["cp", "-R", target.sourceDir, crateDir]); + const cargoTomlPath = path.join(crateDir, "Cargo.toml"); + const cargoToml = await readFile(cargoTomlPath, "utf8"); + const metadata = Bun.TOML.parse(cargoToml); + const expectedLinks = cargoLinksName(target.target); + if (metadata?.package?.name !== target.packageName) { + fail(`${rel(path.join(target.sourceDir, "Cargo.toml"))} has package.name=${JSON.stringify(metadata?.package?.name)}, expected ${target.packageName}`); + } + if (metadata?.package?.version !== version) { + fail(`${rel(path.join(target.sourceDir, "Cargo.toml"))} has package.version=${JSON.stringify(metadata?.package?.version)}, expected ${version}`); + } + if (metadata?.package?.links !== expectedLinks) { + fail(`${rel(path.join(target.sourceDir, "Cargo.toml"))} has package.links=${JSON.stringify(metadata?.package?.links)}, expected ${expectedLinks}`); + } + if (metadata?.package?.build !== "build.rs") { + fail(`${rel(path.join(target.sourceDir, "Cargo.toml"))} must declare build = "build.rs"`); + } + if (!Array.isArray(metadata?.package?.include) || !metadata.package.include.includes("payload/**")) { + fail(`${rel(path.join(target.sourceDir, "Cargo.toml"))} must include "payload/**"`); + } + + const libRsPath = path.join(crateDir, "src/lib.rs"); + const libRs = await readFile(libRsPath, "utf8"); + const constants = Object.fromEntries( + [...libRs.matchAll(/pub const ([A-Z_]+): &str = "([^"]+)";/g)].map((match) => [match[1], match[2]]), + ); + for (const [key, value] of Object.entries({ + PRODUCT, + KIND: "broker-helper", + RELEASE_TARGET: target.target, + })) { + if (constants[key] !== value) { + fail(`${rel(path.join(target.sourceDir, "src/lib.rs"))} has ${key}=${JSON.stringify(constants[key])}, expected ${value}`); + } + } + if (typeof constants.CARGO_TARGET !== "string" || constants.CARGO_TARGET.length === 0) { + fail(`${rel(path.join(target.sourceDir, "src/lib.rs"))} must declare CARGO_TARGET`); + } + if (typeof constants.EXECUTABLE_RELATIVE_PATH !== "string" || constants.EXECUTABLE_RELATIVE_PATH.length === 0) { + fail(`${rel(path.join(target.sourceDir, "src/lib.rs"))} must declare EXECUTABLE_RELATIVE_PATH`); + } + target.executableRelativePath = constants.EXECUTABLE_RELATIVE_PATH; +} + +async function sha256File(file) { + const digest = createHash("sha256"); + for await (const chunk of Bun.file(file).stream()) { + digest.update(chunk); + } + return digest.digest("hex"); +} + +async function validateCrate(cratePath, packageName, version, payloadMember) { + if (!(await isFile(cratePath))) { + fail(`missing generated Cargo crate ${rel(cratePath)}`); + } + const size = (await stat(cratePath)).size; + if (size > CRATES_IO_MAX_BYTES) { + fail(`${rel(cratePath)} is ${size} bytes, above the crates.io 10 MiB package limit`); + } + const expected = new Set([ + `${packageName}-${version}/Cargo.toml`, + `${packageName}-${version}/README.md`, + `${packageName}-${version}/build.rs`, + `${packageName}-${version}/src/lib.rs`, + `${packageName}-${version}/payload/sha256`, + `${packageName}-${version}/payload/${payloadMember}`, + ]); + const names = new Set(run(["tar", "-tzf", cratePath], { capture: true }).split(/\r?\n/).filter(Boolean)); + const missing = [...expected].filter((name) => !names.has(name)).sort(); + if (missing.length > 0) { + fail(`${rel(cratePath)} is missing package members: ${missing.join(", ")}`); + } +} + +async function packageTarget(target, { version, assetDir, sourceRoot, outputDir, cargoTargetDir }) { + const crateDir = path.join(sourceRoot, target.packageName); + await copySourceCrate(target, crateDir, version); + const archive = path.join(assetDir, target.archiveName); + if (!(await isFile(archive))) { + fail(`missing broker release asset: ${rel(archive)}`); + } + const payload = path.join(crateDir, "payload", target.executableRelativePath); + await extractMember(archive, target.executableRelativePath, payload); + if ((await stat(payload)).size <= 0) { + fail(`${rel(payload)} must be a non-empty broker helper payload`); + } + await chmod(payload, 0o755); + await writeFile(path.join(crateDir, "payload/sha256"), `${await sha256File(payload)}\n`, "utf8"); + run( + [ + "cargo", + "package", + "--manifest-path", + path.join(crateDir, "Cargo.toml"), + "--target-dir", + cargoTargetDir, + "--allow-dirty", + ], + { env: { ...process.env, OLIPHAUNT_ARTIFACT_CRATE_REQUIRE_PAYLOAD: "1" } }, + ); + const packaged = path.join(cargoTargetDir, "package", `${target.packageName}-${version}.crate`); + const output = path.join(outputDir, path.basename(packaged)); + await copyFile(packaged, output); + await validateCrate(output, target.packageName, version, target.executableRelativePath); + return output; +} + +async function main() { + const args = await parseArgs(Bun.argv.slice(2)); + if (!(await isDirectory(args.assetDir))) { + fail(`broker release asset directory does not exist: ${rel(args.assetDir)}`); + } + const sourceRoot = path.join(ROOT, "target/oliphaunt-broker/cargo-package-sources"); + const cargoTargetDir = path.join(ROOT, "target/oliphaunt-broker/cargo-package-target"); + await rm(sourceRoot, { recursive: true, force: true }); + await rm(args.outputDir, { recursive: true, force: true }); + await rm(cargoTargetDir, { recursive: true, force: true }); + await mkdir(sourceRoot, { recursive: true }); + await mkdir(args.outputDir, { recursive: true }); + + let targets = TARGETS.map((target) => targetFromSource(target, args.version)); + if (args.targets.length > 0) { + const selected = new Set(args.targets); + const known = new Set(TARGETS); + const unknown = [...selected].filter((target) => !known.has(target)).sort(); + if (unknown.length > 0) { + fail(`unsupported broker target(s): ${unknown.join(", ")}`); + } + targets = targets.filter((target) => selected.has(target.target)); + } + + const outputs = []; + for (const target of targets) { + outputs.push( + await packageTarget(target, { + version: args.version, + assetDir: args.assetDir, + sourceRoot, + outputDir: args.outputDir, + cargoTargetDir, + }), + ); + } + + console.log("generated broker Cargo artifact crates:"); + for (const output of outputs) { + console.log(rel(output)); + } +} + +await main(); diff --git a/tools/release/package_broker_cargo_artifacts.py b/tools/release/package_broker_cargo_artifacts.py deleted file mode 100755 index 74f64a8d..00000000 --- a/tools/release/package_broker_cargo_artifacts.py +++ /dev/null @@ -1,261 +0,0 @@ -#!/usr/bin/env python3 -"""Package oliphaunt-broker helper binaries as Cargo artifact crates.""" - -from __future__ import annotations - -import argparse -import hashlib -import os -import shutil -import subprocess -import sys -import tarfile -import zipfile -from pathlib import Path -from typing import NoReturn - -import artifact_targets -import product_metadata - - -ROOT = Path(__file__).resolve().parents[2] -PRODUCT = "oliphaunt-broker" -KIND = "broker-helper" -SURFACE = "rust-broker" -CRATES_IO_MAX_BYTES = 10 * 1024 * 1024 - - -def fail(message: str) -> NoReturn: - print(f"package_broker_cargo_artifacts.py: {message}", file=sys.stderr) - raise SystemExit(1) - - -def rel(path: Path) -> str: - try: - return path.relative_to(ROOT).as_posix() - except ValueError: - return str(path) - - -def run(args: list[str], *, cwd: Path = ROOT, env: dict[str, str] | None = None) -> None: - print("\n==> " + " ".join(args), flush=True) - result = subprocess.run(args, cwd=cwd, env=env, check=False) - if result.returncode != 0: - raise SystemExit(result.returncode) - - -def sha256_file(path: Path) -> str: - digest = hashlib.sha256() - with path.open("rb") as handle: - for chunk in iter(lambda: handle.read(1024 * 1024), b""): - digest.update(chunk) - return digest.hexdigest() - - -def cargo_package_name(target_id: str) -> str: - return f"oliphaunt-broker-{target_id}" - - -def cargo_links_name(target_id: str) -> str: - return f"oliphaunt_artifact_broker_{target_id.replace('-', '_')}" - - -def source_crate_dir(target_id: str) -> Path: - return ROOT / "src" / "runtimes" / "broker" / "crates" / target_id - - -def extract_member(archive_path: Path, member_name: str, destination: Path) -> None: - destination.parent.mkdir(parents=True, exist_ok=True) - if archive_path.name.endswith(".zip"): - try: - with zipfile.ZipFile(archive_path) as archive: - if member_name not in archive.namelist(): - fail(f"{rel(archive_path)} is missing {member_name}") - destination.write_bytes(archive.read(member_name)) - except zipfile.BadZipFile as error: - fail(f"{rel(archive_path)} is not a readable zip archive: {error}") - return - - try: - with tarfile.open(archive_path, "r:*") as archive: - member = archive.getmember(member_name) - if not member.isfile(): - fail(f"{rel(archive_path)} member {member_name} must be a regular file") - extracted = archive.extractfile(member) - if extracted is None: - fail(f"{rel(archive_path)} member {member_name} could not be read") - with extracted: - destination.write_bytes(extracted.read()) - destination.chmod(member.mode & 0o777) - except KeyError: - fail(f"{rel(archive_path)} is missing {member_name}") - except tarfile.TarError as error: - fail(f"{rel(archive_path)} is not a readable tar archive: {error}") - -def copy_source_crate(target: artifact_targets.ArtifactTarget, crate_dir: Path, version: str) -> None: - source_dir = source_crate_dir(target.target) - if not source_dir.is_dir(): - fail(f"{target.id} source Cargo artifact crate is missing: {rel(source_dir)}") - shutil.copytree(source_dir, crate_dir) - cargo_toml = (crate_dir / "Cargo.toml").read_text(encoding="utf-8") - expected_name = cargo_package_name(target.target) - expected_links = cargo_links_name(target.target) - for required in [ - f'name = "{expected_name}"', - f'version = "{version}"', - f'links = "{expected_links}"', - 'build = "build.rs"', - '"payload/**"', - ]: - if required not in cargo_toml: - fail(f"{rel(source_dir / 'Cargo.toml')} is missing {required!r}") - lib_rs = (crate_dir / "src" / "lib.rs").read_text(encoding="utf-8") - for required in [ - f'RELEASE_TARGET: &str = "{target.target}"', - f'CARGO_TARGET: &str = "{target.triple}"', - f'EXECUTABLE_RELATIVE_PATH: &str = "{target.executable_relative_path}"', - ]: - if required not in lib_rs: - fail(f"{rel(source_dir / 'src/lib.rs')} is missing {required!r}") - - -def validate_crate(crate_path: Path, package_name: str, version: str, payload_member: str) -> None: - if not crate_path.is_file(): - fail(f"missing generated Cargo crate {rel(crate_path)}") - size = crate_path.stat().st_size - if size > CRATES_IO_MAX_BYTES: - fail(f"{rel(crate_path)} is {size} bytes, above the crates.io 10 MiB package limit") - expected = { - f"{package_name}-{version}/Cargo.toml", - f"{package_name}-{version}/README.md", - f"{package_name}-{version}/build.rs", - f"{package_name}-{version}/src/lib.rs", - f"{package_name}-{version}/payload/sha256", - f"{package_name}-{version}/payload/{payload_member}", - } - try: - with tarfile.open(crate_path, "r:gz") as archive: - names = set(archive.getnames()) - except tarfile.TarError as error: - fail(f"{rel(crate_path)} is not a readable .crate archive: {error}") - missing = sorted(expected - names) - if missing: - fail(f"{rel(crate_path)} is missing package members: {', '.join(missing)}") - - -def package_target( - target: artifact_targets.ArtifactTarget, - *, - version: str, - asset_dir: Path, - source_root: Path, - output_dir: Path, - cargo_target_dir: Path, -) -> Path: - if target.triple is None: - fail(f"{target.id} must declare a Cargo target triple") - if target.executable_relative_path is None: - fail(f"{target.id} must declare executable_relative_path") - package_name = cargo_package_name(target.target) - crate_dir = source_root / package_name - copy_source_crate(target, crate_dir, version) - archive = asset_dir / target.asset_name(version) - payload = crate_dir / "payload" / target.executable_relative_path - extract_member(archive, target.executable_relative_path, payload) - if payload.stat().st_size <= 0: - fail(f"{rel(payload)} must be a non-empty broker helper payload") - payload.chmod(0o755) - payload_sha256 = sha256_file(payload) - (crate_dir / "payload" / "sha256").write_text(payload_sha256 + "\n", encoding="utf-8") - env = {**os.environ, "OLIPHAUNT_ARTIFACT_CRATE_REQUIRE_PAYLOAD": "1"} - run( - [ - "cargo", - "package", - "--manifest-path", - str(crate_dir / "Cargo.toml"), - "--target-dir", - str(cargo_target_dir), - "--allow-dirty", - ], - env=env, - ) - packaged = cargo_target_dir / "package" / f"{package_name}-{version}.crate" - output = output_dir / packaged.name - shutil.copy2(packaged, output) - validate_crate(output, package_name, version, target.executable_relative_path) - return output - - -def parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--asset-dir", - default="target/oliphaunt-broker/release-assets", - help="directory containing checked oliphaunt-broker release assets", - ) - parser.add_argument( - "--output-dir", - default="target/oliphaunt-broker/cargo-artifacts", - help="directory where generated .crate files are written", - ) - parser.add_argument( - "--target", - action="append", - default=[], - help="release target id to package, such as linux-x64-gnu; may be passed more than once", - ) - parser.add_argument("--version", default=product_metadata.read_current_version(PRODUCT)) - return parser.parse_args(argv) - - -def main(argv: list[str]) -> int: - args = parse_args(argv) - asset_dir = Path(args.asset_dir) - output_dir = Path(args.output_dir) - if not asset_dir.is_absolute(): - asset_dir = ROOT / asset_dir - if not output_dir.is_absolute(): - output_dir = ROOT / output_dir - if not asset_dir.is_dir(): - fail(f"broker release asset directory does not exist: {rel(asset_dir)}") - source_root = ROOT / "target" / "oliphaunt-broker" / "cargo-package-sources" - cargo_target_dir = ROOT / "target" / "oliphaunt-broker" / "cargo-package-target" - shutil.rmtree(source_root, ignore_errors=True) - shutil.rmtree(output_dir, ignore_errors=True) - shutil.rmtree(cargo_target_dir, ignore_errors=True) - source_root.mkdir(parents=True, exist_ok=True) - output_dir.mkdir(parents=True, exist_ok=True) - - outputs = [] - targets = artifact_targets.artifact_targets( - product=PRODUCT, - kind=KIND, - surface=SURFACE, - published_only=True, - ) - if args.target: - selected_targets = set(args.target) - unknown = selected_targets - {target.target for target in targets} - if unknown: - fail("unsupported broker target(s): " + ", ".join(sorted(unknown))) - targets = [target for target in targets if target.target in selected_targets] - for target in targets: - outputs.append( - package_target( - target, - version=args.version, - asset_dir=asset_dir, - source_root=source_root, - output_dir=output_dir, - cargo_target_dir=cargo_target_dir, - ) - ) - print("generated broker Cargo artifact crates:") - for path in outputs: - print(rel(path)) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) diff --git a/tools/release/release.py b/tools/release/release.py index 40e50402..1bb2a778 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -21,7 +21,6 @@ import check_cratesio_publication import extension_artifact_targets import optimize_native_runtime_payload -import package_broker_cargo_artifacts import package_liboliphaunt_cargo_artifacts import package_liboliphaunt_wasix_cargo_artifacts import product_metadata @@ -442,6 +441,10 @@ def extension_sql_name(product: str) -> str: return value +def broker_cargo_package_name(target_id: str) -> str: + return f"oliphaunt-broker-{target_id}" + + def current_product_version(product: str) -> str: return product_metadata.read_current_version(product) @@ -656,7 +659,7 @@ def render_oliphaunt_release_cargo_toml(source: str, native_version: str, broker surface="rust-broker", published_only=True, ): - crate = package_broker_cargo_artifacts.cargo_package_name(target.target) + crate = broker_cargo_package_name(target.target) cfg = rust_artifact_cargo_target_cfg(target) target_dependencies.setdefault(cfg, []).append(f'{crate} = {{ version = "={broker_version}" }}') for cfg in sorted(target_dependencies): @@ -808,7 +811,7 @@ def prepare_oliphaunt_release_source(version: str) -> Path: surface="rust-broker", published_only=True, ): - crate = package_broker_cargo_artifacts.cargo_package_name(target.target) + crate = broker_cargo_package_name(target.target) if f'{crate} = {{ version = "={broker_version}" }}' not in rendered: fail(f"generated oliphaunt release source is missing broker artifact dependency {crate}") return cargo_toml @@ -2646,8 +2649,8 @@ def broker_cargo_artifact_crates(version: str) -> list[tuple[str, Path, Path]]: output_dir = ROOT / "target" / "oliphaunt-broker" / "cargo-artifacts" run( [ - "python3", - "tools/release/package_broker_cargo_artifacts.py", + "tools/dev/bun.sh", + "tools/release/package_broker_cargo_artifacts.mjs", "--version", version, "--output-dir", @@ -2657,7 +2660,7 @@ def broker_cargo_artifact_crates(version: str) -> list[tuple[str, Path, Path]]: packages: list[tuple[str, Path, Path]] = [] source_root = ROOT / "target" / "oliphaunt-broker" / "cargo-package-sources" expected_crates = { - package_broker_cargo_artifacts.cargo_package_name(target.target) + broker_cargo_package_name(target.target) for target in artifact_targets.artifact_targets( product="oliphaunt-broker", kind="broker-helper", From a3476e2eb2ddb9d5520d2c4a07461c0cb91775f3 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 18:40:08 +0000 Subject: [PATCH 111/137] chore: move product version reads to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 10 + src/sdks/rust/tools/check-sdk.sh | 2 +- tools/policy/check-tooling-stack.sh | 26 +++ tools/release/package-broker-assets.sh | 2 +- .../package-liboliphaunt-aggregate-assets.sh | 2 +- .../package-liboliphaunt-linux-assets.sh | 2 +- .../package-liboliphaunt-macos-assets.sh | 2 +- .../package-liboliphaunt-mobile-assets.sh | 2 +- .../package-liboliphaunt-windows-assets.ps1 | 2 +- tools/release/product-version.mjs | 195 ++++++++++++++++++ 10 files changed, 238 insertions(+), 7 deletions(-) create mode 100644 tools/release/product-version.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index df5ebe03..69901b56 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -632,6 +632,16 @@ until the current-state gates here are checked with fresh local evidence. `check-sdk.sh package-shape`, `check-release-policy.py`, and `git diff --cached --check`; the package-shape lane generated and validated broker Cargo crates for all four release targets through the Bun path. +- Release asset packagers now use `tools/release/product-version.mjs` for + version-only release-please reads instead of invoking + `product_metadata.py version` from shell/PowerShell and the Rust SDK + package-shape broker fixture. The Bun helper resolves canonical + release-please version files for raw, Cargo, npm/JSR, and Gradle products. + On 2026-06-26, it matched the Python helper for all 49 release products, and + focused validation passed with `check-tooling-stack.sh`, + `check_release_metadata.py`, `check_artifact_targets.py`, + `check_consumer_shape.py`, `check-sdk.sh package-shape`, and + `check-release-policy.py`. - Rust helper inventory is currently limited to `tools/xtask` and `tools/perf/runner`. Both remain Rust-owned for now: `xtask` owns WASIX asset parsing, archive/hash work, AOT/template feature-gated paths, and release diff --git a/src/sdks/rust/tools/check-sdk.sh b/src/sdks/rust/tools/check-sdk.sh index 8cb4d13f..bf786dd9 100755 --- a/src/sdks/rust/tools/check-sdk.sh +++ b/src/sdks/rust/tools/check-sdk.sh @@ -110,7 +110,7 @@ check_release_asset_fixture() { } check_broker_release_asset_fixture() { - broker_version="$(python3 tools/release/product_metadata.py version oliphaunt-broker)" + broker_version="$(tools/dev/bun.sh tools/release/product-version.mjs version oliphaunt-broker)" fixture_assets="$(prepare_scratch_dir broker-release-assets)" fixture_cache="$(prepare_scratch_dir broker-release-cache)" fixture_output="$(prepare_scratch_dir broker-release-output)" diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 2f4021e1..f4a6c49e 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -47,6 +47,7 @@ require_file src/sdks/react-native/tools/mobile-extension-artifact-paths.mjs require_file src/runtimes/liboliphaunt/wasix/assets/build/wasix-toml-value.mjs require_file src/extensions/artifacts/wasix/tools/package-release-assets.mjs require_file tools/release/cargo-crate-filename.mjs +require_file tools/release/product-version.mjs require_file tools/release/strip_native_release_binaries.mjs require_file tools/release/package_broker_cargo_artifacts.mjs require_file tools/dev/bun.sh @@ -287,6 +288,31 @@ if git grep -n 'strip_native_release_binaries\.py' -- . ':!tools/policy/check-to fail "native release binary stripping must use the Bun helper" fi rm -f /tmp/oliphaunt-native-strip-python-grep.$$ +for product_version_caller in \ + tools/release/package-broker-assets.sh \ + tools/release/package-liboliphaunt-aggregate-assets.sh \ + tools/release/package-liboliphaunt-linux-assets.sh \ + tools/release/package-liboliphaunt-macos-assets.sh \ + tools/release/package-liboliphaunt-mobile-assets.sh \ + tools/release/package-liboliphaunt-windows-assets.ps1 \ + src/sdks/rust/tools/check-sdk.sh +do + grep -Fq 'tools/release/product-version.mjs version' "$product_version_caller" || + fail "$product_version_caller must use the Bun product version helper" +done +if git grep -n 'product_metadata\.py version' -- \ + tools/release/package-broker-assets.sh \ + tools/release/package-liboliphaunt-aggregate-assets.sh \ + tools/release/package-liboliphaunt-linux-assets.sh \ + tools/release/package-liboliphaunt-macos-assets.sh \ + tools/release/package-liboliphaunt-mobile-assets.sh \ + tools/release/package-liboliphaunt-windows-assets.ps1 \ + src/sdks/rust/tools/check-sdk.sh >/tmp/oliphaunt-product-version-python-grep.$$ 2>/dev/null; then + cat /tmp/oliphaunt-product-version-python-grep.$$ >&2 + rm -f /tmp/oliphaunt-product-version-python-grep.$$ + fail "release asset version-only reads must use the Bun helper" +fi +rm -f /tmp/oliphaunt-product-version-python-grep.$$ for broker_cargo_caller in \ tools/release/release.py \ tools/release/local_registry_publish.py \ diff --git a/tools/release/package-broker-assets.sh b/tools/release/package-broker-assets.sh index 2fb6af3a..9ba7ef3a 100755 --- a/tools/release/package-broker-assets.sh +++ b/tools/release/package-broker-assets.sh @@ -7,7 +7,7 @@ root="$(git rev-parse --show-toplevel 2>/dev/null)" || { } cd "$root" -version="$(python3 tools/release/product_metadata.py version oliphaunt-broker)" +version="$(tools/dev/bun.sh tools/release/product-version.mjs version oliphaunt-broker)" out_dir="${OLIPHAUNT_BROKER_RELEASE_ASSETS:-$root/target/oliphaunt-broker/release-assets}" stage_root="$root/target/oliphaunt-broker/release-stage" host_os="$(uname -s)" diff --git a/tools/release/package-liboliphaunt-aggregate-assets.sh b/tools/release/package-liboliphaunt-aggregate-assets.sh index 318444b0..fa838a1d 100755 --- a/tools/release/package-liboliphaunt-aggregate-assets.sh +++ b/tools/release/package-liboliphaunt-aggregate-assets.sh @@ -15,7 +15,7 @@ fail() { asset_dir="${OLIPHAUNT_LIBOLIPHAUNT_RELEASE_ASSETS:-target/liboliphaunt/release-assets}" [ -d "$asset_dir" ] || fail "missing liboliphaunt release asset directory: $asset_dir" -version="$(python3 tools/release/product_metadata.py version liboliphaunt-native)" +version="$(tools/dev/bun.sh tools/release/product-version.mjs version liboliphaunt-native)" checksum_file="$asset_dir/liboliphaunt-${version}-release-assets.sha256" tools/release/write_checksum_manifest.mjs \ diff --git a/tools/release/package-liboliphaunt-linux-assets.sh b/tools/release/package-liboliphaunt-linux-assets.sh index 609731be..29fa7b01 100755 --- a/tools/release/package-liboliphaunt-linux-assets.sh +++ b/tools/release/package-liboliphaunt-linux-assets.sh @@ -40,7 +40,7 @@ require cargo require bun require python3 -version="$(python3 tools/release/product_metadata.py version liboliphaunt-native)" +version="$(tools/dev/bun.sh tools/release/product-version.mjs version liboliphaunt-native)" out_dir="${OLIPHAUNT_LIBOLIPHAUNT_RELEASE_ASSETS:-$root/target/liboliphaunt/release-assets}" stage_root="$root/target/liboliphaunt/release-stage-$target_id" work_root="${OLIPHAUNT_LINUX_WORK_ROOT:-$root/target/liboliphaunt-pg18-$target_id}" diff --git a/tools/release/package-liboliphaunt-macos-assets.sh b/tools/release/package-liboliphaunt-macos-assets.sh index 289918e9..949293ff 100755 --- a/tools/release/package-liboliphaunt-macos-assets.sh +++ b/tools/release/package-liboliphaunt-macos-assets.sh @@ -30,7 +30,7 @@ case "$(uname -m)" in *) fail "unsupported macOS architecture $(uname -m)" ;; esac -version="$(python3 tools/release/product_metadata.py version liboliphaunt-native)" +version="$(tools/dev/bun.sh tools/release/product-version.mjs version liboliphaunt-native)" command -v bun >/dev/null 2>&1 || fail "missing required command: bun" out_dir="${OLIPHAUNT_LIBOLIPHAUNT_RELEASE_ASSETS:-$root/target/liboliphaunt/release-assets}" stage_root="$root/target/liboliphaunt/release-stage-$target_id" diff --git a/tools/release/package-liboliphaunt-mobile-assets.sh b/tools/release/package-liboliphaunt-mobile-assets.sh index 7fa44fcd..1e75220e 100755 --- a/tools/release/package-liboliphaunt-mobile-assets.sh +++ b/tools/release/package-liboliphaunt-mobile-assets.sh @@ -36,7 +36,7 @@ if [ "$target_id" = "ios-xcframework" ]; then require ditto fi -version="$(python3 tools/release/product_metadata.py version liboliphaunt-native)" +version="$(tools/dev/bun.sh tools/release/product-version.mjs version liboliphaunt-native)" out_dir="${OLIPHAUNT_LIBOLIPHAUNT_RELEASE_ASSETS:-$root/target/liboliphaunt/release-assets}" stage_root="${OLIPHAUNT_LIBOLIPHAUNT_RELEASE_STAGE_ROOT:-$root/target/liboliphaunt/release-stage-$target_id}" headers_dir="$root/src/runtimes/liboliphaunt/native/include" diff --git a/tools/release/package-liboliphaunt-windows-assets.ps1 b/tools/release/package-liboliphaunt-windows-assets.ps1 index 54941ade..eefd7013 100644 --- a/tools/release/package-liboliphaunt-windows-assets.ps1 +++ b/tools/release/package-liboliphaunt-windows-assets.ps1 @@ -70,7 +70,7 @@ if ($env:OLIPHAUNT_RELEASE_FETCH_ASSETS -ne "0") { } } -$Version = python tools/release/product_metadata.py version liboliphaunt-native +$Version = bun tools/release/product-version.mjs version liboliphaunt-native if ($LASTEXITCODE -ne 0 -or -not $Version) { Fail "failed to read liboliphaunt version" } diff --git a/tools/release/product-version.mjs b/tools/release/product-version.mjs new file mode 100644 index 00000000..5766d577 --- /dev/null +++ b/tools/release/product-version.mjs @@ -0,0 +1,195 @@ +#!/usr/bin/env bun +import { readFile } from "node:fs/promises"; +import path from "node:path"; + +const ROOT = path.resolve(import.meta.dir, "../.."); +const CONFIG_PATH = path.join(ROOT, "release-please-config.json"); + +function fail(message) { + console.error(`product-version.mjs: ${message}`); + process.exit(2); +} + +async function readJson(file) { + let text; + try { + text = await readFile(file, "utf8"); + } catch { + fail(`missing ${rel(file)}`); + } + const value = JSON.parse(text); + if (value === null || Array.isArray(value) || typeof value !== "object") { + fail(`${rel(file)} must contain a JSON object`); + } + return value; +} + +function rel(file) { + const relative = path.relative(ROOT, file); + return relative.startsWith("..") ? file : relative; +} + +function usage() { + fail("usage: tools/release/product-version.mjs version "); +} + +function assertRelativePath(value, context) { + if (typeof value !== "string" || value.length === 0) { + fail(`${context} must be a non-empty string`); + } + if (path.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value) || value.split(/[\\/]/).includes("..")) { + fail(`${context} must stay inside release package path: ${JSON.stringify(value)}`); + } + return value; +} + +async function findPackageConfig(product) { + const config = await readJson(CONFIG_PATH); + const packages = config.packages; + if (packages === null || Array.isArray(packages) || typeof packages !== "object") { + fail("release-please-config.json must define packages"); + } + let foundPath; + let foundConfig; + for (const [packagePath, packageConfig] of Object.entries(packages)) { + if (packageConfig === null || Array.isArray(packageConfig) || typeof packageConfig !== "object") { + fail(`${packagePath} release-please config must be an object`); + } + if (packageConfig.component === product) { + if (foundPath !== undefined) { + fail(`duplicate release-please component ${product}`); + } + foundPath = assertRelativePath(packagePath, `${product}.packagePath`); + foundConfig = packageConfig; + } + } + if (foundPath === undefined || foundConfig === undefined) { + fail(`unknown release product ${JSON.stringify(product)}`); + } + return { packagePath: foundPath, packageConfig: foundConfig }; +} + +function packageRelativePath(packagePath, relative, context) { + return path.join(assertRelativePath(packagePath, `${context}.packagePath`), assertRelativePath(relative, context)); +} + +function canonicalVersionFile(product, packagePath, packageConfig) { + const versionFile = packageConfig["version-file"]; + if (typeof versionFile === "string" && versionFile.length > 0) { + return packageRelativePath(packagePath, versionFile, `${product}.version-file`); + } + const releaseType = packageConfig["release-type"]; + if (releaseType === "rust") { + return packageRelativePath(packagePath, "Cargo.toml", `${product}.rust`); + } + if (releaseType === "node" || releaseType === "expo") { + return packageRelativePath(packagePath, "package.json", `${product}.node`); + } + fail(`${product} release-please config must declare version-file for release type ${JSON.stringify(releaseType)}`); +} + +function parserForVersionFile(product, file) { + const name = path.basename(file); + if (name === "Cargo.toml") { + return "cargo"; + } + if (name === "package.json" || name === "jsr.json") { + return "json:version"; + } + if (name === "gradle.properties") { + return "gradle:VERSION_NAME"; + } + if (name === "VERSION" || name === "LIBOLIPHAUNT_VERSION") { + return "raw"; + } + fail(`${product}.version_files has unsupported version file type: ${file}`); +} + +function parseJsonPath(text, dotted) { + let value = JSON.parse(text); + for (const key of dotted.split(".")) { + if (value === null || Array.isArray(value) || typeof value !== "object" || !(key in value)) { + return ""; + } + value = value[key]; + } + return String(value); +} + +function parseTomlPath(text, dotted) { + let value = Bun.TOML.parse(text); + for (const key of dotted.split(".")) { + if (value === null || Array.isArray(value) || typeof value !== "object" || !(key in value)) { + return ""; + } + value = value[key]; + } + return String(value); +} + +function parseGradleProperty(text, name) { + for (const rawLine of text.split(/\r?\n/)) { + const line = rawLine.trim(); + if (line.length === 0 || line.startsWith("#") || !line.includes("=")) { + continue; + } + const [key, ...rest] = line.split("="); + if (key.trim() === name) { + return rest.join("=").trim(); + } + } + return ""; +} + +function parseVersionText(text, file, parser) { + if (parser === "raw") { + return text.trim(); + } + if (parser === "cargo") { + return parseTomlPath(text, "package.version"); + } + if (parser.startsWith("gradle:")) { + return parseGradleProperty(text, parser.slice("gradle:".length)); + } + if (parser.startsWith("json:")) { + return parseJsonPath(text, parser.slice("json:".length)); + } + if (parser.startsWith("toml:")) { + return parseTomlPath(text, parser.slice("toml:".length)); + } + fail(`unknown version parser ${JSON.stringify(parser)} for ${file}`); +} + +function ensureSemver(product, version) { + if (!/^[0-9]+[.][0-9]+[.][0-9]+(?:[-+][0-9A-Za-z][0-9A-Za-z.-]*)?$/.test(version)) { + fail(`${product} version is not semver-like: ${JSON.stringify(version)}`); + } + return version; +} + +async function currentVersion(product) { + const { packagePath, packageConfig } = await findPackageConfig(product); + const versionFile = canonicalVersionFile(product, packagePath, packageConfig); + const parser = parserForVersionFile(product, versionFile); + const file = path.join(ROOT, versionFile); + let text; + try { + text = await readFile(file, "utf8"); + } catch { + fail(`${product} version file does not exist: ${versionFile}`); + } + const version = parseVersionText(text, versionFile, parser); + if (!version) { + fail(`${versionFile} does not define a release version for ${product}`); + } + return ensureSemver(product, version); +} + +async function main(argv) { + if (argv.length !== 2 || argv[0] !== "version") { + usage(); + } + console.log(await currentVersion(argv[1])); +} + +await main(Bun.argv.slice(2)); From 895ed8d24d2760b77d91165ccf6135a598b85b3e Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 18:47:48 +0000 Subject: [PATCH 112/137] chore: move moon affected helper to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 8 ++ tools/graph/affected.mjs | 80 +++++++++++++++++++ tools/graph/affected.py | 64 --------------- tools/graph/ci_plan.py | 15 +++- tools/graph/graph.py | 17 +++- tools/policy/check-repo-structure.sh | 6 +- tools/policy/check-tooling-stack.sh | 11 ++- tools/policy/python-entrypoints.allowlist | 1 - 8 files changed, 129 insertions(+), 73 deletions(-) create mode 100644 tools/graph/affected.mjs delete mode 100755 tools/graph/affected.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 69901b56..aced8ecb 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -642,6 +642,14 @@ until the current-state gates here are checked with fresh local evidence. `check_release_metadata.py`, `check_artifact_targets.py`, `check_consumer_shape.py`, `check-sdk.sh package-shape`, and `check-release-policy.py`. +- Moon affectedness discovery now uses `tools/graph/affected.mjs` instead of the + retired Python helper. The CI planner calls the Bun helper for pull-request + affected project/task selection, while `graph.py` keeps only local result + normalization for its own Moon queries. On 2026-06-26, validation passed with + the direct Bun helper smoke, pull-request-mode `ci_plan.py` smoke, + `graph.py check`, `check-tooling-stack.sh`, `check-repo-structure.sh`, + `check_artifact_targets.py`, and `check-release-policy.py`; the intentional + Python inventory now contains 32 tracked files. - Rust helper inventory is currently limited to `tools/xtask` and `tools/perf/runner`. Both remain Rust-owned for now: `xtask` owns WASIX asset parsing, archive/hash work, AOT/template feature-gated paths, and release diff --git a/tools/graph/affected.mjs b/tools/graph/affected.mjs new file mode 100644 index 00000000..22420e06 --- /dev/null +++ b/tools/graph/affected.mjs @@ -0,0 +1,80 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import path from "node:path"; + +const ROOT = path.resolve(import.meta.dir, "../.."); + +function fail(message) { + console.error(`affected.mjs: ${message}`); + process.exit(2); +} + +function moonBin() { + if (process.env.MOON_BIN) { + return process.env.MOON_BIN; + } + const protoMoon = path.join(process.env.HOME ?? "", ".proto/bin/moon"); + return existsSync(protoMoon) ? protoMoon : "moon"; +} + +function moon(args) { + const result = spawnSync(moonBin(), args, { + cwd: ROOT, + env: process.env, + encoding: "utf8", + stdio: ["ignore", "pipe", "inherit"], + }); + if (result.error !== undefined) { + fail(`failed to run moon: ${result.error.message}`); + } + if (result.status !== 0) { + process.exit(result.status ?? 1); + } + try { + return JSON.parse(result.stdout); + } catch (error) { + fail(`moon query did not return JSON: ${error.message}`); + } +} + +function names(value) { + if (value !== null && !Array.isArray(value) && typeof value === "object") { + return Object.keys(value).sort(); + } + if (Array.isArray(value)) { + const result = new Set(); + for (const item of value) { + if (typeof item === "string") { + result.add(item); + } else if (item !== null && typeof item === "object") { + const identifier = item.id ?? item.target; + if (identifier !== undefined && identifier !== null && identifier !== "") { + result.add(String(identifier)); + } + } + } + return [...result].sort(); + } + return []; +} + +function affectedSummary() { + const direct = moon(["query", "affected", "--upstream", "none", "--downstream", "none"]); + const downstream = moon(["query", "affected", "--upstream", "none", "--downstream", "deep"]); + return { + directProjects: names(direct.projects), + projects: names(downstream.projects), + directTasks: names(direct.tasks), + }; +} + +function usage() { + fail("usage: tools/graph/affected.mjs summary"); +} + +const [command] = Bun.argv.slice(2); +if (command !== "summary") { + usage(); +} +console.log(JSON.stringify(affectedSummary())); diff --git a/tools/graph/affected.py b/tools/graph/affected.py deleted file mode 100755 index 79f7c091..00000000 --- a/tools/graph/affected.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python3 -"""Shared Moon affectedness helpers for local and GitHub CI planners.""" - -from __future__ import annotations - -import json -import os -import subprocess -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] - - -def moon_bin() -> str: - if configured := os.environ.get("MOON_BIN"): - return configured - proto_moon = Path.home() / ".proto" / "bin" / "moon" - return str(proto_moon) if proto_moon.exists() else "moon" - - -def moon(args: list[str]) -> dict[str, object]: - command = [moon_bin(), *args] - env = dict(os.environ) - output = subprocess.check_output(command, cwd=ROOT, env=env, text=True) - return json.loads(output) - - -def names(value: object) -> set[str]: - if isinstance(value, dict): - return {str(key) for key in value} - if isinstance(value, list): - result: set[str] = set() - for item in value: - if isinstance(item, str): - result.add(item) - elif isinstance(item, dict): - identifier = item.get("id") or item.get("target") - if identifier: - result.add(str(identifier)) - return result - return set() - - -def affected_projects_and_tasks() -> tuple[set[str], set[str], set[str]]: - direct = moon(["query", "affected", "--upstream", "none", "--downstream", "none"]) - downstream = moon(["query", "affected", "--upstream", "none", "--downstream", "deep"]) - direct_projects = names(direct.get("projects")) - direct_tasks = names(direct.get("tasks")) - projects = names(downstream.get("projects")) - return direct_projects, projects, direct_tasks - - -def project_task_targets(projects: set[str], task_name: str) -> list[str]: - queried = moon(["query", "tasks"]) - tasks_by_project = queried.get("tasks") - if not isinstance(tasks_by_project, dict): - raise RuntimeError("moon query tasks did not return a tasks object") - - targets: list[str] = [] - for project in sorted(projects): - project_tasks = tasks_by_project.get(project) - if isinstance(project_tasks, dict) and task_name in project_tasks: - targets.append(f"{project}:{task_name}") - return targets diff --git a/tools/graph/ci_plan.py b/tools/graph/ci_plan.py index f28f23b2..c4130479 100644 --- a/tools/graph/ci_plan.py +++ b/tools/graph/ci_plan.py @@ -20,7 +20,6 @@ sys.path.insert(0, str(ROOT / "tools" / "release")) import artifact_target_matrix # noqa: E402 -from affected import affected_projects_and_tasks # noqa: E402 BASE_JOBS = {"affected"} @@ -100,6 +99,20 @@ def moon(args: list[str]) -> dict[str, object]: return json.loads(output) +def affected_projects_and_tasks() -> tuple[set[str], set[str], set[str]]: + output = subprocess.check_output( + ["tools/dev/bun.sh", "tools/graph/affected.mjs", "summary"], + cwd=ROOT, + text=True, + ) + summary = json.loads(output) + return ( + {str(value) for value in summary.get("directProjects", [])}, + {str(value) for value in summary.get("projects", [])}, + {str(value) for value in summary.get("directTasks", [])}, + ) + + def moon_ci_job_targets() -> dict[str, list[str]]: queried = moon(["query", "tasks"]) tasks_by_project = queried.get("tasks") diff --git a/tools/graph/graph.py b/tools/graph/graph.py index c5ebd59e..4d445c83 100755 --- a/tools/graph/graph.py +++ b/tools/graph/graph.py @@ -40,7 +40,6 @@ sys.path.insert(0, str(ROOT / "tools" / "release")) sys.path.insert(0, str(ROOT / "tools" / "graph")) import release_plan # noqa: E402 -from affected import names as affected_names # noqa: E402 from ci_plan import CI_JOB_TARGETS, CI_JOBS_CONFIG, plan_jobs_for_affected # noqa: E402 @@ -73,6 +72,22 @@ def run_moon(args: list[str], *, stdin: str | None = None) -> dict[str, Any]: return json.loads(output) +def affected_names(value: object) -> set[str]: + if isinstance(value, dict): + return {str(key) for key in value} + if isinstance(value, list): + result: set[str] = set() + for item in value: + if isinstance(item, str): + result.add(item) + elif isinstance(item, dict): + identifier = item.get("id") or item.get("target") + if identifier: + result.add(str(identifier)) + return result + return set() + + def moon_projects() -> list[dict[str, Any]]: data = run_moon(["query", "projects"]) projects = data.get("projects") diff --git a/tools/policy/check-repo-structure.sh b/tools/policy/check-repo-structure.sh index 9fbbd002..63d3404a 100755 --- a/tools/policy/check-repo-structure.sh +++ b/tools/policy/check-repo-structure.sh @@ -371,6 +371,7 @@ reject_tracked_under tools/graph/moon.mjs reject_tracked_under tools/graph/tool-versions.mjs reject_tracked_under tools/graph/tool_versions.py reject_tracked_under tools/graph/run-affected-task.py +reject_tracked_under tools/graph/affected.py reject_tracked_under tools/policy/check-source-inputs.sh reject_tracked_under tools/policy/check-source-inputs.mjs require_file tools/policy/assertions/assert-source-inputs.mjs @@ -540,8 +541,9 @@ reject_path .github/scripts/run-moon-ci.sh reject_text .github/scripts/run-affected-moon-task.sh 'pnpm moon' reject_text .github/scripts/select-affected-moon-targets.mjs 'pnpm moon' reject_text .github/scripts/run-moon-targets.sh 'pnpm moon' -require_text tools/graph/affected.py 'moon(["query", "affected", "--upstream", "none", "--downstream", "none"])' -require_text tools/graph/affected.py 'moon(["query", "affected", "--upstream", "none", "--downstream", "deep"])' +require_text tools/graph/affected.mjs 'moon(["query", "affected", "--upstream", "none", "--downstream", "none"])' +require_text tools/graph/affected.mjs 'moon(["query", "affected", "--upstream", "none", "--downstream", "deep"])' +require_text tools/graph/ci_plan.py 'tools/graph/affected.mjs' reject_path tools/graph/jobs.toml reject_path tools/release/release-inputs.toml require_text tools/graph/ci_plan.py 'moon_ci_job_targets' diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index f4a6c49e..bc1e5ae8 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -173,6 +173,9 @@ for retired_moon_helper in tools/graph/moon.mjs tools/graph/tool-versions.mjs to fail "retired Moon helper must not exist: $retired_moon_helper" fi done +if git ls-files --error-unmatch tools/graph/affected.py >/dev/null 2>&1; then + fail "Moon affectedness helper must use Bun instead of Python" +fi for catalog_dep in '@vitest/coverage-v8' 'tsx' 'typedoc' 'typescript' 'vitest'; do grep -Eq "^[[:space:]]+\"?$catalog_dep\"?:" pnpm-workspace.yaml || fail "pnpm-workspace.yaml must catalog shared JS test/build tool $catalog_dep" @@ -217,12 +220,12 @@ grep -Fq 'bun --version' .github/actions/setup-moon/action.yml || if grep -Fq -- '--affected --downstream deep' package.json; then fail "root package scripts must not carry affected Moon aliases" fi -grep -Fq 'moon(["query", "affected", "--upstream", "none", "--downstream", "none"])' tools/graph/affected.py || +grep -Fq 'moon(["query", "affected", "--upstream", "none", "--downstream", "none"])' tools/graph/affected.mjs || fail "affected runner must get direct affected projects from Moon" -grep -Fq 'moon(["query", "affected", "--upstream", "none", "--downstream", "deep"])' tools/graph/affected.py || +grep -Fq 'moon(["query", "affected", "--upstream", "none", "--downstream", "deep"])' tools/graph/affected.mjs || fail "affected runner must get downstream affected projects from Moon" -grep -Fq 'moon(["query", "tasks"])' tools/graph/affected.py || - fail "affected runner must discover task availability from Moon" +grep -Fq 'tools/graph/affected.mjs' tools/graph/ci_plan.py || + fail "CI planner must use the Bun affectedness helper" grep -Fq 'tools/dev/bun.sh' tools/dev/doctor.sh || fail "pnpm doctor must report the pinned Bun launcher used by TypeScript SDK checks" grep -Fq 'https://github.com/oven-sh/bun/releases/download/bun-v$version/$asset' tools/dev/bun.sh || diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index fb7d6f0a..6c13b58f 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -2,7 +2,6 @@ # New Python files should be ported to Bun or deliberately added here. src/extensions/tools/check-extension-model.py tools/coverage/coverage.py -tools/graph/affected.py tools/graph/ci_plan.py tools/graph/graph.py tools/policy/check-final-source-architecture.py From ec5df2a94ee5581a81461c3bcceed9f333f2fe09 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 18:57:43 +0000 Subject: [PATCH 113/137] docs: record fresh example e2e validation --- docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index aced8ecb..536af73c 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -69,12 +69,14 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence - 2026-06-26: `git status --short --branch` was clean on - `f0rr0/reduce-oliphaunt-icu-crate-size` at commit `6ced470`. + `f0rr0/reduce-oliphaunt-icu-crate-size` at commit `895ed8d` before the fresh + example e2e run. - 2026-06-26: Current-state example e2e re-run passed against the staged local - registries: `examples/tools/run-electron-driver-smoke.sh examples/electron`, - `examples/tools/run-electron-driver-smoke.sh examples/electron-wasix`, - `examples/tools/run-tauri-webdriver-smoke.sh examples/tauri`, and - `examples/tools/run-tauri-webdriver-smoke.sh examples/tauri-wasix`. + registries from commit `895ed8d`: `examples/tools/run-electron-driver-smoke.sh + examples/electron`, `examples/tools/run-electron-driver-smoke.sh + examples/electron-wasix`, `examples/tools/run-tauri-webdriver-smoke.sh + examples/tauri`, and `examples/tools/run-tauri-webdriver-smoke.sh + examples/tauri-wasix`. Native Electron verified `@oliphaunt/ts`, `@oliphaunt/liboliphaunt-linux-x64-gnu`, `@oliphaunt/tools-linux-x64-gnu`, and `@oliphaunt/extension-hstore` from From c549de2f87e666f9a15000e59c48caa6329dbe06 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 19:05:28 +0000 Subject: [PATCH 114/137] test: enforce native tools package split --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 5 ++ tools/release/check_consumer_shape.py | 52 ++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 536af73c..9b842c5f 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -101,6 +101,11 @@ until the current-state gates here are checked with fresh local evidence. and `postgres`; native `oliphaunt-tools-*` carrying `pg_dump` and `psql`; WASIX root carrying only `initdb` plus runtime/template payloads; and `oliphaunt-wasix-tools` carrying `pg_dump.wasix.wasm` and `psql.wasix.wasm`. +- 2026-06-26: Native root/tools npm descriptor checks now read + `publishConfig.executableFiles` directly. Root package descriptors must list + only `initdb`, `pg_ctl`, and `postgres`; split `@oliphaunt/tools-*` + descriptors must list only `pg_dump` and `psql`, including Windows `.exe` + variants. Fresh check passed: `python3 tools/release/check_consumer_shape.py`. - 2026-06-26: Rechecked the split tools model against current local-registry artifacts. Native `liboliphaunt-0.1.0-linux-x64-gnu.tar.gz` contains `runtime/bin/initdb`, `runtime/bin/pg_ctl`, and `runtime/bin/postgres`; diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index c8aac7d5..f088d9fc 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -297,6 +297,44 @@ def liboliphaunt_native_expected_registry_packages() -> set[str]: } +def native_npm_tool_split_failures( + root: str, + *, + tool_set: optimize_native_runtime_payload.NativeToolSet, +) -> list[str]: + failures: list[str] = [] + for package_json_path in sorted((ROOT / root).glob("*/package.json")): + path = relative(package_json_path) + package = read_json(path) + metadata = package.get("oliphaunt", {}) + target = metadata.get("target") if isinstance(metadata, dict) else None + if not isinstance(target, str) or not target: + failures.append(f"{path}: missing oliphaunt.target") + continue + publish_config = package.get("publishConfig", {}) + executable_files = ( + publish_config.get("executableFiles") if isinstance(publish_config, dict) else None + ) + if not isinstance(executable_files, list) or not all( + isinstance(item, str) for item in executable_files + ): + failures.append(f"{path}: publishConfig.executableFiles={executable_files!r}") + continue + if tool_set == "runtime": + expected_tools = optimize_native_runtime_payload.required_runtime_tools(target) + elif tool_set == "tools": + expected_tools = optimize_native_runtime_payload.required_tools_package_tools(target) + else: + fail(f"unsupported native npm tool split check: {tool_set}") + expected = {f"./runtime/bin/{tool}" for tool in expected_tools} + actual = set(executable_files) + if actual != expected: + failures.append( + f"{path}: expected executableFiles={sorted(expected)!r}, got {sorted(actual)!r}" + ) + return failures + + def broker_expected_registry_packages() -> set[str]: targets = artifact_targets.artifact_targets( product="oliphaunt-broker", @@ -414,6 +452,14 @@ def check_liboliphaunt(findings: list[Finding]) -> None: native_optimizer = read_text("tools/release/optimize_native_runtime_payload.py") release_cli = read_text("tools/release/release.py") local_registry_publisher = read_text("tools/release/local_registry_publish.py") + native_runtime_package_split_failures = native_npm_tool_split_failures( + "src/runtimes/liboliphaunt/native/packages", + tool_set="runtime", + ) + native_tools_package_split_failures = native_npm_tool_split_failures( + "src/runtimes/liboliphaunt/native/tools-packages", + tool_set="tools", + ) require( findings, product, @@ -433,12 +479,16 @@ def check_liboliphaunt(findings: list[Finding]) -> None: and "ensure_native_tools_absent_from_runtime" in release_cli and 'oliphaunt-tools-{lib_version}-*' in local_registry_publisher and "NATIVE_RUNTIME_TOOL_STEMS" in native_optimizer - and "NATIVE_TOOLS_TOOL_STEMS" in native_optimizer, + and "NATIVE_TOOLS_TOOL_STEMS" in native_optimizer + and not native_runtime_package_split_failures + and not native_tools_package_split_failures, "Native root packages and crates must keep postgres/initdb/pg_ctl only, with pg_dump/psql published through oliphaunt-tools packages/crates.", [ "tools/release/optimize_native_runtime_payload.py", "tools/release/package_liboliphaunt_cargo_artifacts.py", "tools/release/release.py", + *native_runtime_package_split_failures, + *native_tools_package_split_failures, ], severity="P0", ) From 1f35982565ec08728bb454d6f072fdfb75bc780e Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 19:26:32 +0000 Subject: [PATCH 115/137] chore: port source architecture check to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 6 + docs/internal/IMPLEMENTATION_CHECKLIST.md | 2 +- .../check-final-source-architecture.mjs | 750 ++++++++++++++++++ .../policy/check-final-source-architecture.py | 598 -------------- tools/policy/check-repo-structure.sh | 4 +- tools/policy/check-tooling-stack.sh | 7 + tools/policy/python-entrypoints.allowlist | 1 - 7 files changed, 766 insertions(+), 602 deletions(-) create mode 100755 tools/policy/check-final-source-architecture.mjs delete mode 100755 tools/policy/check-final-source-architecture.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 9b842c5f..298916f6 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -158,6 +158,12 @@ until the current-state gates here are checked with fresh local evidence. `bash src/sdks/react-native/tools/check-sdk.sh check-static`, `python3 tools/release/check_release_metadata.py`, `python3 tools/release/check_consumer_shape.py`, and `git diff --check`. +- 2026-06-26: Final source architecture policy checks now run through + `tools/policy/check-final-source-architecture.mjs` and the pinned Bun + launcher instead of the retired Python entrypoint. The Python entrypoint was + removed from `tools/policy/python-entrypoints.allowlist`, and + `check-tooling-stack.sh` now rejects stale references to + the retired checker path. - 2026-06-26: Rust SDK broker Cargo relay smoke setup now prepares the generated publish source through `python3 tools/release/release.py prepare-rust-release-source` instead of an inline Python heredoc that imports diff --git a/docs/internal/IMPLEMENTATION_CHECKLIST.md b/docs/internal/IMPLEMENTATION_CHECKLIST.md index 7899dbda..82eab842 100644 --- a/docs/internal/IMPLEMENTATION_CHECKLIST.md +++ b/docs/internal/IMPLEMENTATION_CHECKLIST.md @@ -526,7 +526,7 @@ or CI/build output proves the contract. generated-state inputs, and mobile source-build fallbacks. - [x] Policy checks reject retired release-tool references on active product, workflow, and release surfaces. Evidence: - `tools/policy/check-final-source-architecture.py --self-test` scans tracked + `tools/policy/check-final-source-architecture.mjs --self-test` scans tracked `src`, `.github`, and `tools/release` files for retired `release-plz` and `git-cliff` references while allowing the architecture/tooling docs to name retired surfaces as policy. diff --git a/tools/policy/check-final-source-architecture.mjs b/tools/policy/check-final-source-architecture.mjs new file mode 100755 index 00000000..47ca6513 --- /dev/null +++ b/tools/policy/check-final-source-architecture.mjs @@ -0,0 +1,750 @@ +#!/usr/bin/env bun +import { spawnSync } from 'node:child_process'; +import { existsSync, statSync } from 'node:fs'; +import { readFile, readdir } from 'node:fs/promises'; +import path from 'node:path'; + +const ROOT = path.resolve(import.meta.dir, '..', '..'); +const EXTENSION_ID = /^[a-z][a-z0-9_]{0,127}$/u; +const SQL_EXTENSION_NAME = /^[a-z][a-z0-9_-]{0,127}$/u; + +const CURRENT_SOURCE_DOMAINS = new Set([ + 'src/postgres/versions/18', + 'src/sources', + 'src/extensions', + 'src/shared', +]); + +const CURRENT_SOURCE_DOMAIN_PROJECTS = new Set([ + 'src/postgres/versions/18', + 'src/sources/third-party/shared', + 'src/sources/third-party/native', + 'src/sources/third-party/wasix', + 'src/sources/toolchains', + 'src/extensions', + 'src/shared/js-core', +]); + +const TARGET_SOURCE_DOMAINS = new Set([ + 'src/postgres', + 'src/sources', + 'src/extensions', + 'src/runtimes', + 'src/shared', + 'src/sdks', + 'src/bindings', + 'src/docs', +]); + +const CURRENT_PRODUCT_ROOTS = new Map([ + ['src/runtimes/liboliphaunt/native', 'liboliphaunt-native'], + ['src/sdks/rust', 'oliphaunt-rust'], + ['src/sdks/swift', 'oliphaunt-swift'], + ['src/sdks/kotlin', 'oliphaunt-kotlin'], + ['src/sdks/react-native', 'oliphaunt-react-native'], + ['src/sdks/js', 'oliphaunt-js'], + ['src/bindings/wasix-rust', 'oliphaunt-wasix-rust'], + ['src/docs', 'docs'], +]); + +const ALLOWED_SRC_TOP_LEVEL = new Set([ + ...[...CURRENT_SOURCE_DOMAINS].map((item) => item.replace(/^src\//u, '')), + ...[...TARGET_SOURCE_DOMAINS].map((item) => item.replace(/^src\//u, '')), + ...[...CURRENT_PRODUCT_ROOTS.keys()].map((item) => item.replace(/^src\//u, '')), +]); + +const RETIRED_ROOTS = new Set(['assets', 'crates', 'fixtures', 'liboliphaunt-native', 'sdks']); +const FORBIDDEN_PRODUCT_IDENTITIES = new Set(['@oliphaunt/sdk-apple', 'apple-sdk', 'oliphaunt-apple']); +const FORBIDDEN_RETIRED_RELEASE_TOOL_TEXT = new Set(['release-plz', 'git-cliff']); + +const SDK_RUNTIME_SOURCE_PREFIXES = [ + 'src/sdks/rust/src/', + 'src/sdks/swift/Sources/', + 'src/sdks/kotlin/oliphaunt/src/commonMain/', + 'src/sdks/kotlin/oliphaunt/src/androidMain/', + 'src/sdks/kotlin/oliphaunt/src/nativeMain/', + 'src/sdks/react-native/src/', + 'src/sdks/react-native/ios/', + 'src/sdks/react-native/android/src/main/', + 'src/sdks/js/src/', +]; + +const TRANSITIONAL_EXTENSION_RULE_ALLOWLIST = new Set([ + 'src/sdks/js/src/config.ts\0if (extension === \'pg_search\')', + 'src/sdks/js/src/config.ts\0libraries.add(\'pg_search\')', +]); + +const TRANSITIONAL_EXTENSION_RULE_FILES = new Set([ + 'src/sdks/rust/src/extension.rs', + 'src/sdks/rust/src/runtime_resources.rs', + 'src/sdks/swift/Sources/COliphaunt/include/oliphaunt.h', + 'src/sdks/kotlin/oliphaunt/src/androidMain/cpp/include/oliphaunt.h', + 'src/sdks/react-native/android/src/main/cpp/include/oliphaunt.h', +]); + +const PROMOTED_CATALOG = 'src/extensions/catalog/extensions.promoted.toml'; +const SMOKE_CATALOG = 'src/extensions/catalog/extensions.smoke.toml'; +const GENERATED_CATALOG = 'src/extensions/generated/extensions.catalog.json'; +const GENERATED_BUILD_PLAN = 'src/extensions/generated/extensions.build-plan.json'; +const GENERATED_EXTENSION_DOCS = 'src/extensions/generated/docs/extensions.json'; +const GENERATED_EXTENSION_EVIDENCE = 'src/extensions/generated/docs/extension-evidence.json'; +const EVIDENCE_MATRIX = 'src/extensions/evidence/matrix.toml'; +const EVIDENCE_RUN_SCHEMA = 'src/extensions/evidence/schemas/run.schema.json'; +const EVIDENCE_MATRIX_SCHEMA = 'src/extensions/evidence/schemas/matrix.schema.json'; +const EVIDENCE_RUNS = 'src/extensions/evidence/runs'; +const GENERATED_SDK_METADATA = [ + 'src/extensions/generated/sdk/rust.json', + 'src/extensions/generated/sdk/swift.json', + 'src/extensions/generated/sdk/kotlin.json', + 'src/extensions/generated/sdk/js.json', + 'src/extensions/generated/sdk/react-native.json', +]; +const GENERATED_SDK_PACKAGE_METADATA = [ + 'src/sdks/js/src/generated/extensions.ts', + 'src/sdks/kotlin/oliphaunt/src/generated/extensions.json', + 'src/sdks/react-native/src/generated/extensions.ts', + 'src/sdks/react-native/src/generated/extensions.json', +]; +const GENERATED_MOBILE_REGISTRY = 'src/extensions/generated/mobile/static-registry.json'; +const GENERATED_WASIX_METADATA = 'src/extensions/generated/wasix/extensions.json'; +const GENERATED_TSV = [ + 'src/extensions/generated/contrib-build.tsv', + 'src/extensions/generated/pgxs-build.tsv', +]; + +class PolicyFailure extends Error { + constructor(message) { + super(message); + this.name = 'PolicyFailure'; + } +} + +class TextDecodeFailure extends Error { + constructor(relativePath, cause) { + super(`${relativePath} is not valid UTF-8: ${cause.message}`); + this.name = 'TextDecodeFailure'; + } +} + +function fail(message) { + throw new PolicyFailure(message); +} + +function rel(file) { + return path.relative(ROOT, file).split(path.sep).join('/'); +} + +function absolute(relativePath) { + return path.join(ROOT, relativePath); +} + +function requireFile(relativePath) { + if (!existsSync(absolute(relativePath)) || !statSync(absolute(relativePath)).isFile()) { + fail(`missing required file: ${relativePath}`); + } +} + +function requireDir(relativePath) { + if (!existsSync(absolute(relativePath)) || !statSync(absolute(relativePath)).isDirectory()) { + fail(`missing required directory: ${relativePath}`); + } +} + +function trackedFiles(...paths) { + const result = spawnSync('git', ['ls-files', '-z', '--', ...paths], { + cwd: ROOT, + encoding: 'utf8', + }); + if (result.error) { + fail(`git ls-files failed: ${result.error.message}`); + } + if (result.status !== 0) { + fail(`git ls-files failed: ${result.stderr.trim()}`); + } + return result.stdout + .split('\0') + .filter(Boolean) + .sort(compareText); +} + +async function readText(relativePath) { + const bytes = await readFile(absolute(relativePath)); + try { + return new TextDecoder('utf-8', { fatal: true }).decode(bytes); + } catch (error) { + throw new TextDecodeFailure(relativePath, error); + } +} + +async function readToml(relativePath) { + requireFile(relativePath); + try { + return Bun.TOML.parse(await readText(relativePath)); + } catch (error) { + if (error instanceof TextDecodeFailure) { + fail(error.message); + } + fail(`${relativePath} is invalid TOML: ${error.message}`); + } +} + +async function readJson(relativePath) { + requireFile(relativePath); + let value; + try { + value = JSON.parse(await readText(relativePath)); + } catch (error) { + if (error instanceof TextDecodeFailure) { + fail(error.message); + } + fail(`${relativePath} is invalid JSON: ${error.message}`); + } + if (!isRecord(value)) { + fail(`${relativePath} must contain a JSON object`); + } + return value; +} + +function isRecord(value) { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function compareText(left, right) { + return left < right ? -1 : left > right ? 1 : 0; +} + +function pythonTruthy(value) { + if (value === undefined || value === null || value === false || value === 0 || value === '') { + return false; + } + if (Array.isArray(value)) { + return value.length > 0; + } + if (isRecord(value)) { + return Object.keys(value).length > 0; + } + return true; +} + +function validateExtensionId(value, context) { + if (typeof value !== 'string' || !EXTENSION_ID.test(value)) { + fail(`${context} has invalid exact SQL extension id ${JSON.stringify(value)}`); + } + return value; +} + +function validateSqlExtensionName(value, context) { + if (typeof value !== 'string' || !SQL_EXTENSION_NAME.test(value)) { + fail(`${context} has invalid exact SQL extension name ${JSON.stringify(value)}`); + } + return value; +} + +function validateUniqueIds(ids, context) { + const seen = new Set(); + const duplicates = new Set(); + for (const extensionId of ids) { + if (seen.has(extensionId)) { + duplicates.add(extensionId); + } + seen.add(extensionId); + } + if (duplicates.size > 0) { + fail(`${context} has duplicate extension ids: ${JSON.stringify([...duplicates].sort(compareText))}`); + } +} + +async function extensionRows(relativePath) { + const value = (await readToml(relativePath)).extensions; + if (!Array.isArray(value)) { + fail(`${relativePath} must define [[extensions]] rows`); + } + const rows = []; + for (const [index, row] of value.entries()) { + if (!isRecord(row)) { + fail(`${relativePath} extensions[${index}] must be a table`); + } + rows.push(row); + } + return rows; +} + +function checkSourceDomains() { + for (const sourceDomain of CURRENT_SOURCE_DOMAINS) { + requireDir(sourceDomain); + } + for (const sourceDomain of CURRENT_SOURCE_DOMAIN_PROJECTS) { + requireFile(path.posix.join(sourceDomain, 'moon.yml')); + } + requireFile('src/shared/contracts/moon.yml'); + requireFile('src/shared/fixtures/moon.yml'); + for (const retired of RETIRED_ROOTS) { + const files = trackedFiles(retired); + if (files.length > 0) { + fail(`retired root source alias ${retired}/ still has tracked files: ${JSON.stringify(files.slice(0, 8))}`); + } + } + + const srcChildren = new Set( + trackedFiles('src') + .filter((item) => item.includes('/')) + .map((item) => item.split('/')[1]), + ); + const unexpected = [...srcChildren].filter((item) => !ALLOWED_SRC_TOP_LEVEL.has(item)).sort(compareText); + if (unexpected.length > 0) { + fail(`unexpected top-level source domains under src/: ${JSON.stringify(unexpected)}`); + } +} + +async function checkSourceSpinePolicy() { + const file = 'tools/xtask/src/source_spine.rs'; + const sourceSpine = await readText(file); + if (!sourceSpine.includes('Path::new(SOURCE_CHECKOUT_ROOT).join(name)')) { + fail(`${file} must derive source checkout paths from SOURCE_CHECKOUT_ROOT and source name`); + } + for (const forbidden of [ + '"pgtap" =>', + '"postgis" =>', + '"pgvector" =>', + 'target/oliphaunt-sources/checkouts/pgtap', + 'target/oliphaunt-sources/checkouts/postgis', + 'target/oliphaunt-sources/checkouts/pgvector', + ]) { + if (sourceSpine.includes(forbidden)) { + fail(`${file} must not hardcode source checkout mapping ${JSON.stringify(forbidden)}`); + } + } +} + +async function checkXtaskExtensionPolicy() { + const file = 'tools/xtask/src/postgres_guard.rs'; + const text = await readText(file); + if (text.includes('extension.build_kind == "postgis"')) { + fail(`${file} must not key PostGIS source-shape checks off the reusable build-kind family`); + } + if (!text.includes('extension.source_kind == "postgis"')) { + fail(`${file} must keep PostGIS source-shape checks keyed to source_kind`); + } +} + +async function checkProductRoots() { + for (const [productRoot, projectId] of CURRENT_PRODUCT_ROOTS) { + const moonYml = path.posix.join(productRoot, 'moon.yml'); + requireFile(moonYml); + const text = await readText(moonYml); + if (!text.includes(`id: "${projectId}"`)) { + fail(`${productRoot}/moon.yml must declare id ${JSON.stringify(projectId)}`); + } + } + + for (const forbidden of ['src/apple-sdk', 'src/oliphaunt-apple', 'src/apple']) { + const files = trackedFiles(forbidden); + if (files.length > 0) { + fail(`forbidden Swift SDK alias has tracked files: ${JSON.stringify(files.slice(0, 8))}`); + } + } +} + +async function checkForbiddenProductIdentityText() { + const scanFiles = trackedFiles( + 'src', + '.github', + 'tools/release', + 'Cargo.toml', + 'Package.swift', + 'package.json', + 'pnpm-workspace.yaml', + ); + const offenders = []; + for (const file of scanFiles) { + if (file.startsWith('src/postgres/versions/18/')) { + continue; + } + if (!existsSync(absolute(file))) { + continue; + } + let text; + try { + text = await readText(file); + } catch (error) { + if (error instanceof TextDecodeFailure) { + continue; + } + throw error; + } + const lowered = text.toLowerCase(); + for (const identity of FORBIDDEN_PRODUCT_IDENTITIES) { + if (lowered.includes(identity)) { + offenders.push(`${file}: contains ${identity}`); + } + } + } + if (offenders.length > 0) { + fail(`forbidden product identity text found:\n${offenders.slice(0, 20).join('\n')}`); + } +} + +async function checkForbiddenRetiredReleaseToolText() { + const scanFiles = trackedFiles( + 'src', + '.github', + 'tools/release', + 'Cargo.toml', + 'Package.swift', + 'package.json', + 'pnpm-workspace.yaml', + 'release-please-config.json', + '.release-please-manifest.json', + ); + const offenders = []; + for (const file of scanFiles) { + if (file.startsWith('src/postgres/versions/18/')) { + continue; + } + if (!existsSync(absolute(file))) { + continue; + } + let text; + try { + text = await readText(file); + } catch (error) { + if (error instanceof TextDecodeFailure) { + continue; + } + throw error; + } + const lowered = text.toLowerCase(); + for (const name of FORBIDDEN_RETIRED_RELEASE_TOOL_TEXT) { + if (lowered.includes(name)) { + offenders.push(`${file}: contains retired release tool reference ${name}`); + } + } + } + if (offenders.length > 0) { + fail(`retired release tool text found on active product/release surfaces:\n${offenders.slice(0, 20).join('\n')}`); + } +} + +async function checkExtensionCatalogs() { + const promotedRows = await extensionRows(PROMOTED_CATALOG); + const smokeRows = await extensionRows(SMOKE_CATALOG); + const promotedIds = promotedRows.map((row) => validateExtensionId(row.id, `${PROMOTED_CATALOG} row`)); + const smokeIds = smokeRows.map((row) => validateExtensionId(row.id, `${SMOKE_CATALOG} row`)); + validateUniqueIds(promotedIds, PROMOTED_CATALOG); + validateUniqueIds(smokeIds, SMOKE_CATALOG); + const promotedSet = new Set(promotedIds); + const unknownSmoke = [...new Set(smokeIds)].filter((item) => !promotedSet.has(item)).sort(compareText); + if (unknownSmoke.length > 0) { + fail(`${SMOKE_CATALOG} references extensions not in promoted catalog: ${JSON.stringify(unknownSmoke)}`); + } + + for (const row of promotedRows) { + const unexpectedPackKeys = Object.keys(row) + .filter((key) => key.includes('pack') || key.includes('bundle') || key.includes('alias')) + .sort(compareText); + if (unexpectedPackKeys.length > 0) { + fail(`extension row ${row.id} must not use pack/bundle/alias keys: ${JSON.stringify(unexpectedPackKeys)}`); + } + if (row.stable === false && !pythonTruthy(row.blocker)) { + fail(`candidate extension ${row.id} must explain its blocker`); + } + } +} + +async function checkGeneratedExtensionMetadata() { + const catalog = await readJson(GENERATED_CATALOG); + const buildPlan = await readJson(GENERATED_BUILD_PLAN); + const docsTable = await readJson(GENERATED_EXTENSION_DOCS); + const evidenceTable = await readJson(GENERATED_EXTENSION_EVIDENCE); + if (catalog['format-version'] !== 1) { + fail(`${GENERATED_CATALOG} must use format-version 1`); + } + if (buildPlan['format-version'] !== 1) { + fail(`${GENERATED_BUILD_PLAN} must use format-version 1`); + } + if (docsTable['format-version'] !== 1) { + fail(`${GENERATED_EXTENSION_DOCS} must use format-version 1`); + } + if (evidenceTable['format-version'] !== 1) { + fail(`${GENERATED_EXTENSION_EVIDENCE} must use format-version 1`); + } + for (const file of [...GENERATED_SDK_METADATA, GENERATED_MOBILE_REGISTRY, GENERATED_WASIX_METADATA]) { + const value = await readJson(file); + if (value['format-version'] !== 1) { + fail(`${file} must use format-version 1`); + } + } + for (const file of GENERATED_SDK_PACKAGE_METADATA) { + requireFile(file); + } + + const promotedIds = new Set( + (await extensionRows(PROMOTED_CATALOG)).map((row) => + validateExtensionId(row.id, `${PROMOTED_CATALOG} row`), + ), + ); + const catalogExtensions = catalog.extensions; + const buildExtensions = buildPlan.extensions; + if (!Array.isArray(catalogExtensions) || catalogExtensions.length === 0) { + fail(`${GENERATED_CATALOG} must define non-empty extensions`); + } + if (!Array.isArray(buildExtensions) || buildExtensions.length === 0) { + fail(`${GENERATED_BUILD_PLAN} must define non-empty extensions`); + } + + const catalogIds = catalogExtensions.map((row) => validateExtensionId(row.id, `${GENERATED_CATALOG} row`)); + const buildIds = buildExtensions.map((row) => validateExtensionId(row.id, `${GENERATED_BUILD_PLAN} row`)); + validateUniqueIds(catalogIds, GENERATED_CATALOG); + validateUniqueIds(buildIds, GENERATED_BUILD_PLAN); + const unknownCatalog = [...new Set(catalogIds)].filter((item) => !promotedIds.has(item)).sort(compareText); + const unknownBuild = [...new Set(buildIds)].filter((item) => !promotedIds.has(item)).sort(compareText); + if (unknownCatalog.length > 0) { + fail(`${GENERATED_CATALOG} has ids not declared in promoted catalog: ${JSON.stringify(unknownCatalog)}`); + } + if (unknownBuild.length > 0) { + fail(`${GENERATED_BUILD_PLAN} has ids not declared in promoted catalog: ${JSON.stringify(unknownBuild)}`); + } + + for (const row of buildExtensions) { + const extensionId = validateExtensionId(row.id, `${GENERATED_BUILD_PLAN} row`); + const sqlName = validateSqlExtensionName( + Object.hasOwn(row, 'sql-name') ? row['sql-name'] : extensionId, + `${GENERATED_BUILD_PLAN} row`, + ); + const buildKind = row['build-kind']; + if (!new Set(['postgres-contrib', 'pgxs-external', 'pgxs-sql-only', 'autotools']).has(buildKind)) { + fail(`${GENERATED_BUILD_PLAN} extension ${extensionId} has unsupported build-kind ${JSON.stringify(buildKind)}`); + } + if (buildKind === sqlName) { + fail(`${GENERATED_BUILD_PLAN} extension ${extensionId} uses extension-specific build-kind ${JSON.stringify(buildKind)}; build-kind must be a reusable build family`); + } + const archive = row.archive; + if (typeof archive !== 'string' || archive !== `extensions/${sqlName}.tar.zst`) { + fail(`${GENERATED_BUILD_PLAN} extension ${extensionId} has invalid exact-extension archive ${JSON.stringify(archive)}`); + } + if (['pack', 'packs', 'bundle', 'alias', 'aliases'].some((key) => Object.hasOwn(row, key))) { + fail(`${GENERATED_BUILD_PLAN} extension ${extensionId} must not use pack/bundle/alias metadata`); + } + if (buildKind === 'autotools') { + const buildScript = row['build-script']; + if (typeof buildScript !== 'string' || buildScript.length === 0) { + fail(`${GENERATED_BUILD_PLAN} extension ${extensionId} must declare build-script for recipe-staged autotools builds`); + } + for (const field of ['required-build-files', 'required-build-globs']) { + const values = row[field]; + if (!Array.isArray(values) || values.length === 0 || values.some((value) => typeof value !== 'string' || value.length === 0)) { + fail(`${GENERATED_BUILD_PLAN} extension ${extensionId} must declare non-empty ${field} for recipe-staged autotools builds`); + } + } + } + } + + for (const file of GENERATED_TSV) { + requireFile(file); + const text = await readText(file); + if (text.toLowerCase().includes('pack') || text.toLowerCase().includes('bundle')) { + fail(`${file} must not contain extension pack/bundle metadata`); + } + } +} + +async function checkExtensionEvidence() { + requireFile(EVIDENCE_MATRIX); + requireFile(EVIDENCE_RUN_SCHEMA); + requireFile(EVIDENCE_MATRIX_SCHEMA); + requireDir(EVIDENCE_RUNS); + if ((await readdir(absolute(EVIDENCE_RUNS))).filter((item) => item.endsWith('.json')).length === 0) { + fail(`${EVIDENCE_RUNS} must contain extension evidence run files`); + } + + const matrix = await readToml(EVIDENCE_MATRIX); + if (matrix['format-version'] !== 1) { + fail(`${EVIDENCE_MATRIX} must use format-version 1`); + } + const claims = matrix.claims; + if (!Array.isArray(claims) || claims.length === 0) { + fail(`${EVIDENCE_MATRIX} must declare [[claims]]`); + } + + const publicIds = new Set( + (await extensionRows(PROMOTED_CATALOG)) + .filter((row) => row.stable === true && row.build !== false) + .map((row) => validateExtensionId(row.id, `${PROMOTED_CATALOG} row`)), + ); + const claimIds = new Set( + claims + .filter((claim) => isRecord(claim) && claim.public === true) + .map((claim) => validateExtensionId(claim.extension, `${EVIDENCE_MATRIX} claim`)), + ); + const missing = [...publicIds].filter((item) => !claimIds.has(item)).sort(compareText); + const extra = [...claimIds].filter((item) => !publicIds.has(item)).sort(compareText); + if (missing.length > 0) { + fail(`${EVIDENCE_MATRIX} is missing public claims for stable catalog rows: ${JSON.stringify(missing)}`); + } + if (extra.length > 0) { + fail(`${EVIDENCE_MATRIX} claims public support for non-stable catalog rows: ${JSON.stringify(extra)}`); + } +} + +async function checkExtensionRecipes() { + const retiredRecipesRoot = 'src/extensions/recipes'; + if (existsSync(absolute(retiredRecipesRoot))) { + fail(`${retiredRecipesRoot} is retired; external extension definitions live under src/extensions/external`); + } + const externalRoot = 'src/extensions/external'; + if (!existsSync(absolute(externalRoot))) { + fail(`${externalRoot} must exist`); + } + const entries = await readdir(absolute(externalRoot), { withFileTypes: true }); + const recipeFiles = entries + .filter((entry) => entry.isDirectory() && existsSync(absolute(path.posix.join(externalRoot, entry.name, 'recipe.toml')))) + .map((entry) => path.posix.join(externalRoot, entry.name, 'recipe.toml')) + .sort(compareText); + for (const recipe of recipeFiles) { + const data = await readToml(recipe); + if (data.schema !== 'oliphaunt-extension-recipe-v1') { + fail(`${recipe} must use schema = oliphaunt-extension-recipe-v1`); + } + const sqlName = validateSqlExtensionName(data.sql_name, `${recipe} recipe`); + const kind = data.kind; + if (!new Set(['external-simple-pgxs', 'external-complex']).has(kind)) { + fail(`${recipe} must declare an external recipe kind`); + } + if (path.posix.basename(path.posix.dirname(recipe)) !== sqlName) { + fail(`${recipe} directory must match exact SQL extension name`); + } + for (const section of ['lifecycle', 'artifacts', 'support']) { + if (!isRecord(data[section])) { + fail(`${recipe} must declare [${section}]`); + } + } + const recipeDir = path.posix.dirname(recipe); + requireFile(path.posix.join(recipeDir, 'tests/smoke.sql')); + const targets = path.posix.join(recipeDir, 'targets'); + const hasTargetToml = + existsSync(absolute(targets)) && + statSync(absolute(targets)).isDirectory() && + (await readdir(absolute(targets))).some((item) => item.endsWith('.toml')); + if (!hasTargetToml) { + fail(`${recipe} must declare at least one target TOML under targets/`); + } + if (kind === 'external-complex') { + requireFile(path.posix.join(recipeDir, 'deps.toml')); + requireFile(path.posix.join(recipeDir, 'tests/upstream.toml')); + requireFile(path.posix.join(recipeDir, 'patches/README.md')); + requireFile(path.posix.join(recipeDir, 'blockers.toml')); + } + } +} + +async function checkSdkLocalExtensionRules() { + const catalogIds = new Set( + (await extensionRows(PROMOTED_CATALOG)).map((row) => + validateExtensionId(row.id, `${PROMOTED_CATALOG} row`), + ), + ); + const complexIds = [...catalogIds].filter((item) => + new Set(['age', 'graph', 'pg_search', 'pg_textsearch', 'postgis', 'vector']).has(item), + ); + const offenders = []; + for (const file of trackedFiles('src/sdks/rust', 'src/sdks/swift', 'src/sdks/kotlin', 'src/sdks/react-native', 'src/sdks/js')) { + if (!SDK_RUNTIME_SOURCE_PREFIXES.some((prefix) => file.startsWith(prefix))) { + continue; + } + if (TRANSITIONAL_EXTENSION_RULE_FILES.has(file) || file.includes('/generated/')) { + continue; + } + if (file.includes('/tests/') || file.includes('/Tests/') || file.includes('/__tests__/')) { + continue; + } + let lines; + try { + lines = (await readText(file)).split(/\r?\n/u); + } catch (error) { + if (error instanceof TextDecodeFailure) { + continue; + } + throw error; + } + for (const [index, line] of lines.entries()) { + const stripped = line.trim(); + if (TRANSITIONAL_EXTENSION_RULE_ALLOWLIST.has(`${file}\0${stripped}`)) { + continue; + } + for (const extensionId of complexIds) { + const pattern = new RegExp(`['"\`](${escapeRegExp(extensionId)})['"\`]`, 'u'); + if (pattern.test(stripped)) { + offenders.push(`${file}:${index + 1}: hardcodes extension ${JSON.stringify(extensionId)}: ${stripped}`); + } + } + } + } + if (offenders.length > 0) { + fail(`SDK runtime source must not hardcode complex extension rules outside generated metadata; known transitional exceptions must be explicit:\n${offenders.slice(0, 20).join('\n')}`); + } +} + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'); +} + +function selfTest() { + const expectFailure = (callback, label) => { + let failedAsExpected = false; + try { + callback(); + } catch (error) { + if (error instanceof PolicyFailure) { + failedAsExpected = true; + } else { + throw error; + } + } + if (!failedAsExpected) { + fail(`self-test expected ${label} to fail`); + } + }; + expectFailure(() => validateExtensionId('bad-name', 'self-test'), 'invalid extension id'); + expectFailure(() => validateUniqueIds(['vector', 'vector'], 'self-test'), 'duplicate extension ids'); +} + +async function checkLiveRepo() { + checkSourceDomains(); + await checkSourceSpinePolicy(); + await checkXtaskExtensionPolicy(); + await checkProductRoots(); + await checkForbiddenProductIdentityText(); + await checkForbiddenRetiredReleaseToolText(); + await checkExtensionCatalogs(); + await checkGeneratedExtensionMetadata(); + await checkExtensionEvidence(); + await checkExtensionRecipes(); + await checkSdkLocalExtensionRules(); +} + +function parseArgs(argv) { + const args = { selfTest: false }; + for (const arg of argv) { + if (arg === '--self-test') { + args.selfTest = true; + } else { + fail(`unknown argument: ${arg}`); + } + } + return args; +} + +const args = parseArgs(Bun.argv.slice(2)); +try { + if (args.selfTest) { + selfTest(); + } + await checkLiveRepo(); + console.log('final source architecture policy checks passed'); +} catch (error) { + if (error instanceof PolicyFailure) { + console.error(`check-final-source-architecture.mjs: ${error.message}`); + process.exit(1); + } + throw error; +} diff --git a/tools/policy/check-final-source-architecture.py b/tools/policy/check-final-source-architecture.py deleted file mode 100755 index 90da31a3..00000000 --- a/tools/policy/check-final-source-architecture.py +++ /dev/null @@ -1,598 +0,0 @@ -#!/usr/bin/env python3 -"""Validate Oliphaunt's target source architecture invariants. - -This is a source architecture guard. It rejects retired product aliases and -validates the structured source/extension metadata that current products rely -on. -""" - -from __future__ import annotations - -import argparse -import json -import re -import subprocess -import sys -import tomllib -from pathlib import Path -from typing import Any, NoReturn - - -ROOT = Path(__file__).resolve().parents[2] -EXTENSION_ID = re.compile(r"^[a-z][a-z0-9_]{0,127}$") -SQL_EXTENSION_NAME = re.compile(r"^[a-z][a-z0-9_-]{0,127}$") - -CURRENT_SOURCE_DOMAINS = { - "src/postgres/versions/18", - "src/sources", - "src/extensions", - "src/shared", -} - -CURRENT_SOURCE_DOMAIN_PROJECTS = { - "src/postgres/versions/18", - "src/sources/third-party/shared", - "src/sources/third-party/native", - "src/sources/third-party/wasix", - "src/sources/toolchains", - "src/extensions", - "src/shared/js-core", -} - -TARGET_SOURCE_DOMAINS = { - "src/postgres", - "src/sources", - "src/extensions", - "src/runtimes", - "src/shared", - "src/sdks", - "src/bindings", - "src/docs", -} - -CURRENT_PRODUCT_ROOTS = { - "src/runtimes/liboliphaunt/native": "liboliphaunt-native", - "src/sdks/rust": "oliphaunt-rust", - "src/sdks/swift": "oliphaunt-swift", - "src/sdks/kotlin": "oliphaunt-kotlin", - "src/sdks/react-native": "oliphaunt-react-native", - "src/sdks/js": "oliphaunt-js", - "src/bindings/wasix-rust": "oliphaunt-wasix-rust", - "src/docs": "docs", -} - -ALLOWED_SRC_TOP_LEVEL = { - *(path.removeprefix("src/") for path in CURRENT_SOURCE_DOMAINS), - *(path.removeprefix("src/") for path in TARGET_SOURCE_DOMAINS), - *(path.removeprefix("src/") for path in CURRENT_PRODUCT_ROOTS), -} - -RETIRED_ROOTS = { - "assets", - "crates", - "fixtures", - "liboliphaunt-native", - "sdks", -} - -FORBIDDEN_PRODUCT_IDENTITIES = { - "@oliphaunt/sdk-apple", - "apple-sdk", - "oliphaunt-apple", -} - -FORBIDDEN_RETIRED_RELEASE_TOOL_TEXT = { - "release-plz", - "git-cliff", -} - -SDK_RUNTIME_SOURCE_PREFIXES = ( - "src/sdks/rust/src/", - "src/sdks/swift/Sources/", - "src/sdks/kotlin/oliphaunt/src/commonMain/", - "src/sdks/kotlin/oliphaunt/src/androidMain/", - "src/sdks/kotlin/oliphaunt/src/nativeMain/", - "src/sdks/react-native/src/", - "src/sdks/react-native/ios/", - "src/sdks/react-native/android/src/main/", - "src/sdks/js/src/", -) - -TRANSITIONAL_EXTENSION_RULE_ALLOWLIST = { - ( - "src/sdks/js/src/config.ts", - "if (extension === 'pg_search')", - ), - ( - "src/sdks/js/src/config.ts", - "libraries.add('pg_search')", - ), -} - -TRANSITIONAL_EXTENSION_RULE_FILES = { - # Replaced by generated SDK extension metadata in checklist item 8. - "src/sdks/rust/src/extension.rs", - "src/sdks/rust/src/runtime_resources.rs", - # Copied native ABI headers currently include one example module stem. - "src/sdks/swift/Sources/COliphaunt/include/oliphaunt.h", - "src/sdks/kotlin/oliphaunt/src/androidMain/cpp/include/oliphaunt.h", - "src/sdks/react-native/android/src/main/cpp/include/oliphaunt.h", -} - -PROMOTED_CATALOG = ROOT / "src/extensions/catalog/extensions.promoted.toml" -SMOKE_CATALOG = ROOT / "src/extensions/catalog/extensions.smoke.toml" -GENERATED_CATALOG = ROOT / "src/extensions/generated/extensions.catalog.json" -GENERATED_BUILD_PLAN = ROOT / "src/extensions/generated/extensions.build-plan.json" -GENERATED_EXTENSION_DOCS = ROOT / "src/extensions/generated/docs/extensions.json" -GENERATED_EXTENSION_EVIDENCE = ROOT / "src/extensions/generated/docs/extension-evidence.json" -EVIDENCE_MATRIX = ROOT / "src/extensions/evidence/matrix.toml" -EVIDENCE_RUN_SCHEMA = ROOT / "src/extensions/evidence/schemas/run.schema.json" -EVIDENCE_MATRIX_SCHEMA = ROOT / "src/extensions/evidence/schemas/matrix.schema.json" -EVIDENCE_RUNS = ROOT / "src/extensions/evidence/runs" -GENERATED_SDK_METADATA = [ - ROOT / "src/extensions/generated/sdk/rust.json", - ROOT / "src/extensions/generated/sdk/swift.json", - ROOT / "src/extensions/generated/sdk/kotlin.json", - ROOT / "src/extensions/generated/sdk/js.json", - ROOT / "src/extensions/generated/sdk/react-native.json", -] -GENERATED_SDK_PACKAGE_METADATA = [ - ROOT / "src/sdks/js/src/generated/extensions.ts", - ROOT / "src/sdks/kotlin/oliphaunt/src/generated/extensions.json", - ROOT / "src/sdks/react-native/src/generated/extensions.ts", - ROOT / "src/sdks/react-native/src/generated/extensions.json", -] -GENERATED_MOBILE_REGISTRY = ROOT / "src/extensions/generated/mobile/static-registry.json" -GENERATED_WASIX_METADATA = ROOT / "src/extensions/generated/wasix/extensions.json" -GENERATED_TSV = [ - ROOT / "src/extensions/generated/contrib-build.tsv", - ROOT / "src/extensions/generated/pgxs-build.tsv", -] - - -def fail(message: str) -> NoReturn: - raise SystemExit(f"check-final-source-architecture.py: {message}") - - -def rel(path: Path) -> str: - return path.relative_to(ROOT).as_posix() - - -def require_file(path: Path) -> None: - if not path.is_file(): - fail(f"missing required file: {rel(path)}") - - -def require_dir(path: Path) -> None: - if not path.is_dir(): - fail(f"missing required directory: {rel(path)}") - - -def tracked_files(*paths: str) -> list[str]: - command = ["git", "ls-files", "-z", "--", *paths] - output = subprocess.check_output(command, cwd=ROOT) - return sorted(path for path in output.decode("utf-8").split("\0") if path) - - -def read_toml(path: Path) -> dict[str, Any]: - require_file(path) - with path.open("rb") as handle: - return tomllib.load(handle) - - -def read_json(path: Path) -> dict[str, Any]: - require_file(path) - with path.open(encoding="utf-8") as handle: - value = json.load(handle) - if not isinstance(value, dict): - fail(f"{rel(path)} must contain a JSON object") - return value - - -def validate_extension_id(value: object, context: str) -> str: - if not isinstance(value, str) or not EXTENSION_ID.fullmatch(value): - fail(f"{context} has invalid exact SQL extension id {value!r}") - return value - - -def validate_sql_extension_name(value: object, context: str) -> str: - if not isinstance(value, str) or not SQL_EXTENSION_NAME.fullmatch(value): - fail(f"{context} has invalid exact SQL extension name {value!r}") - return value - - -def validate_unique_ids(ids: list[str], context: str) -> None: - seen: set[str] = set() - duplicates: set[str] = set() - for extension_id in ids: - if extension_id in seen: - duplicates.add(extension_id) - seen.add(extension_id) - if duplicates: - fail(f"{context} has duplicate extension ids: {sorted(duplicates)}") - - -def extension_rows(path: Path) -> list[dict[str, Any]]: - value = read_toml(path).get("extensions") - if not isinstance(value, list): - fail(f"{rel(path)} must define [[extensions]] rows") - rows: list[dict[str, Any]] = [] - for index, row in enumerate(value): - if not isinstance(row, dict): - fail(f"{rel(path)} extensions[{index}] must be a table") - rows.append(row) - return rows - - -def check_source_domains() -> None: - for source_domain in CURRENT_SOURCE_DOMAINS: - require_dir(ROOT / source_domain) - for source_domain in CURRENT_SOURCE_DOMAIN_PROJECTS: - require_file(ROOT / source_domain / "moon.yml") - require_file(ROOT / "src/shared/contracts/moon.yml") - require_file(ROOT / "src/shared/fixtures/moon.yml") - for retired in RETIRED_ROOTS: - files = tracked_files(retired) - if files: - fail(f"retired root source alias {retired}/ still has tracked files: {files[:8]}") - - src_children = { - path.split("/", 2)[1] - for path in tracked_files("src") - if path.count("/") >= 1 - } - unexpected = sorted(src_children - ALLOWED_SRC_TOP_LEVEL) - if unexpected: - fail(f"unexpected top-level source domains under src/: {unexpected}") - - -def check_source_spine_policy() -> None: - path = ROOT / "tools/xtask/src/source_spine.rs" - source_spine = path.read_text(encoding="utf-8") - if "Path::new(SOURCE_CHECKOUT_ROOT).join(name)" not in source_spine: - fail(f"{rel(path)} must derive source checkout paths from SOURCE_CHECKOUT_ROOT and source name") - for forbidden in [ - '"pgtap" =>', - '"postgis" =>', - '"pgvector" =>', - "target/oliphaunt-sources/checkouts/pgtap", - "target/oliphaunt-sources/checkouts/postgis", - "target/oliphaunt-sources/checkouts/pgvector", - ]: - if forbidden in source_spine: - fail(f"{rel(path)} must not hardcode source checkout mapping {forbidden!r}") - - -def check_xtask_extension_policy() -> None: - postgres_guard = ROOT / "tools/xtask/src/postgres_guard.rs" - postgres_guard_text = postgres_guard.read_text(encoding="utf-8") - if 'extension.build_kind == "postgis"' in postgres_guard_text: - fail( - f"{rel(postgres_guard)} must not key PostGIS source-shape checks off " - "the reusable build-kind family" - ) - if 'extension.source_kind == "postgis"' not in postgres_guard_text: - fail( - f"{rel(postgres_guard)} must keep PostGIS source-shape checks keyed " - "to source_kind" - ) - - -def check_product_roots() -> None: - for product_root, project_id in CURRENT_PRODUCT_ROOTS.items(): - moon_yml = ROOT / product_root / "moon.yml" - require_file(moon_yml) - text = moon_yml.read_text(encoding="utf-8") - if f'id: "{project_id}"' not in text: - fail(f"{product_root}/moon.yml must declare id {project_id!r}") - - for forbidden in ("src/apple-sdk", "src/oliphaunt-apple", "src/apple"): - files = tracked_files(forbidden) - if files: - fail(f"forbidden Swift SDK alias has tracked files: {files[:8]}") - - -def check_forbidden_product_identity_text() -> None: - scan_files = tracked_files( - "src", - ".github", - "tools/release", - "Cargo.toml", - "Package.swift", - "package.json", - "pnpm-workspace.yaml", - ) - offenders: list[str] = [] - for path in scan_files: - if path.startswith("src/postgres/versions/18/"): - continue - full_path = ROOT / path - if not full_path.exists(): - continue - try: - text = full_path.read_text(encoding="utf-8") - except UnicodeDecodeError: - continue - lowered = text.lower() - for identity in FORBIDDEN_PRODUCT_IDENTITIES: - if identity in lowered: - offenders.append(f"{path}: contains {identity}") - if offenders: - fail("forbidden product identity text found:\n" + "\n".join(offenders[:20])) - - -def check_forbidden_retired_release_tool_text() -> None: - scan_files = tracked_files( - "src", - ".github", - "tools/release", - "Cargo.toml", - "Package.swift", - "package.json", - "pnpm-workspace.yaml", - "release-please-config.json", - ".release-please-manifest.json", - ) - offenders: list[str] = [] - for path in scan_files: - if path.startswith("src/postgres/versions/18/"): - continue - full_path = ROOT / path - if not full_path.exists(): - continue - try: - text = full_path.read_text(encoding="utf-8") - except UnicodeDecodeError: - continue - lowered = text.lower() - for name in FORBIDDEN_RETIRED_RELEASE_TOOL_TEXT: - if name in lowered: - offenders.append(f"{path}: contains retired release tool reference {name}") - if offenders: - fail("retired release tool text found on active product/release surfaces:\n" + "\n".join(offenders[:20])) - - -def check_extension_catalogs() -> None: - promoted_rows = extension_rows(PROMOTED_CATALOG) - smoke_rows = extension_rows(SMOKE_CATALOG) - promoted_ids = [validate_extension_id(row.get("id"), f"{rel(PROMOTED_CATALOG)} row") for row in promoted_rows] - smoke_ids = [validate_extension_id(row.get("id"), f"{rel(SMOKE_CATALOG)} row") for row in smoke_rows] - validate_unique_ids(promoted_ids, rel(PROMOTED_CATALOG)) - validate_unique_ids(smoke_ids, rel(SMOKE_CATALOG)) - unknown_smoke = sorted(set(smoke_ids) - set(promoted_ids)) - if unknown_smoke: - fail(f"{rel(SMOKE_CATALOG)} references extensions not in promoted catalog: {unknown_smoke}") - - for row in promoted_rows: - unexpected_pack_keys = sorted(key for key in row if "pack" in key or "bundle" in key or "alias" in key) - if unexpected_pack_keys: - fail(f"extension row {row.get('id')} must not use pack/bundle/alias keys: {unexpected_pack_keys}") - if row.get("stable") is False and not row.get("blocker"): - fail(f"candidate extension {row.get('id')} must explain its blocker") - - -def check_generated_extension_metadata() -> None: - catalog = read_json(GENERATED_CATALOG) - build_plan = read_json(GENERATED_BUILD_PLAN) - docs_table = read_json(GENERATED_EXTENSION_DOCS) - evidence_table = read_json(GENERATED_EXTENSION_EVIDENCE) - if catalog.get("format-version") != 1: - fail(f"{rel(GENERATED_CATALOG)} must use format-version 1") - if build_plan.get("format-version") != 1: - fail(f"{rel(GENERATED_BUILD_PLAN)} must use format-version 1") - if docs_table.get("format-version") != 1: - fail(f"{rel(GENERATED_EXTENSION_DOCS)} must use format-version 1") - if evidence_table.get("format-version") != 1: - fail(f"{rel(GENERATED_EXTENSION_EVIDENCE)} must use format-version 1") - for path in [*GENERATED_SDK_METADATA, GENERATED_MOBILE_REGISTRY, GENERATED_WASIX_METADATA]: - value = read_json(path) - if value.get("format-version") != 1: - fail(f"{rel(path)} must use format-version 1") - for path in GENERATED_SDK_PACKAGE_METADATA: - require_file(path) - - promoted_ids = {validate_extension_id(row.get("id"), f"{rel(PROMOTED_CATALOG)} row") for row in extension_rows(PROMOTED_CATALOG)} - catalog_extensions = catalog.get("extensions") - build_extensions = build_plan.get("extensions") - if not isinstance(catalog_extensions, list) or not catalog_extensions: - fail(f"{rel(GENERATED_CATALOG)} must define non-empty extensions") - if not isinstance(build_extensions, list) or not build_extensions: - fail(f"{rel(GENERATED_BUILD_PLAN)} must define non-empty extensions") - - catalog_ids = [validate_extension_id(row.get("id"), f"{rel(GENERATED_CATALOG)} row") for row in catalog_extensions] - build_ids = [validate_extension_id(row.get("id"), f"{rel(GENERATED_BUILD_PLAN)} row") for row in build_extensions] - validate_unique_ids(catalog_ids, rel(GENERATED_CATALOG)) - validate_unique_ids(build_ids, rel(GENERATED_BUILD_PLAN)) - unknown_catalog = sorted(set(catalog_ids) - promoted_ids) - unknown_build = sorted(set(build_ids) - promoted_ids) - if unknown_catalog: - fail(f"{rel(GENERATED_CATALOG)} has ids not declared in promoted catalog: {unknown_catalog}") - if unknown_build: - fail(f"{rel(GENERATED_BUILD_PLAN)} has ids not declared in promoted catalog: {unknown_build}") - - for row in build_extensions: - extension_id = validate_extension_id(row.get("id"), f"{rel(GENERATED_BUILD_PLAN)} row") - sql_name = validate_sql_extension_name(row.get("sql-name", extension_id), f"{rel(GENERATED_BUILD_PLAN)} row") - build_kind = row.get("build-kind") - if build_kind not in {"postgres-contrib", "pgxs-external", "pgxs-sql-only", "autotools"}: - fail( - f"{rel(GENERATED_BUILD_PLAN)} extension {extension_id} has unsupported " - f"build-kind {build_kind!r}" - ) - if build_kind == sql_name: - fail( - f"{rel(GENERATED_BUILD_PLAN)} extension {extension_id} uses extension-specific " - f"build-kind {build_kind!r}; build-kind must be a reusable build family" - ) - archive = row.get("archive") - if not isinstance(archive, str) or archive != f"extensions/{sql_name}.tar.zst": - fail(f"{rel(GENERATED_BUILD_PLAN)} extension {extension_id} has invalid exact-extension archive {archive!r}") - if any(key in row for key in ("pack", "packs", "bundle", "alias", "aliases")): - fail(f"{rel(GENERATED_BUILD_PLAN)} extension {extension_id} must not use pack/bundle/alias metadata") - if build_kind == "autotools": - build_script = row.get("build-script") - if not isinstance(build_script, str) or not build_script: - fail( - f"{rel(GENERATED_BUILD_PLAN)} extension {extension_id} " - "must declare build-script for recipe-staged autotools builds" - ) - for field in ("required-build-files", "required-build-globs"): - values = row.get(field) - if not isinstance(values, list) or not values or not all(isinstance(value, str) and value for value in values): - fail( - f"{rel(GENERATED_BUILD_PLAN)} extension {extension_id} " - f"must declare non-empty {field} for recipe-staged autotools builds" - ) - - for path in GENERATED_TSV: - require_file(path) - text = path.read_text(encoding="utf-8") - if "pack" in text.lower() or "bundle" in text.lower(): - fail(f"{rel(path)} must not contain extension pack/bundle metadata") - - -def check_extension_evidence() -> None: - require_file(EVIDENCE_MATRIX) - require_file(EVIDENCE_RUN_SCHEMA) - require_file(EVIDENCE_MATRIX_SCHEMA) - require_dir(EVIDENCE_RUNS) - if not list(EVIDENCE_RUNS.glob("*.json")): - fail(f"{rel(EVIDENCE_RUNS)} must contain extension evidence run files") - - matrix = read_toml(EVIDENCE_MATRIX) - if matrix.get("format-version") != 1: - fail(f"{rel(EVIDENCE_MATRIX)} must use format-version 1") - claims = matrix.get("claims") - if not isinstance(claims, list) or not claims: - fail(f"{rel(EVIDENCE_MATRIX)} must declare [[claims]]") - - public_ids = { - validate_extension_id(row.get("id"), f"{rel(PROMOTED_CATALOG)} row") - for row in extension_rows(PROMOTED_CATALOG) - if row.get("stable") is True and row.get("build") is not False - } - claim_ids = { - validate_extension_id(claim.get("extension"), f"{rel(EVIDENCE_MATRIX)} claim") - for claim in claims - if isinstance(claim, dict) and claim.get("public") is True - } - missing = sorted(public_ids - claim_ids) - extra = sorted(claim_ids - public_ids) - if missing: - fail(f"{rel(EVIDENCE_MATRIX)} is missing public claims for stable catalog rows: {missing}") - if extra: - fail(f"{rel(EVIDENCE_MATRIX)} claims public support for non-stable catalog rows: {extra}") - - -def check_extension_recipes() -> None: - retired_recipes_root = ROOT / "src/extensions/recipes" - if retired_recipes_root.exists(): - fail(f"{rel(retired_recipes_root)} is retired; external extension definitions live under src/extensions/external") - external_root = ROOT / "src/extensions/external" - if not external_root.exists(): - fail(f"{rel(external_root)} must exist") - recipe_files = sorted(external_root.glob("*/recipe.toml")) - for recipe in recipe_files: - data = read_toml(recipe) - if data.get("schema") != "oliphaunt-extension-recipe-v1": - fail(f"{rel(recipe)} must use schema = oliphaunt-extension-recipe-v1") - sql_name = validate_sql_extension_name(data.get("sql_name"), f"{rel(recipe)} recipe") - kind = data.get("kind") - if kind not in {"external-simple-pgxs", "external-complex"}: - fail(f"{rel(recipe)} must declare an external recipe kind") - if recipe.parent.name != sql_name: - fail(f"{rel(recipe)} directory must match exact SQL extension name") - for section in ("lifecycle", "artifacts", "support"): - if not isinstance(data.get(section), dict): - fail(f"{rel(recipe)} must declare [{section}]") - recipe_dir = recipe.parent - require_file(recipe_dir / "tests" / "smoke.sql") - targets = recipe_dir / "targets" - if not targets.is_dir() or not any(targets.glob("*.toml")): - fail(f"{rel(recipe)} must declare at least one target TOML under targets/") - if kind == "external-complex": - require_file(recipe_dir / "deps.toml") - require_file(recipe_dir / "tests" / "upstream.toml") - require_file(recipe_dir / "patches" / "README.md") - require_file(recipe_dir / "blockers.toml") - - -def check_sdk_local_extension_rules() -> None: - catalog_ids = { - validate_extension_id(row.get("id"), f"{rel(PROMOTED_CATALOG)} row") - for row in extension_rows(PROMOTED_CATALOG) - } - complex_ids = catalog_ids & {"age", "graph", "pg_search", "pg_textsearch", "postgis", "vector"} - offenders: list[str] = [] - for path in tracked_files("src/sdks/rust", "src/sdks/swift", "src/sdks/kotlin", "src/sdks/react-native", "src/sdks/js"): - if not path.startswith(SDK_RUNTIME_SOURCE_PREFIXES): - continue - if path in TRANSITIONAL_EXTENSION_RULE_FILES or "/generated/" in path: - continue - if "/tests/" in path or "/Tests/" in path or "/__tests__/" in path: - continue - try: - lines = (ROOT / path).read_text(encoding="utf-8").splitlines() - except UnicodeDecodeError: - continue - for line_number, line in enumerate(lines, start=1): - stripped = line.strip() - if (path, stripped) in TRANSITIONAL_EXTENSION_RULE_ALLOWLIST: - continue - for extension_id in complex_ids: - if re.search(rf"['\"`]({re.escape(extension_id)})['\"`]", stripped): - offenders.append(f"{path}:{line_number}: hardcodes extension {extension_id!r}: {stripped}") - if offenders: - fail( - "SDK runtime source must not hardcode complex extension rules outside generated metadata; " - "known transitional exceptions must be explicit:\n" + "\n".join(offenders[:20]) - ) - - -def self_test() -> None: - try: - validate_extension_id("bad-name", "self-test") - except SystemExit: - pass - else: - fail("self-test expected invalid extension id to fail") - - try: - validate_unique_ids(["vector", "vector"], "self-test") - except SystemExit: - pass - else: - fail("self-test expected duplicate extension ids to fail") - - -def check_live_repo() -> None: - check_source_domains() - check_source_spine_policy() - check_xtask_extension_policy() - check_product_roots() - check_forbidden_product_identity_text() - check_forbidden_retired_release_tool_text() - check_extension_catalogs() - check_generated_extension_metadata() - check_extension_evidence() - check_extension_recipes() - check_sdk_local_extension_rules() - - -def parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--self-test", action="store_true", help="run embedded failure-case checks") - return parser.parse_args(argv) - - -def main(argv: list[str]) -> int: - args = parse_args(argv) - if args.self_test: - self_test() - check_live_repo() - print("final source architecture policy checks passed") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) diff --git a/tools/policy/check-repo-structure.sh b/tools/policy/check-repo-structure.sh index 63d3404a..63fc7bea 100755 --- a/tools/policy/check-repo-structure.sh +++ b/tools/policy/check-repo-structure.sh @@ -208,7 +208,7 @@ require_file tools/release/release.py require_file tools/dev/bun.sh require_file tools/dev/doctor.sh require_file tools/policy/check-policy-tools.sh -require_file tools/policy/check-final-source-architecture.py +require_file tools/policy/check-final-source-architecture.mjs require_file tools/policy/assertions/assert-ci-workflows.mjs require_file tools/policy/assertions/assert-moon-task-policy.mjs require_file tools/graph/moon.yml @@ -624,4 +624,4 @@ require_text tools/policy/check-crate-package.sh 'bun tools/policy/list-publisha require_text src/bindings/wasix-rust/tools/check-examples.sh '--target-dir target/oliphaunt-wasix-rust/examples/tauri-sqlx-vanilla/src-tauri' require_text src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh 'oliphaunt_resolve_repo_root' require_text src/runtimes/liboliphaunt/native/bin/common.sh 'git -C "$script_dir" rev-parse --show-toplevel' -python3 tools/policy/check-final-source-architecture.py --self-test +tools/dev/bun.sh tools/policy/check-final-source-architecture.mjs --self-test diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index bc1e5ae8..bef703b9 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -38,6 +38,7 @@ require_file docs/maintainers/tooling.md require_file tools/test/moon.yml require_file tools/test/run-js-tests.mjs require_file tools/graph/cache-witness.mjs +require_file tools/policy/check-final-source-architecture.mjs require_file tools/policy/check-python-entrypoints.mjs require_file tools/policy/check-native-boundaries.mjs require_file tools/policy/python-entrypoints.allowlist @@ -343,6 +344,12 @@ fi if grep -Fq 'python3' tools/dev/bootstrap-tools.sh; then fail "local tool bootstrap must not use Python for archive extraction" fi +if git grep -n 'check-final-source-architecture\.py' -- . ':!tools/policy/check-tooling-stack.sh' >/tmp/oliphaunt-final-source-architecture-python-grep.$$ 2>/dev/null; then + cat /tmp/oliphaunt-final-source-architecture-python-grep.$$ >&2 + rm -f /tmp/oliphaunt-final-source-architecture-python-grep.$$ + fail "final source architecture policy checks must use the Bun entrypoint" +fi +rm -f /tmp/oliphaunt-final-source-architecture-python-grep.$$ grep -Fq 'unzip -q "$archive" -d "$tmp"' tools/dev/bootstrap-tools.sh || fail "local tool bootstrap must extract cargo-binstall zip archives with unzip" grep -Fq 'cargo install ripgrep --version 15.1.0 --locked' .github/actions/setup-rust-tools/action.yml || diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index 6c13b58f..da66fcc6 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -4,7 +4,6 @@ src/extensions/tools/check-extension-model.py tools/coverage/coverage.py tools/graph/ci_plan.py tools/graph/graph.py -tools/policy/check-final-source-architecture.py tools/policy/check-release-policy.py tools/release/artifact_target_matrix.py tools/release/artifact_targets.py From 82c10516ab0955c7c11c6c47983bb7e0f2667a4a Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 19:37:37 +0000 Subject: [PATCH 116/137] chore: port coverage tooling to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 10 + tools/coverage/check-product | 3 +- tools/coverage/coverage.mjs | 1015 +++++++++++++++++ tools/coverage/coverage.py | 805 ------------- tools/coverage/moon.yml | 5 +- tools/coverage/run-product | 3 +- tools/coverage/summarize | 3 +- tools/policy/check-test-strategy.mjs | 6 +- tools/policy/python-entrypoints.allowlist | 1 - 9 files changed, 1037 insertions(+), 814 deletions(-) create mode 100755 tools/coverage/coverage.mjs delete mode 100755 tools/coverage/coverage.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 298916f6..27938ad7 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -63,6 +63,10 @@ until the current-state gates here are checked with fresh local evidence. - [ ] Inventory remaining Python and Rust helper scripts; move nonessential scripts to Bun where that improves local developer experience without making critical product code less idiomatic. +- [ ] Fix or refresh the measured `oliphaunt-js` coverage lane; a fresh + `tools/coverage/run-product oliphaunt-js` attempt stops in Vitest at 70.82% + line coverage against the 80% global threshold before coverage summary + parsing runs. - [ ] Re-run Linux CI-like and release/local-registry lanes after each tooling migration batch. @@ -164,6 +168,12 @@ until the current-state gates here are checked with fresh local evidence. removed from `tools/policy/python-entrypoints.allowlist`, and `check-tooling-stack.sh` now rejects stale references to the retired checker path. +- 2026-06-26: Coverage orchestration now runs through + `tools/coverage/coverage.mjs` and the pinned Bun launcher while keeping the + stable wrapper API (`tools/coverage/run-product`, `check-product`, and + `summarize`). The port preserves the existing lcov, Vitest, Swift JSON, and + Kover report contracts and removes `tools/coverage/coverage.py` from the + intentional Python entrypoint inventory. - 2026-06-26: Rust SDK broker Cargo relay smoke setup now prepares the generated publish source through `python3 tools/release/release.py prepare-rust-release-source` instead of an inline Python heredoc that imports diff --git a/tools/coverage/check-product b/tools/coverage/check-product index 478e6544..45817dd7 100755 --- a/tools/coverage/check-product +++ b/tools/coverage/check-product @@ -1,3 +1,4 @@ #!/usr/bin/env sh set -eu -exec "$(dirname "$0")/coverage.py" check-product "$@" +root="$(git rev-parse --show-toplevel 2>/dev/null)" +exec "$root/tools/dev/bun.sh" "$root/tools/coverage/coverage.mjs" check-product "$@" diff --git a/tools/coverage/coverage.mjs b/tools/coverage/coverage.mjs new file mode 100755 index 00000000..cb0686e1 --- /dev/null +++ b/tools/coverage/coverage.mjs @@ -0,0 +1,1015 @@ +#!/usr/bin/env bun +import { spawnSync } from 'node:child_process'; +import { + constants, + copyFileSync, + existsSync, + mkdirSync, + readFileSync, + readdirSync, + rmSync, + statSync, + accessSync, + writeFileSync, +} from 'node:fs'; +import path from 'node:path'; + +const PRODUCTS = [ + 'oliphaunt-rust', + 'oliphaunt-swift', + 'oliphaunt-kotlin', + 'oliphaunt-js', + 'oliphaunt-react-native', + 'oliphaunt-wasix-rust', +]; + +const PRODUCT_SOURCE_ROOTS = new Map([ + ['oliphaunt-rust', 'src/sdks/rust'], + ['oliphaunt-swift', 'src/sdks/swift'], + ['oliphaunt-kotlin', 'src/sdks/kotlin'], + ['oliphaunt-js', 'src/sdks/js'], + ['oliphaunt-react-native', 'src/sdks/react-native'], + ['oliphaunt-wasix-rust', 'src/bindings/wasix-rust/crates/oliphaunt-wasix'], +]); + +const FORBIDDEN_PATH_PARTS = [ + '/node_modules/', + '/target/', + '/.build/', + '/DerivedData/', + '/build/', + '/.cxx/', + '/generated/', + '/vendor/', +]; + +const ROOT = path.resolve(import.meta.dir, '..', '..'); +const BASELINE = path.join(ROOT, 'coverage/baseline.toml'); +const COVERAGE_ROOT = path.join(ROOT, 'target/coverage'); +const globRegexCache = new Map(); + +function fail(message) { + console.error(`coverage.mjs: ${message}`); + process.exit(1); +} + +function posixPath(value) { + return value.split(path.sep).join('/'); +} + +function relPath(value) { + const raw = String(value); + const resolved = path.isAbsolute(raw) ? path.resolve(raw) : path.resolve(ROOT, raw); + const relative = path.relative(ROOT, resolved); + if (relative && !relative.startsWith('..') && !path.isAbsolute(relative)) { + return posixPath(relative); + } + return posixPath(raw); +} + +function run(command, { cwd = ROOT, env = process.env } = {}) { + console.log(`\n==> ${command.join(' ')}`); + const result = spawnSync(command[0], command.slice(1), { + cwd, + env, + stdio: 'inherit', + }); + if (result.error) { + throw result.error; + } + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + +function capture(command, { cwd = ROOT, env = process.env } = {}) { + console.log(`\n==> ${command.join(' ')}`); + const result = spawnSync(command[0], command.slice(1), { + cwd, + env, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + if (result.error) { + throw result.error; + } + const output = `${result.stdout ?? ''}${result.stderr ?? ''}`; + process.stdout.write(output); + if (result.status !== 0) { + process.exit(result.status ?? 1); + } + return output; +} + +function optionalCapture(command, { cwd = ROOT } = {}) { + const result = spawnSync(command[0], command.slice(1), { + cwd, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }); + if (result.error || result.status !== 0) { + return null; + } + const value = result.stdout.trim(); + return value || null; +} + +function isExecutable(file) { + try { + accessSync(file, constants.X_OK); + return true; + } catch { + return false; + } +} + +function which(name) { + const pathValue = process.env.PATH ?? ''; + const extensions = process.platform === 'win32' + ? (process.env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM').split(';') + : ['']; + for (const directory of pathValue.split(path.delimiter)) { + if (!directory) { + continue; + } + for (const extension of extensions) { + const candidate = path.join(directory, `${name}${extension}`); + if (existsSync(candidate) && statSync(candidate).isFile() && isExecutable(candidate)) { + return candidate; + } + } + } + return null; +} + +function requireTool(name, installHint) { + if (which(name) === null) { + fail(`missing required coverage tool: ${name}\n\nInstall with:\n ${installHint}`); + } +} + +function commandOk(command) { + const result = spawnSync(command[0], command.slice(1), { + cwd: ROOT, + stdio: 'ignore', + }); + return !result.error && result.status === 0; +} + +function loadBaseline() { + if (!existsSync(BASELINE) || !statSync(BASELINE).isFile()) { + fail(`missing coverage baseline: ${relPath(BASELINE)}`); + } + const data = Bun.TOML.parse(readFileSync(BASELINE, 'utf8')); + if (!data.products || typeof data.products !== 'object' || Array.isArray(data.products)) { + fail('coverage baseline must define [products.] tables'); + } + return data; +} + +function productConfig(product) { + const data = loadBaseline(); + const config = data.products[product]; + if (!config || typeof config !== 'object' || Array.isArray(config)) { + fail(`coverage baseline does not define product ${JSON.stringify(product)}`); + } + return config; +} + +function outputDir(product) { + return path.join(COVERAGE_ROOT, product); +} + +function productSourceRoot(product) { + const source = PRODUCT_SOURCE_ROOTS.get(product); + if (source === undefined) { + fail(`missing source root mapping for coverage product ${product}`); + } + return path.join(ROOT, source); +} + +function productSourcePrefix(product) { + return relPath(productSourceRoot(product)); +} + +function resetOutput(product) { + const out = outputDir(product); + rmSync(out, { recursive: true, force: true }); + mkdirSync(out, { recursive: true }); + return out; +} + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'); +} + +function repoGlobRegex(pattern) { + const normalized = pattern.replaceAll(path.sep, '/'); + const cached = globRegexCache.get(normalized); + if (cached !== undefined) { + return cached; + } + const parts = ['^']; + let index = 0; + while (index < normalized.length) { + const char = normalized[index]; + if (char === '*') { + if (index + 1 < normalized.length && normalized[index + 1] === '*') { + index += 2; + if (index < normalized.length && normalized[index] === '/') { + index += 1; + parts.push('(?:.*/)?'); + } else { + parts.push('.*'); + } + continue; + } + parts.push('[^/]*'); + } else if (char === '?') { + parts.push('[^/]'); + } else { + parts.push(escapeRegExp(char)); + } + index += 1; + } + parts.push('$'); + const regex = new RegExp(parts.join(''), 'u'); + globRegexCache.set(normalized, regex); + return regex; +} + +function matchesAny(file, patterns) { + const normalized = file.replaceAll(path.sep, '/'); + return patterns.some((pattern) => repoGlobRegex(pattern).test(normalized)); +} + +function sourceGlobs(config) { + const globs = config.source_globs; + if (!Array.isArray(globs) || globs.length === 0 || !globs.every((item) => typeof item === 'string')) { + fail('coverage product config must define non-empty source_globs'); + } + return globs; +} + +function excludeGlobs(config) { + const globs = config.exclude_globs ?? []; + if (!Array.isArray(globs) || !globs.every((item) => typeof item === 'string')) { + fail('coverage product config exclude_globs must be a list of strings'); + } + return globs; +} + +function waiverEntries(config) { + const entries = config.waivers ?? []; + if (!Array.isArray(entries)) { + fail('coverage waivers must be an array of tables'); + } + return entries.map((entry) => { + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) { + fail('coverage waiver entries must be tables'); + } + const exact = entry.path; + const pattern = entry.glob; + if ((exact === undefined) === (pattern === undefined)) { + fail('coverage waiver must define exactly one of path or glob'); + } + for (const [key, value] of [ + ['path/glob', exact ?? pattern], + ['reason', entry.reason], + ['evidence', entry.evidence], + ['owner', entry.owner], + ['expires', entry.expires], + ]) { + if (typeof value !== 'string') { + fail(`coverage waiver ${key}, reason, evidence, owner, and expires must be strings`); + } + if (key !== 'path/glob' && value.trim() === '') { + fail('coverage waiver reason, evidence, owner, and expires must be non-empty'); + } + } + return { + path: exact ?? '', + glob: pattern ?? '', + reason: entry.reason, + evidence: entry.evidence, + owner: entry.owner, + expires: entry.expires, + }; + }); +} + +function waiverPatterns(config) { + return waiverEntries(config).map((waiver) => waiver.path || waiver.glob); +} + +function isWaived(file, config) { + const relative = relPath(file); + for (const waiver of waiverEntries(config)) { + if (waiver.path && relative === waiver.path) { + return true; + } + if (waiver.glob && matchesAny(relative, [waiver.glob])) { + return true; + } + } + return false; +} + +function allowedFile(file, config) { + const relative = relPath(file); + const normalized = `/${relative}`; + if (!matchesAny(relative, sourceGlobs(config))) { + return false; + } + if (matchesAny(relative, excludeGlobs(config))) { + return false; + } + if (isWaived(relative, config)) { + return false; + } + return !FORBIDDEN_PATH_PARTS.some((part) => normalized.includes(part)); +} + +function staticGlobPrefix(pattern) { + const wildcardIndex = pattern.search(/[*?]/u); + if (wildcardIndex === -1) { + return pattern; + } + const slashIndex = pattern.lastIndexOf('/', wildcardIndex); + return slashIndex === -1 ? '.' : pattern.slice(0, slashIndex); +} + +function walkFiles(root) { + if (!existsSync(root)) { + return []; + } + const files = []; + const stack = [root]; + while (stack.length > 0) { + const current = stack.pop(); + let entries; + try { + entries = readdirSync(current, { withFileTypes: true }); + } catch { + continue; + } + for (const entry of entries) { + const child = path.join(current, entry.name); + if (entry.isDirectory()) { + stack.push(child); + } else if (entry.isFile()) { + files.push(child); + } + } + } + return files.sort(); +} + +function trackedOrLocalSourceFiles(config) { + const files = new Set(); + for (const pattern of sourceGlobs(config)) { + const prefix = staticGlobPrefix(pattern); + for (const candidate of walkFiles(path.join(ROOT, prefix))) { + const relative = relPath(candidate); + if (matchesAny(relative, [pattern])) { + files.add(relative); + } + } + } + return [...files].sort(); +} + +function validateWaivers(config) { + const files = trackedOrLocalSourceFiles(config); + for (const waiver of waiverEntries(config)) { + const matched = files.filter((file) => + (waiver.path && file === waiver.path) || + (waiver.glob && matchesAny(file, [waiver.glob])) + ); + if (matched.length === 0) { + fail(`coverage waiver does not match an owned source file: ${waiver.path || waiver.glob}`); + } + } + return waiverEntries(config); +} + +function ownedUnwaivedSourceFiles(config) { + validateWaivers(config); + const owned = []; + for (const file of trackedOrLocalSourceFiles(config)) { + const normalized = `/${file}`; + if (matchesAny(file, excludeGlobs(config))) { + continue; + } + if (isWaived(file, config)) { + continue; + } + if (FORBIDDEN_PATH_PARTS.some((part) => normalized.includes(part))) { + continue; + } + owned.push(file); + } + return owned.sort(); +} + +function percent(covered, total) { + if (total <= 0) { + return 0.0; + } + return Math.round((covered / total) * 10000) / 100; +} + +function parseLcov(reportPath, config) { + const files = []; + let currentFile = null; + let currentLines = new Map(); + const flush = () => { + if (currentFile === null) { + return; + } + if (allowedFile(currentFile, config)) { + const total = currentLines.size; + const covered = [...currentLines.values()].filter((count) => count > 0).length; + if (total > 0) { + files.push({ path: relPath(currentFile), covered_lines: covered, total_lines: total }); + } + } + currentFile = null; + currentLines = new Map(); + }; + for (const rawLine of readFileSync(reportPath, 'utf8').split(/\r?\n/u)) { + const line = rawLine.trimEnd(); + if (line.startsWith('SF:')) { + flush(); + currentFile = line.slice(3); + } else if (line.startsWith('DA:') && currentFile !== null) { + const [lineNo, count] = line.slice(3).split(','); + currentLines.set(Number.parseInt(lineNo, 10), Number.parseInt(count, 10)); + } else if (line === 'end_of_record') { + flush(); + } + } + flush(); + const covered = files.reduce((sum, file) => sum + file.covered_lines, 0); + const total = files.reduce((sum, file) => sum + file.total_lines, 0); + return { covered, total, files }; +} + +function normalizeJavascriptReportPath(product, rawPath) { + if (path.isAbsolute(rawPath)) { + return rawPath; + } + const sourcePrefix = productSourcePrefix(product); + if (rawPath.startsWith(`${sourcePrefix}/`)) { + return rawPath; + } + return `${sourcePrefix}/${rawPath}`; +} + +function parseJavascriptSummary(reportPath, product, config) { + const data = JSON.parse(readFileSync(reportPath, 'utf8')); + const files = []; + for (const [rawPath, entry] of Object.entries(data)) { + const sourcePath = normalizeJavascriptReportPath(product, rawPath); + if (rawPath === 'total' || !allowedFile(sourcePath, config)) { + continue; + } + const lines = entry.lines ?? {}; + const total = Number.parseInt(lines.total ?? 0, 10); + const covered = Number.parseInt(lines.covered ?? 0, 10); + if (total > 0) { + files.push({ path: relPath(sourcePath), covered_lines: covered, total_lines: total }); + } + } + return { + covered: files.reduce((sum, file) => sum + file.covered_lines, 0), + total: files.reduce((sum, file) => sum + file.total_lines, 0), + files, + }; +} + +function xmlUnescape(value) { + return value + .replaceAll('"', '"') + .replaceAll(''', "'") + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('&', '&'); +} + +function parseXmlAttributes(raw) { + const attributes = new Map(); + for (const match of raw.matchAll(/([A-Za-z_:][\w:.-]*)\s*=\s*"([^"]*)"/gu)) { + attributes.set(match[1], xmlUnescape(match[2])); + } + return attributes; +} + +function resolveKoverSourcePath(packageName, sourceFileName) { + const packagePath = packageName.replaceAll('.', '/'); + const sourceRoot = path.join(productSourceRoot('oliphaunt-kotlin'), 'oliphaunt/src'); + const candidates = walkFiles(sourceRoot) + .filter((candidate) => posixPath(candidate).endsWith(`${packagePath}/${sourceFileName}`)) + .sort(); + const sourceCandidates = candidates.filter((candidate) => !candidate.split(path.sep).includes('Test')); + if (sourceCandidates.length > 0) { + return relPath(sourceCandidates[0]); + } + if (candidates.length > 0) { + return relPath(candidates[0]); + } + return `src/sdks/kotlin/oliphaunt/src/${packagePath}/${sourceFileName}`; +} + +function parseKoverXml(reportPath, config) { + const xml = readFileSync(reportPath, 'utf8'); + const files = []; + for (const packageMatch of xml.matchAll(/]*)>([\s\S]*?)<\/package>/gu)) { + const packageName = parseXmlAttributes(packageMatch[1]).get('name') ?? ''; + for (const sourceMatch of packageMatch[2].matchAll(/]*)>([\s\S]*?)<\/sourcefile>/gu)) { + const sourceFileName = parseXmlAttributes(sourceMatch[1]).get('name') ?? ''; + const sourcePath = resolveKoverSourcePath(packageName, sourceFileName); + if (!allowedFile(sourcePath, config)) { + continue; + } + const lines = [...sourceMatch[2].matchAll(/]*)\/?>/gu)]; + const total = lines.length; + const covered = lines.filter((line) => { + const attributes = parseXmlAttributes(line[1]); + return Number.parseInt(attributes.get('ci') ?? '0', 10) > 0; + }).length; + if (total > 0) { + files.push({ path: sourcePath, covered_lines: covered, total_lines: total }); + } + } + } + return { + covered: files.reduce((sum, file) => sum + file.covered_lines, 0), + total: files.reduce((sum, file) => sum + file.total_lines, 0), + files, + }; +} + +function parseSwiftJson(reportPath, config) { + const data = JSON.parse(readFileSync(reportPath, 'utf8')); + const files = []; + for (const report of data.data ?? []) { + for (const fileEntry of report.files ?? []) { + const filename = fileEntry.filename ?? fileEntry.name; + if (!filename || !allowedFile(filename, config)) { + continue; + } + const lines = fileEntry.summary?.lines ?? {}; + const total = Number.parseInt(lines.count ?? lines.total ?? 0, 10); + const covered = Number.parseInt(lines.covered ?? 0, 10); + if (total > 0) { + files.push({ path: relPath(filename), covered_lines: covered, total_lines: total }); + } + } + } + return { + covered: files.reduce((sum, file) => sum + file.covered_lines, 0), + total: files.reduce((sum, file) => sum + file.total_lines, 0), + files, + }; +} + +function sortForJson(value) { + if (Array.isArray(value)) { + return value.map(sortForJson); + } + if (value && typeof value === 'object') { + return Object.fromEntries( + Object.entries(value) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, item]) => [key, sortForJson(item)]), + ); + } + return value; +} + +function writeJson(file, value) { + writeFileSync(file, `${JSON.stringify(sortForJson(value), null, 2)}\n`); +} + +function writeSummary(product, tool, coveredLines, totalLines, files, reports) { + const out = outputDir(product); + const config = productConfig(product); + files.sort((left, right) => left.path.localeCompare(right.path)); + const summary = { + schema: 'oliphaunt-coverage-summary-v1', + product, + tool, + line_coverage: percent(coveredLines, totalLines), + line_threshold: Number.parseFloat(config.line_threshold), + covered_lines: coveredLines, + total_lines: totalLines, + files, + reports: reports.map(relPath), + source_globs: sourceGlobs(config), + exclude_globs: excludeGlobs(config), + waived_files: waiverEntries(config).map((waiver) => ({ + path: waiver.path || waiver.glob, + reason: waiver.reason, + evidence: waiver.evidence, + owner: waiver.owner, + expires: waiver.expires, + })), + }; + const summaryPath = path.join(out, 'summary.json'); + writeJson(summaryPath, summary); + return summaryPath; +} + +function checkSummary(product) { + const config = productConfig(product); + const summaryPath = path.join(ROOT, config.summary); + if (!existsSync(summaryPath) || !statSync(summaryPath).isFile()) { + fail(`${product}: missing measured coverage summary ${relPath(summaryPath)}`); + } + const summary = JSON.parse(readFileSync(summaryPath, 'utf8')); + if (summary.product !== product) { + fail(`${product}: coverage summary product mismatch`); + } + const total = Number.parseInt(summary.total_lines ?? 0, 10); + const covered = Number.parseInt(summary.covered_lines ?? 0, 10); + if (total <= 0 || covered <= 0) { + fail(`${product}: coverage summary is unmeasured: covered=${covered} total=${total}`); + } + const files = summary.files; + if (!Array.isArray(files) || files.length === 0) { + fail(`${product}: coverage summary contains no measured source files`); + } + const measured = Number.parseFloat(summary.line_coverage ?? 0.0); + const threshold = Number.parseFloat(config.line_threshold); + const committedMeasured = Number.parseFloat(config.measured_line_coverage ?? 0.0); + if (committedMeasured < threshold) { + fail(`${product}: committed measured_line_coverage is below line_threshold`); + } + if (measured + 0.005 < threshold) { + fail(`${product}: line coverage ${measured.toFixed(2)}% is below threshold ${threshold.toFixed(2)}%`); + } + const summaryReports = new Set(summary.reports ?? []); + for (const report of config.reports ?? []) { + if (!summaryReports.has(report)) { + fail(`${product}: coverage summary is missing expected report ${report}`); + } + } + for (const report of summaryReports) { + const reportPath = path.join(ROOT, report); + if (!existsSync(reportPath) || !statSync(reportPath).isFile() || statSync(reportPath).size === 0) { + fail(`${product}: missing or empty coverage report ${report}`); + } + } + for (const file of files) { + const sourcePath = file.path ?? ''; + const normalized = `/${sourcePath}`; + if (FORBIDDEN_PATH_PARTS.some((part) => normalized.includes(part))) { + fail(`${product}: coverage includes generated/vendor/build path ${sourcePath}`); + } + if (!allowedFile(sourcePath, config)) { + fail(`${product}: coverage includes a source path outside the baseline scope: ${sourcePath}`); + } + } + const perFileThreshold = Number.parseFloat(config.per_file_line_threshold ?? 0.0); + if (perFileThreshold > 0.0) { + for (const file of files) { + const sourcePath = file.path ?? ''; + const fileTotal = Number.parseInt(file.total_lines ?? 0, 10); + const fileCovered = Number.parseInt(file.covered_lines ?? 0, 10); + const filePercent = percent(fileCovered, fileTotal); + if (filePercent + 0.005 < perFileThreshold) { + fail(`${product}: ${sourcePath} line coverage ${filePercent.toFixed(2)}% is below per-file threshold ${perFileThreshold.toFixed(2)}%`); + } + } + } + const measuredPaths = new Set(files.map((file) => file.path ?? '')); + const missingOwned = ownedUnwaivedSourceFiles(config).filter((file) => !measuredPaths.has(file)); + if (missingOwned.length > 0) { + fail( + `${product}: owned source files are neither measured nor waived: ` + + missingOwned.slice(0, 20).join(', ') + + (missingOwned.length > 20 ? ' ...' : ''), + ); + } + return summary; +} + +function runRust(product) { + const packageName = product === 'oliphaunt-rust' ? 'oliphaunt' : 'oliphaunt-wasix'; + const out = resetOutput(product); + const lcov = path.join(out, 'lcov.info'); + requireTool('cargo', 'rustup toolchain install 1.93'); + if (!commandOk(['cargo', 'llvm-cov', '--version'])) { + fail('missing required coverage tool: cargo-llvm-cov\n\nInstall with:\n cargo install cargo-llvm-cov'); + } + if (!commandOk(['cargo', 'nextest', '--version'])) { + fail('missing required coverage tool: cargo-nextest\n\nInstall with:\n cargo install cargo-nextest --locked'); + } + const env = { ...process.env }; + if (env.LLVM_COV === undefined) { + const llvmCov = which('llvm-cov') ?? optionalCapture(['xcrun', '--find', 'llvm-cov']); + if (llvmCov) { + env.LLVM_COV = llvmCov; + } + } + if (env.LLVM_PROFDATA === undefined) { + const llvmProfdata = which('llvm-profdata') ?? optionalCapture(['xcrun', '--find', 'llvm-profdata']); + if (llvmProfdata) { + env.LLVM_PROFDATA = llvmProfdata; + } + } + const featureArgs = product === 'oliphaunt-wasix-rust' ? ['--no-default-features'] : []; + const targetArgs = product === 'oliphaunt-wasix-rust' ? ['--lib'] : []; + run(['cargo', 'llvm-cov', 'clean', '--profraw-only'], { env }); + run( + [ + 'cargo', + 'llvm-cov', + 'nextest', + '--package', + packageName, + ...targetArgs, + ...featureArgs, + '--locked', + '--profile', + 'ci', + '--no-tests=fail', + '--test-threads=1', + '--no-report', + ], + { env }, + ); + run(['cargo', 'test', '--doc', '--package', packageName, '--locked'], { env }); + run(['cargo', 'llvm-cov', 'report', '--lcov', '--output-path', lcov], { env }); + const parsed = parseLcov(lcov, productConfig(product)); + writeSummary(product, 'cargo-llvm-cov', parsed.covered, parsed.total, parsed.files, [lcov]); + checkSummary(product); +} + +function runSwift() { + const out = resetOutput('oliphaunt-swift'); + const scratch = path.join(ROOT, 'target/coverage-build/oliphaunt-swift'); + rmSync(scratch, { recursive: true, force: true }); + requireTool('swift', 'Install Xcode or the Swift toolchain'); + run([ + 'swift', + 'test', + '--package-path', + ROOT, + '--scratch-path', + scratch, + '--enable-code-coverage', + ]); + const output = capture([ + 'swift', + 'test', + '--package-path', + ROOT, + '--scratch-path', + scratch, + '--show-codecov-path', + ]); + let candidates = output + .split(/\r?\n/u) + .map((line) => line.trim()) + .filter((line) => line.endsWith('.json') && existsSync(line) && statSync(line).isFile()); + if (candidates.length === 0) { + candidates = walkFiles(scratch).filter((candidate) => candidate.endsWith('.json')); + } + if (candidates.length === 0) { + fail('oliphaunt-swift: swift test did not emit a code coverage JSON path'); + } + const report = path.join(out, 'swift-coverage.json'); + copyFileSync(candidates.at(-1), report); + const parsed = parseSwiftJson(report, productConfig('oliphaunt-swift')); + writeSummary('oliphaunt-swift', 'swift test --enable-code-coverage', parsed.covered, parsed.total, parsed.files, [report]); + checkSummary('oliphaunt-swift'); +} + +function runKotlin() { + const out = resetOutput('oliphaunt-kotlin'); + requireTool('java', 'Install JDK 17'); + const packageDir = productSourceRoot('oliphaunt-kotlin'); + const gradle = path.join(packageDir, 'gradlew'); + const buildRoot = path.join(ROOT, 'target/coverage-build/oliphaunt-kotlin/gradle'); + const cxxBuildRoot = path.join(ROOT, 'target/coverage-build/oliphaunt-kotlin/cxx'); + const projectCache = path.join(ROOT, 'target/coverage-build/oliphaunt-kotlin/gradle-cache'); + rmSync(buildRoot, { recursive: true, force: true }); + rmSync(cxxBuildRoot, { recursive: true, force: true }); + run([ + gradle, + '-p', + relPath(packageDir), + ':oliphaunt:koverXmlReport', + ':oliphaunt:koverVerify', + '--no-daemon', + `-PoliphauntBuildRoot=${buildRoot}`, + `-PoliphauntCxxBuildRoot=${cxxBuildRoot}`, + '--project-cache-dir', + projectCache, + ]); + let reports = walkFiles(buildRoot) + .filter((candidate) => posixPath(candidate).includes('/reports/kover/') && candidate.endsWith('.xml')) + .sort(); + if (reports.length === 0) { + reports = walkFiles(packageDir) + .filter((candidate) => posixPath(candidate).includes('/build/reports/kover/') && candidate.endsWith('.xml')) + .sort(); + } + if (reports.length === 0) { + fail('oliphaunt-kotlin: Kover did not emit an XML report'); + } + const report = path.join(out, 'kover.xml'); + copyFileSync(reports.at(-1), report); + const parsed = parseKoverXml(report, productConfig('oliphaunt-kotlin')); + writeSummary('oliphaunt-kotlin', 'kover', parsed.covered, parsed.total, parsed.files, [report]); + checkSummary('oliphaunt-kotlin'); +} + +function runJavascript(product) { + const out = resetOutput(product); + const packageDir = productSourceRoot(product); + requireTool('pnpm', 'corepack enable && corepack prepare pnpm@11.5.0 --activate'); + const config = productConfig(product); + const threshold = String(Math.trunc(Number.parseFloat(config.line_threshold))); + const sourcePrefix = `${productSourcePrefix(product)}/`; + const includePatterns = sourceGlobs(config).map((pattern) => + pattern.startsWith(sourcePrefix) ? pattern.slice(sourcePrefix.length) : pattern + ); + const excludePatterns = [...excludeGlobs(config), ...waiverPatterns(config)].map((pattern) => + pattern.startsWith(sourcePrefix) ? pattern.slice(sourcePrefix.length) : pattern + ); + const env = { + ...process.env, + OLIPHAUNT_VITEST_COVERAGE: '1', + OLIPHAUNT_VITEST_COVERAGE_DIR: out, + OLIPHAUNT_VITEST_COVERAGE_INCLUDE: JSON.stringify(includePatterns), + OLIPHAUNT_VITEST_COVERAGE_EXCLUDE: JSON.stringify(excludePatterns), + OLIPHAUNT_VITEST_COVERAGE_LINES: threshold, + }; + run(['pnpm', '--dir', packageDir, 'test'], { env }); + const summaryReport = path.join(out, 'coverage-summary.json'); + if (!existsSync(summaryReport) || !statSync(summaryReport).isFile()) { + fail(`${product}: Vitest did not emit ${relPath(summaryReport)}`); + } + const parsed = parseJavascriptSummary(summaryReport, product, config); + const reports = [summaryReport]; + const lcov = path.join(out, 'lcov.info'); + if (existsSync(lcov) && statSync(lcov).isFile()) { + reports.push(lcov); + } + writeSummary(product, 'vitest-v8', parsed.covered, parsed.total, parsed.files, reports); + checkSummary(product); +} + +function runProduct(product) { + if (!PRODUCTS.includes(product)) { + fail(`unknown product ${JSON.stringify(product)}; expected one of ${PRODUCTS.join(', ')}`); + } + if (product === 'oliphaunt-rust' || product === 'oliphaunt-wasix-rust') { + runRust(product); + } else if (product === 'oliphaunt-swift') { + runSwift(); + } else if (product === 'oliphaunt-kotlin') { + runKotlin(); + } else if (product === 'oliphaunt-js' || product === 'oliphaunt-react-native') { + runJavascript(product); + } else { + fail(`unhandled coverage product ${product}`); + } +} + +function parseProductsJson(value) { + if (value === undefined || value.trim() === '') { + return [...PRODUCTS]; + } + let parsed; + try { + parsed = JSON.parse(value); + } catch (error) { + fail(`coverage products JSON is invalid: ${error.message}`); + } + if (!Array.isArray(parsed) || !parsed.every((item) => typeof item === 'string')) { + fail('coverage products JSON must be a string array'); + } + const unknown = [...new Set(parsed.filter((item) => !PRODUCTS.includes(item)))].sort(); + if (unknown.length > 0) { + fail(`unknown coverage product(s): ${unknown.join(', ')}`); + } + return [...new Set(parsed)].sort((left, right) => PRODUCTS.indexOf(left) - PRODUCTS.indexOf(right)); +} + +function summarize({ allowMissing = false, productsJson } = {}) { + const data = loadBaseline(); + const products = data.products; + const selectedProducts = parseProductsJson(productsJson); + const rows = []; + const allSummaries = []; + for (const product of selectedProducts) { + if (!Object.hasOwn(products, product)) { + if (data.policy?.fail_on_unmeasured_product ?? true) { + fail(`missing coverage baseline for ${product}`); + } + continue; + } + const summaryPath = path.join(ROOT, products[product].summary); + if (allowMissing && (!existsSync(summaryPath) || !statSync(summaryPath).isFile())) { + continue; + } + if (!existsSync(summaryPath) || !statSync(summaryPath).isFile()) { + fail(`missing required coverage summary: ${relPath(summaryPath)}`); + } + const summary = checkSummary(product); + allSummaries.push(summary); + rows.push( + `| ${summary.product} | ${summary.tool} | ${summary.line_coverage.toFixed(2)}% | ` + + `${summary.line_threshold.toFixed(2)}% | ${summary.covered_lines}/${summary.total_lines} |`, + ); + } + mkdirSync(COVERAGE_ROOT, { recursive: true }); + writeJson(path.join(COVERAGE_ROOT, 'summary.json'), { + schema: 'oliphaunt-coverage-aggregate-v1', + products: allSummaries, + }); + const markdown = [ + '| Product | Tool | Lines | Threshold | Covered |', + '| --- | --- | ---: | ---: | ---: |', + ...rows, + '', + ].join('\n'); + writeFileSync(path.join(COVERAGE_ROOT, 'summary.md'), markdown); + console.log(markdown); +} + +function checkTools() { + const data = loadBaseline(); + for (const product of PRODUCTS) { + if (!data.products[product]) { + fail(`missing coverage baseline for ${product}`); + } + validateWaivers(data.products[product]); + sourceGlobs(data.products[product]); + excludeGlobs(data.products[product]); + } + console.log('coverage tooling checks passed'); +} + +function usage() { + return `usage: + tools/coverage/coverage.mjs run-product + tools/coverage/coverage.mjs check-product + tools/coverage/coverage.mjs summarize [--allow-missing] [--products-json JSON] + tools/coverage/coverage.mjs check-tools`; +} + +function parseArgs(argv) { + const [command, ...rest] = argv; + if (command === undefined || command === '-h' || command === '--help') { + console.log(usage()); + process.exit(0); + } + if (command === 'run-product' || command === 'check-product') { + if (rest.length !== 1 || !PRODUCTS.includes(rest[0])) { + fail(`${command} requires one product: ${PRODUCTS.join(', ')}`); + } + return { command, product: rest[0] }; + } + if (command === 'summarize') { + const options = { command, allowMissing: false, productsJson: undefined }; + for (let index = 0; index < rest.length; index += 1) { + const arg = rest[index]; + if (arg === '--allow-missing') { + options.allowMissing = true; + } else if (arg === '--products-json') { + index += 1; + if (index >= rest.length) { + fail('--products-json requires a value'); + } + options.productsJson = rest[index]; + } else { + fail(`unknown summarize argument: ${arg}`); + } + } + return options; + } + if (command === 'check-tools') { + if (rest.length !== 0) { + fail('check-tools does not take arguments'); + } + return { command }; + } + fail(`unknown command: ${command}\n${usage()}`); +} + +const args = parseArgs(Bun.argv.slice(2)); +if (args.command === 'run-product') { + runProduct(args.product); +} else if (args.command === 'check-product') { + const summary = checkSummary(args.product); + console.log(`${args.product}: ${summary.line_coverage.toFixed(2)}% line coverage`); +} else if (args.command === 'summarize') { + summarize({ allowMissing: args.allowMissing, productsJson: args.productsJson }); +} else if (args.command === 'check-tools') { + checkTools(); +} diff --git a/tools/coverage/coverage.py b/tools/coverage/coverage.py deleted file mode 100755 index 306bf775..00000000 --- a/tools/coverage/coverage.py +++ /dev/null @@ -1,805 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import argparse -import json -import os -import re -import shutil -import subprocess -import sys -import tomllib -import xml.etree.ElementTree as ET -from functools import lru_cache -from pathlib import Path -from typing import Any - - -PRODUCTS = ( - "oliphaunt-rust", - "oliphaunt-swift", - "oliphaunt-kotlin", - "oliphaunt-js", - "oliphaunt-react-native", - "oliphaunt-wasix-rust", -) - -PRODUCT_SOURCE_ROOTS = { - "oliphaunt-rust": "src/sdks/rust", - "oliphaunt-swift": "src/sdks/swift", - "oliphaunt-kotlin": "src/sdks/kotlin", - "oliphaunt-js": "src/sdks/js", - "oliphaunt-react-native": "src/sdks/react-native", - "oliphaunt-wasix-rust": "src/bindings/wasix-rust/crates/oliphaunt-wasix", -} - -FORBIDDEN_PATH_PARTS = ( - "/node_modules/", - "/target/", - "/.build/", - "/DerivedData/", - "/build/", - "/.cxx/", - "/generated/", - "/vendor/", -) - - -def repo_root() -> Path: - return Path(__file__).resolve().parents[2] - - -ROOT = repo_root() -BASELINE = ROOT / "coverage" / "baseline.toml" -COVERAGE_ROOT = ROOT / "target" / "coverage" - - -def fail(message: str) -> None: - raise SystemExit(message) - - -def run(command: list[str], *, cwd: Path = ROOT, env: dict[str, str] | None = None) -> None: - print(f"\n==> {' '.join(command)}", flush=True) - subprocess.run(command, cwd=cwd, env=env, check=True) - - -def capture(command: list[str], *, cwd: Path = ROOT, env: dict[str, str] | None = None) -> str: - print(f"\n==> {' '.join(command)}", flush=True) - result = subprocess.run( - command, - cwd=cwd, - env=env, - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - ) - print(result.stdout, end="") - return result.stdout - - -def optional_capture(command: list[str], *, cwd: Path = ROOT) -> str | None: - try: - result = subprocess.run( - command, - cwd=cwd, - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - text=True, - ) - except FileNotFoundError: - return None - if result.returncode != 0: - return None - value = result.stdout.strip() - return value or None - - -def require_tool(name: str, install_hint: str) -> None: - if shutil.which(name) is None: - fail(f"missing required coverage tool: {name}\n\nInstall with:\n {install_hint}") - - -def load_baseline() -> dict[str, Any]: - if not BASELINE.is_file(): - fail(f"missing coverage baseline: {BASELINE.relative_to(ROOT)}") - with BASELINE.open("rb") as handle: - data = tomllib.load(handle) - products = data.get("products") - if not isinstance(products, dict): - fail("coverage baseline must define [products.] tables") - return data - - -def product_config(product: str) -> dict[str, Any]: - data = load_baseline() - config = data["products"].get(product) - if not isinstance(config, dict): - fail(f"coverage baseline does not define product {product!r}") - return config - - -def output_dir(product: str) -> Path: - return COVERAGE_ROOT / product - - -def product_source_root(product: str) -> Path: - source = PRODUCT_SOURCE_ROOTS.get(product) - if source is None: - fail(f"missing source root mapping for coverage product {product}") - return ROOT / source - - -def product_source_prefix(product: str) -> str: - return product_source_root(product).relative_to(ROOT).as_posix() - - -def reset_output(product: str) -> Path: - out = output_dir(product) - shutil.rmtree(out, ignore_errors=True) - out.mkdir(parents=True, exist_ok=True) - return out - - -def rel_path(path: str | Path) -> str: - raw = Path(path) - try: - return raw.resolve().relative_to(ROOT).as_posix() - except (OSError, ValueError): - return raw.as_posix() - - -@lru_cache(maxsize=512) -def repo_glob_regex(pattern: str) -> re.Pattern[str]: - normalized = pattern.replace(os.sep, "/") - parts: list[str] = ["^"] - index = 0 - while index < len(normalized): - char = normalized[index] - if char == "*": - if index + 1 < len(normalized) and normalized[index + 1] == "*": - index += 2 - if index < len(normalized) and normalized[index] == "/": - index += 1 - parts.append("(?:.*/)?") - else: - parts.append(".*") - continue - parts.append("[^/]*") - elif char == "?": - parts.append("[^/]") - else: - parts.append(re.escape(char)) - index += 1 - parts.append("$") - return re.compile("".join(parts)) - - -def matches_any(path: str, patterns: list[str]) -> bool: - normalized = path.replace(os.sep, "/") - return any(repo_glob_regex(pattern).match(normalized) is not None for pattern in patterns) - - -def source_globs(config: dict[str, Any]) -> list[str]: - globs = config.get("source_globs") - if not isinstance(globs, list) or not all(isinstance(item, str) for item in globs) or not globs: - fail("coverage product config must define non-empty source_globs") - return globs - - -def exclude_globs(config: dict[str, Any]) -> list[str]: - globs = config.get("exclude_globs") or [] - if not isinstance(globs, list) or not all(isinstance(item, str) for item in globs): - fail("coverage product config exclude_globs must be a list of strings") - return globs - - -def waiver_entries(config: dict[str, Any]) -> list[dict[str, str]]: - entries = config.get("waivers") or [] - if not isinstance(entries, list): - fail("coverage waivers must be an array of tables") - normalized = [] - for entry in entries: - if not isinstance(entry, dict): - fail("coverage waiver entries must be tables") - path = entry.get("path") - pattern = entry.get("glob") - reason = entry.get("reason") - evidence = entry.get("evidence") - owner = entry.get("owner") - expires = entry.get("expires") - if (path is None) == (pattern is None): - fail("coverage waiver must define exactly one of path or glob") - if ( - not isinstance(path or pattern, str) - or not isinstance(reason, str) - or not isinstance(evidence, str) - or not isinstance(owner, str) - or not isinstance(expires, str) - ): - fail("coverage waiver path/glob, reason, evidence, owner, and expires must be strings") - if not reason.strip() or not evidence.strip() or not owner.strip() or not expires.strip(): - fail("coverage waiver reason, evidence, owner, and expires must be non-empty") - normalized.append( - { - "path": path or "", - "glob": pattern or "", - "reason": reason, - "evidence": evidence, - "owner": owner, - "expires": expires, - } - ) - return normalized - - -def waiver_patterns(config: dict[str, Any]) -> list[str]: - patterns: list[str] = [] - for waiver in waiver_entries(config): - patterns.append(waiver["path"] or waiver["glob"]) - return patterns - - -def is_waived(path: str | Path, config: dict[str, Any]) -> bool: - relative = rel_path(path) - for waiver in waiver_entries(config): - exact = waiver["path"] - pattern = waiver["glob"] - if exact and relative == exact: - return True - if pattern and matches_any(relative, [pattern]): - return True - return False - - -def allowed_file(path: str | Path, config: dict[str, Any]) -> bool: - relative = rel_path(path) - normalized = f"/{relative}" - if not matches_any(relative, source_globs(config)): - return False - if matches_any(relative, exclude_globs(config)): - return False - if is_waived(relative, config): - return False - return not any(part in normalized for part in FORBIDDEN_PATH_PARTS) - - -def tracked_or_local_source_files(config: dict[str, Any]) -> list[str]: - files: set[str] = set() - for pattern in source_globs(config): - for candidate in ROOT.glob(pattern): - if candidate.is_file(): - files.add(rel_path(candidate)) - return sorted(files) - - -def validate_waivers(config: dict[str, Any]) -> list[dict[str, str]]: - files = tracked_or_local_source_files(config) - for waiver in waiver_entries(config): - exact = waiver["path"] - pattern = waiver["glob"] - matched = [file for file in files if (exact and file == exact) or (pattern and matches_any(file, [pattern]))] - if not matched: - target = exact or pattern - fail(f"coverage waiver does not match an owned source file: {target}") - return waiver_entries(config) - - -def owned_unwaived_source_files(config: dict[str, Any]) -> list[str]: - validate_waivers(config) - owned = [] - for file in tracked_or_local_source_files(config): - normalized = f"/{file}" - if matches_any(file, exclude_globs(config)): - continue - if is_waived(file, config): - continue - if any(part in normalized for part in FORBIDDEN_PATH_PARTS): - continue - owned.append(file) - return sorted(owned) - - -def percent(covered: int, total: int) -> float: - if total <= 0: - return 0.0 - return round((covered / total) * 100.0, 2) - - -def parse_lcov(path: Path, config: dict[str, Any]) -> tuple[int, int, list[dict[str, Any]]]: - files: list[dict[str, Any]] = [] - current_file: str | None = None - current_lines: dict[int, int] = {} - - def flush() -> None: - nonlocal current_file, current_lines - if current_file is None: - return - if allowed_file(current_file, config): - total = len(current_lines) - covered = sum(1 for count in current_lines.values() if count > 0) - if total > 0: - files.append({"path": rel_path(current_file), "covered_lines": covered, "total_lines": total}) - current_file = None - current_lines = {} - - with path.open("r", encoding="utf-8", errors="replace") as handle: - for raw_line in handle: - line = raw_line.rstrip("\n") - if line.startswith("SF:"): - flush() - current_file = line[3:] - elif line.startswith("DA:") and current_file is not None: - line_no, count, *_ = line[3:].split(",") - current_lines[int(line_no)] = int(count) - elif line == "end_of_record": - flush() - flush() - covered = sum(file["covered_lines"] for file in files) - total = sum(file["total_lines"] for file in files) - return covered, total, files - - -def normalize_javascript_report_path(product: str, raw_path: str) -> str: - path = Path(raw_path) - if path.is_absolute(): - return raw_path - source_prefix = product_source_prefix(product) - if raw_path.startswith(f"{source_prefix}/"): - return raw_path - return f"{source_prefix}/{raw_path}" - - -def parse_javascript_summary( - path: Path, - product: str, - config: dict[str, Any], -) -> tuple[int, int, list[dict[str, Any]]]: - data = json.loads(path.read_text()) - files: list[dict[str, Any]] = [] - for raw_path, entry in data.items(): - source_path = normalize_javascript_report_path(product, raw_path) - if raw_path == "total" or not allowed_file(source_path, config): - continue - lines = entry.get("lines") or {} - total = int(lines.get("total") or 0) - covered = int(lines.get("covered") or 0) - if total > 0: - files.append({"path": rel_path(source_path), "covered_lines": covered, "total_lines": total}) - covered = sum(file["covered_lines"] for file in files) - total = sum(file["total_lines"] for file in files) - return covered, total, files - - -def resolve_kover_source_path(package_name: str, sourcefile_name: str) -> str: - package_path = package_name.replace(".", "/") - source_root = product_source_root("oliphaunt-kotlin") / "oliphaunt" / "src" - candidates = sorted(source_root.glob(f"**/{package_path}/{sourcefile_name}")) - source_candidates = [candidate for candidate in candidates if "Test" not in candidate.parts] - if source_candidates: - return rel_path(source_candidates[0]) - if candidates: - return rel_path(candidates[0]) - return f"src/sdks/kotlin/oliphaunt/src/{package_path}/{sourcefile_name}" - - -def parse_kover_xml(path: Path, config: dict[str, Any]) -> tuple[int, int, list[dict[str, Any]]]: - root = ET.parse(path).getroot() - files: list[dict[str, Any]] = [] - for package in root.findall(".//package"): - package_name = package.attrib.get("name", "") - for sourcefile in package.findall("sourcefile"): - name = sourcefile.attrib.get("name", "") - source_path = resolve_kover_source_path(package_name, name) - if not allowed_file(source_path, config): - continue - lines = sourcefile.findall("line") - total = len(lines) - covered = 0 - for line in lines: - covered_instructions = int(line.attrib.get("ci", "0")) - if covered_instructions > 0: - covered += 1 - if total > 0: - files.append( - { - "path": source_path, - "covered_lines": covered, - "total_lines": total, - } - ) - covered = sum(file["covered_lines"] for file in files) - total = sum(file["total_lines"] for file in files) - return covered, total, files - - -def parse_swift_json(path: Path, config: dict[str, Any]) -> tuple[int, int, list[dict[str, Any]]]: - data = json.loads(path.read_text()) - files: list[dict[str, Any]] = [] - for report in data.get("data", []): - for file_entry in report.get("files", []): - filename = file_entry.get("filename") or file_entry.get("name") - if not filename or not allowed_file(filename, config): - continue - summary = file_entry.get("summary") or {} - lines = summary.get("lines") or {} - total = int(lines.get("count") or lines.get("total") or 0) - covered = int(lines.get("covered") or 0) - if total > 0: - files.append({"path": rel_path(filename), "covered_lines": covered, "total_lines": total}) - covered = sum(file["covered_lines"] for file in files) - total = sum(file["total_lines"] for file in files) - return covered, total, files - - -def write_summary( - product: str, - tool: str, - covered_lines: int, - total_lines: int, - files: list[dict[str, Any]], - reports: list[Path], -) -> Path: - out = output_dir(product) - config = product_config(product) - files = sorted(files, key=lambda item: item["path"]) - summary = { - "schema": "oliphaunt-coverage-summary-v1", - "product": product, - "tool": tool, - "line_coverage": percent(covered_lines, total_lines), - "line_threshold": float(config["line_threshold"]), - "covered_lines": covered_lines, - "total_lines": total_lines, - "files": files, - "reports": [rel_path(path) for path in reports], - "source_globs": source_globs(config), - "exclude_globs": exclude_globs(config), - "waived_files": [ - { - "path": waiver["path"] or waiver["glob"], - "reason": waiver["reason"], - "evidence": waiver["evidence"], - "owner": waiver["owner"], - "expires": waiver["expires"], - } - for waiver in waiver_entries(config) - ], - } - path = out / "summary.json" - path.write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n") - return path - - -def check_summary(product: str) -> dict[str, Any]: - config = product_config(product) - summary_path = ROOT / config["summary"] - if not summary_path.is_file(): - fail(f"{product}: missing measured coverage summary {summary_path.relative_to(ROOT)}") - summary = json.loads(summary_path.read_text()) - if summary.get("product") != product: - fail(f"{product}: coverage summary product mismatch") - total = int(summary.get("total_lines") or 0) - covered = int(summary.get("covered_lines") or 0) - if total <= 0 or covered <= 0: - fail(f"{product}: coverage summary is unmeasured: covered={covered} total={total}") - files = summary.get("files", []) - if not isinstance(files, list) or not files: - fail(f"{product}: coverage summary contains no measured source files") - measured = float(summary.get("line_coverage") or 0.0) - threshold = float(config["line_threshold"]) - committed_measured = float(config.get("measured_line_coverage", 0.0)) - if committed_measured < threshold: - fail(f"{product}: committed measured_line_coverage is below line_threshold") - if measured + 0.005 < threshold: - fail(f"{product}: line coverage {measured:.2f}% is below threshold {threshold:.2f}%") - summary_reports = set(summary.get("reports", [])) - for report in config.get("reports", []): - if report not in summary_reports: - fail(f"{product}: coverage summary is missing expected report {report}") - for report in summary_reports: - report_path = ROOT / report - if not report_path.is_file() or report_path.stat().st_size == 0: - fail(f"{product}: missing or empty coverage report {report}") - for file in files: - source_path = file.get("path", "") - path = f"/{source_path}" - if any(part in path for part in FORBIDDEN_PATH_PARTS): - fail(f"{product}: coverage includes generated/vendor/build path {source_path}") - if not allowed_file(source_path, config): - fail(f"{product}: coverage includes a source path outside the baseline scope: {source_path}") - per_file_threshold = float(config.get("per_file_line_threshold", 0.0)) - if per_file_threshold > 0.0: - for file in files: - source_path = file.get("path", "") - file_total = int(file.get("total_lines") or 0) - file_covered = int(file.get("covered_lines") or 0) - file_percent = percent(file_covered, file_total) - if file_percent + 0.005 < per_file_threshold: - fail( - f"{product}: {source_path} line coverage {file_percent:.2f}% " - f"is below per-file threshold {per_file_threshold:.2f}%" - ) - measured_paths = {file.get("path", "") for file in files} - missing_owned = sorted(set(owned_unwaived_source_files(config)) - measured_paths) - if missing_owned: - fail( - f"{product}: owned source files are neither measured nor waived: " - + ", ".join(missing_owned[:20]) - + (" ..." if len(missing_owned) > 20 else "") - ) - return summary - - -def run_rust(product: str) -> None: - package = "oliphaunt" if product == "oliphaunt-rust" else "oliphaunt-wasix" - out = reset_output(product) - lcov = out / "lcov.info" - require_tool("cargo", "rustup toolchain install 1.93") - if subprocess.run(["cargo", "llvm-cov", "--version"], cwd=ROOT, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode != 0: - fail("missing required coverage tool: cargo-llvm-cov\n\nInstall with:\n cargo install cargo-llvm-cov") - if subprocess.run(["cargo", "nextest", "--version"], cwd=ROOT, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode != 0: - fail("missing required coverage tool: cargo-nextest\n\nInstall with:\n cargo install cargo-nextest --locked") - env = os.environ.copy() - if "LLVM_COV" not in env: - llvm_cov = shutil.which("llvm-cov") or optional_capture(["xcrun", "--find", "llvm-cov"]) - if llvm_cov: - env["LLVM_COV"] = llvm_cov - if "LLVM_PROFDATA" not in env: - llvm_profdata = shutil.which("llvm-profdata") or optional_capture(["xcrun", "--find", "llvm-profdata"]) - if llvm_profdata: - env["LLVM_PROFDATA"] = llvm_profdata - feature_args = ["--no-default-features"] if product == "oliphaunt-wasix-rust" else [] - target_args = ["--lib"] if product == "oliphaunt-wasix-rust" else [] - run(["cargo", "llvm-cov", "clean", "--profraw-only"], env=env) - run( - [ - "cargo", - "llvm-cov", - "nextest", - "--package", - package, - *target_args, - *feature_args, - "--locked", - "--profile", - "ci", - "--no-tests=fail", - "--test-threads=1", - "--no-report", - ], - env=env, - ) - run( - [ - "cargo", - "test", - "--doc", - "--package", - package, - "--locked", - ], - env=env, - ) - run(["cargo", "llvm-cov", "report", "--lcov", "--output-path", str(lcov)], env=env) - covered, total, files = parse_lcov(lcov, product_config(product)) - write_summary(product, "cargo-llvm-cov", covered, total, files, [lcov]) - check_summary(product) - - -def run_swift() -> None: - out = reset_output("oliphaunt-swift") - scratch = ROOT / "target" / "coverage-build" / "oliphaunt-swift" - shutil.rmtree(scratch, ignore_errors=True) - require_tool("swift", "Install Xcode or the Swift toolchain") - run( - [ - "swift", - "test", - "--package-path", - str(ROOT), - "--scratch-path", - str(scratch), - "--enable-code-coverage", - ] - ) - output = capture( - [ - "swift", - "test", - "--package-path", - str(ROOT), - "--scratch-path", - str(scratch), - "--show-codecov-path", - ] - ) - candidates = [ - Path(line.strip()) - for line in output.splitlines() - if line.strip().endswith(".json") and Path(line.strip()).is_file() - ] - if not candidates: - candidates = list(scratch.rglob("*.json")) - if not candidates: - fail("oliphaunt-swift: swift test did not emit a code coverage JSON path") - report = out / "swift-coverage.json" - shutil.copyfile(candidates[-1], report) - covered, total, files = parse_swift_json(report, product_config("oliphaunt-swift")) - write_summary("oliphaunt-swift", "swift test --enable-code-coverage", covered, total, files, [report]) - check_summary("oliphaunt-swift") - - -def run_kotlin() -> None: - out = reset_output("oliphaunt-kotlin") - require_tool("java", "Install JDK 17") - package_dir = product_source_root("oliphaunt-kotlin") - gradle = package_dir / "gradlew" - build_root = ROOT / "target" / "coverage-build" / "oliphaunt-kotlin" / "gradle" - cxx_build_root = ROOT / "target" / "coverage-build" / "oliphaunt-kotlin" / "cxx" - project_cache = ROOT / "target" / "coverage-build" / "oliphaunt-kotlin" / "gradle-cache" - shutil.rmtree(build_root, ignore_errors=True) - shutil.rmtree(cxx_build_root, ignore_errors=True) - run( - [ - str(gradle), - "-p", - str(package_dir.relative_to(ROOT)), - ":oliphaunt:koverXmlReport", - ":oliphaunt:koverVerify", - "--no-daemon", - f"-PoliphauntBuildRoot={build_root}", - f"-PoliphauntCxxBuildRoot={cxx_build_root}", - "--project-cache-dir", - str(project_cache), - ] - ) - reports = sorted(build_root.rglob("reports/kover/**/*.xml")) - if not reports: - reports = sorted(package_dir.rglob("build/reports/kover/**/*.xml")) - if not reports: - fail("oliphaunt-kotlin: Kover did not emit an XML report") - report = out / "kover.xml" - shutil.copyfile(reports[-1], report) - covered, total, files = parse_kover_xml(report, product_config("oliphaunt-kotlin")) - write_summary("oliphaunt-kotlin", "kover", covered, total, files, [report]) - check_summary("oliphaunt-kotlin") - - -def run_javascript(product: str) -> None: - out = reset_output(product) - package_dir = product_source_root(product) - require_tool("pnpm", "corepack enable && corepack prepare pnpm@11.5.0 --activate") - config = product_config(product) - threshold = str(int(float(config["line_threshold"]))) - include_patterns: list[str] = [] - for pattern in source_globs(config): - prefix = f"{product_source_prefix(product)}/" - include_patterns.append(pattern.removeprefix(prefix)) - exclude_patterns: list[str] = [] - for pattern in [*exclude_globs(config), *waiver_patterns(config)]: - prefix = f"{product_source_prefix(product)}/" - exclude_patterns.append(pattern.removeprefix(prefix)) - env = os.environ.copy() - env.update( - { - "OLIPHAUNT_VITEST_COVERAGE": "1", - "OLIPHAUNT_VITEST_COVERAGE_DIR": str(out), - "OLIPHAUNT_VITEST_COVERAGE_INCLUDE": json.dumps(include_patterns), - "OLIPHAUNT_VITEST_COVERAGE_EXCLUDE": json.dumps(exclude_patterns), - "OLIPHAUNT_VITEST_COVERAGE_LINES": threshold, - } - ) - run(["pnpm", "--dir", str(package_dir), "test"], env=env) - summary_report = out / "coverage-summary.json" - if not summary_report.is_file(): - fail(f"{product}: Vitest did not emit {summary_report.relative_to(ROOT)}") - covered, total, files = parse_javascript_summary(summary_report, product, config) - reports = [summary_report] - lcov = out / "lcov.info" - if lcov.is_file(): - reports.append(lcov) - write_summary(product, "vitest-v8", covered, total, files, reports) - check_summary(product) - - -def run_product(product: str) -> None: - if product not in PRODUCTS: - fail(f"unknown product {product!r}; expected one of {', '.join(PRODUCTS)}") - if product in ("oliphaunt-rust", "oliphaunt-wasix-rust"): - run_rust(product) - elif product == "oliphaunt-swift": - run_swift() - elif product == "oliphaunt-kotlin": - run_kotlin() - elif product in ("oliphaunt-js", "oliphaunt-react-native"): - run_javascript(product) - else: - fail(f"unhandled coverage product {product}") - - -def parse_products_json(value: str | None) -> list[str]: - if value is None or not value.strip(): - return list(PRODUCTS) - try: - parsed = json.loads(value) - except json.JSONDecodeError as error: - fail(f"coverage products JSON is invalid: {error}") - if not isinstance(parsed, list) or not all(isinstance(item, str) for item in parsed): - fail("coverage products JSON must be a string array") - unknown = sorted(set(parsed) - set(PRODUCTS)) - if unknown: - fail("unknown coverage product(s): " + ", ".join(unknown)) - return sorted(set(parsed), key=PRODUCTS.index) - - -def summarize(*, allow_missing: bool = False, products_json: str | None = None) -> None: - data = load_baseline() - products = data["products"] - selected_products = parse_products_json(products_json) - rows = [] - all_summaries = [] - for product in selected_products: - if product not in products: - if data.get("policy", {}).get("fail_on_unmeasured_product", True): - fail(f"missing coverage baseline for {product}") - continue - summary_path = ROOT / products[product]["summary"] - if allow_missing and not summary_path.is_file(): - continue - if not summary_path.is_file(): - fail(f"missing required coverage summary: {summary_path.relative_to(ROOT)}") - summary = check_summary(product) - all_summaries.append(summary) - rows.append( - "| {product} | {tool} | {line_coverage:.2f}% | {line_threshold:.2f}% | {covered_lines}/{total_lines} |".format( - **summary - ) - ) - COVERAGE_ROOT.mkdir(parents=True, exist_ok=True) - aggregate = { - "schema": "oliphaunt-coverage-aggregate-v1", - "products": all_summaries, - } - (COVERAGE_ROOT / "summary.json").write_text(json.dumps(aggregate, indent=2, sort_keys=True) + "\n") - markdown = "\n".join( - [ - "| Product | Tool | Lines | Threshold | Covered |", - "| --- | --- | ---: | ---: | ---: |", - *rows, - "", - ] - ) - (COVERAGE_ROOT / "summary.md").write_text(markdown) - print(markdown) - - -def main(argv: list[str]) -> None: - parser = argparse.ArgumentParser(description="Oliphaunt coverage runner") - subparsers = parser.add_subparsers(dest="command", required=True) - run_parser = subparsers.add_parser("run-product") - run_parser.add_argument("product", choices=PRODUCTS) - check_parser = subparsers.add_parser("check-product") - check_parser.add_argument("product", choices=PRODUCTS) - summarize_parser = subparsers.add_parser("summarize") - summarize_parser.add_argument( - "--allow-missing", - action="store_true", - help="summarize only measured product reports that are present", - ) - summarize_parser.add_argument( - "--products-json", - help="JSON string array of product reports that must be present", - ) - args = parser.parse_args(argv) - if args.command == "run-product": - run_product(args.product) - elif args.command == "check-product": - summary = check_summary(args.product) - print(f"{args.product}: {summary['line_coverage']:.2f}% line coverage") - elif args.command == "summarize": - summarize(allow_missing=args.allow_missing, products_json=args.products_json) - - -if __name__ == "__main__": - main(sys.argv[1:]) diff --git a/tools/coverage/moon.yml b/tools/coverage/moon.yml index cc64491e..ff8a5996 100644 --- a/tools/coverage/moon.yml +++ b/tools/coverage/moon.yml @@ -1,7 +1,7 @@ $schema: "https://moonrepo.dev/schemas/project.json" id: "coverage-tools" -language: "python" +language: "javascript" layer: "tool" stack: "infrastructure" tags: ["tools", "coverage", "repo-hygiene"] @@ -19,9 +19,10 @@ owners: tasks: check: tags: ["quality", "static"] - command: "python3 -m py_compile tools/coverage/coverage.py" + command: "bash tools/dev/bun.sh tools/coverage/coverage.mjs check-tools" inputs: - "/tools/coverage/**/*" + - "/coverage/baseline.toml" options: cache: true runFromWorkspaceRoot: true diff --git a/tools/coverage/run-product b/tools/coverage/run-product index fbb05058..008a0cfd 100755 --- a/tools/coverage/run-product +++ b/tools/coverage/run-product @@ -1,3 +1,4 @@ #!/usr/bin/env sh set -eu -exec "$(dirname "$0")/coverage.py" run-product "$@" +root="$(git rev-parse --show-toplevel 2>/dev/null)" +exec "$root/tools/dev/bun.sh" "$root/tools/coverage/coverage.mjs" run-product "$@" diff --git a/tools/coverage/summarize b/tools/coverage/summarize index ce71196a..c2c2f05f 100755 --- a/tools/coverage/summarize +++ b/tools/coverage/summarize @@ -1,3 +1,4 @@ #!/usr/bin/env sh set -eu -exec "$(dirname "$0")/coverage.py" summarize "$@" +root="$(git rev-parse --show-toplevel 2>/dev/null)" +exec "$root/tools/dev/bun.sh" "$root/tools/coverage/coverage.mjs" summarize "$@" diff --git a/tools/policy/check-test-strategy.mjs b/tools/policy/check-test-strategy.mjs index 8d3b40e6..fc1c1c6c 100755 --- a/tools/policy/check-test-strategy.mjs +++ b/tools/policy/check-test-strategy.mjs @@ -530,9 +530,9 @@ if (jsRunner.includes("'tsx'")) { requireText('tools/test/run-js-tests.mjs', '--coverage.provider=v8'); requireText('tools/test/run-js-tests.mjs', 'OLIPHAUNT_VITEST_COVERAGE_INCLUDE'); requireText('tools/test/run-js-tests.mjs', 'OLIPHAUNT_VITEST_COVERAGE_EXCLUDE'); -requireText('tools/coverage/coverage.py', '"OLIPHAUNT_VITEST_COVERAGE": "1"'); -requireText('tools/coverage/coverage.py', 'write_summary(product, "vitest-v8"'); -rejectText('tools/coverage/coverage.py', '"c8"'); +requireText('tools/coverage/coverage.mjs', "OLIPHAUNT_VITEST_COVERAGE: '1'"); +requireText('tools/coverage/coverage.mjs', "writeSummary(product, 'vitest-v8'"); +rejectText('tools/coverage/coverage.mjs', "'c8'"); for (const productDir of ['src/sdks/js', 'src/sdks/react-native']) { const testsDir = path.join(productDir, 'src', '__tests__'); diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index da66fcc6..17893854 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -1,7 +1,6 @@ # Intentional Python tooling inventory. # New Python files should be ported to Bun or deliberately added here. src/extensions/tools/check-extension-model.py -tools/coverage/coverage.py tools/graph/ci_plan.py tools/graph/graph.py tools/policy/check-release-policy.py From df0335478a9a1d18df0d37bbd6a936be79989844 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 19:57:39 +0000 Subject: [PATCH 117/137] test: cover js registry asset resolution --- coverage/baseline.toml | 2 +- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 16 +- .../js/src/__tests__/asset-resolver.test.ts | 291 +++++++++++++++++- src/sdks/js/src/__tests__/jsr.test.ts | 18 ++ 4 files changed, 309 insertions(+), 18 deletions(-) create mode 100644 src/sdks/js/src/__tests__/jsr.test.ts diff --git a/coverage/baseline.toml b/coverage/baseline.toml index e6cd8bb4..9aa2c87a 100644 --- a/coverage/baseline.toml +++ b/coverage/baseline.toml @@ -151,7 +151,7 @@ expires = "before-0.2.0" [products.oliphaunt-js] tool = "vitest-v8" line_threshold = 80.0 -measured_line_coverage = 82.43 +measured_line_coverage = 81.65 summary = "target/coverage/oliphaunt-js/summary.json" reports = [ "target/coverage/oliphaunt-js/coverage-summary.json", diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 27938ad7..5567e653 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -63,10 +63,9 @@ until the current-state gates here are checked with fresh local evidence. - [ ] Inventory remaining Python and Rust helper scripts; move nonessential scripts to Bun where that improves local developer experience without making critical product code less idiomatic. -- [ ] Fix or refresh the measured `oliphaunt-js` coverage lane; a fresh - `tools/coverage/run-product oliphaunt-js` attempt stops in Vitest at 70.82% - line coverage against the 80% global threshold before coverage summary - parsing runs. +- [x] Fix or refresh the measured `oliphaunt-js` coverage lane; the current + focused asset resolver and JSR entrypoint tests keep the lane above the 80% + global threshold and produce the structured coverage summary. - [ ] Re-run Linux CI-like and release/local-registry lanes after each tooling migration batch. @@ -75,6 +74,15 @@ until the current-state gates here are checked with fresh local evidence. - 2026-06-26: `git status --short --branch` was clean on `f0rr0/reduce-oliphaunt-icu-crate-size` at commit `895ed8d` before the fresh example e2e run. +- 2026-06-26: The `oliphaunt-js` coverage lane was refreshed after adding + focused Node asset resolver coverage for split native tools, ICU package + metadata, extension payload materialization, and the JSR entrypoint. + `tools/coverage/run-product oliphaunt-js` passed with 17 tests and the + structured summary now reports 81.65% line coverage against the 80% gate. + Follow-up checks passed: `tools/coverage/check-product oliphaunt-js`, + `tools/coverage/summarize --allow-missing --products-json '["oliphaunt-js"]'`, + `bash tools/policy/check-coverage.sh oliphaunt-js`, and + `tools/dev/bun.sh tools/coverage/coverage.mjs check-tools`. - 2026-06-26: Current-state example e2e re-run passed against the staged local registries from commit `895ed8d`: `examples/tools/run-electron-driver-smoke.sh examples/electron`, `examples/tools/run-electron-driver-smoke.sh diff --git a/src/sdks/js/src/__tests__/asset-resolver.test.ts b/src/sdks/js/src/__tests__/asset-resolver.test.ts index 43a618d2..44704bce 100644 --- a/src/sdks/js/src/__tests__/asset-resolver.test.ts +++ b/src/sdks/js/src/__tests__/asset-resolver.test.ts @@ -1,13 +1,20 @@ import assert from 'node:assert/strict'; -import { test } from 'vitest'; -import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; +import { chmod, mkdir, mkdtemp, readFile, rm, rmdir, writeFile } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import { arch, platform, tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; import { deflateRawSync, inflateRawSync } from 'node:zlib'; - -import { liboliphauntPackageTarget } from '../native/common.js'; +import { test } from 'vitest'; import { resolvePackageRelativeUrl } from '../native/assets-deno.js'; -import { resolveNodeNativeInstall, resolvePackageRelativePath } from '../native/assets-node.js'; +import { + materializeNodeExtensionInstall, + type ResolvedNativeInstall, + resolveNodeIcuDataDirectory, + resolveNodeNativeInstall, + resolvePackageRelativePath, +} from '../native/assets-node.js'; +import { liboliphauntPackageTarget } from '../native/common.js'; import { extractTarArchive } from '../native/tar.js'; import { extractZipArchive } from '../native/zip.js'; import { brokerModeSupport } from '../runtime/broker.js'; @@ -32,6 +39,10 @@ async function main(): Promise { await zipExtractionWritesFilesAndRejectsTraversal(); packageMetadataPathsAreConfinedToPackageRoot(); await nodeResolverUsesInstalledPackages(); + await nodeResolverMergesPackageManagedRuntimeAndSplitTools(); + await nodeIcuResolverAcceptsValidPortablePackage(); + await nodeExtensionMaterializationValidatesSelections(); + await nodeExtensionMaterializationCopiesPackagePayloads(); await typeScriptPackageMetadataMatchesRuntimePackages(); await brokerSupportUsesInstalledPackages(); } @@ -171,13 +182,195 @@ async function nodeResolverUsesInstalledPackages(): Promise { delete process.env.LIBOLIPHAUNT_PATH; delete process.env.OLIPHAUNT_RUNTIME_DIR; try { - await assert.rejects( - () => resolveNodeNativeInstall(), - /@oliphaunt\/liboliphaunt-/, + await assert.rejects(() => resolveNodeNativeInstall(), /@oliphaunt\/liboliphaunt-/); + } finally { + restoreEnv('LIBOLIPHAUNT_PATH', previousLibraryPath); + restoreEnv('OLIPHAUNT_RUNTIME_DIR', previousRuntimeDir); + } +} + +async function nodeResolverMergesPackageManagedRuntimeAndSplitTools(): Promise { + const previousLibraryPath = process.env.LIBOLIPHAUNT_PATH; + const previousRuntimeDir = process.env.OLIPHAUNT_RUNTIME_DIR; + delete process.env.LIBOLIPHAUNT_PATH; + delete process.env.OLIPHAUNT_RUNTIME_DIR; + + const target = liboliphauntPackageTarget(platform(), arch()); + const runtimePackageRoot = packageRoot(target.packageName); + const toolsPackageRoot = packageRoot(target.toolsPackageName); + const createdFiles: string[] = []; + try { + await writeFixtureFile( + join(runtimePackageRoot, target.libraryRelativePath), + 'liboliphaunt-test', + createdFiles, ); + const runtimeBin = join(runtimePackageRoot, target.runtimeRelativePath, 'bin'); + for (const tool of nativeRuntimeToolsForTarget(target.id)) { + await writeFixtureFile(join(runtimeBin, tool), `runtime:${tool}`, createdFiles); + } + const toolsBin = join(toolsPackageRoot, target.toolsRuntimeRelativePath, 'bin'); + for (const tool of nativeClientToolsForTarget(target.id)) { + await writeFixtureFile(join(toolsBin, tool), `tools:${tool}`, createdFiles); + } + + const install = await resolveNodeNativeInstall(); + assert.equal(install.libraryPath, join(runtimePackageRoot, target.libraryRelativePath)); + const runtimeDirectory = install.runtimeDirectory; + if (runtimeDirectory === undefined) { + assert.fail('node resolver should materialize a package-managed runtime cache'); + } + assert.ok(runtimeDirectory.includes('oliphaunt-js-runtime-cache')); + assert.equal(install.icuDataDirectory, undefined); + for (const tool of [ + ...nativeRuntimeToolsForTarget(target.id), + ...nativeClientToolsForTarget(target.id), + ]) { + const bytes = await readFile(join(runtimeDirectory, 'bin', tool)); + assert.ok(bytes.byteLength > 0, `${tool} should be materialized into the runtime cache`); + } + await rm(dirname(runtimeDirectory), { recursive: true, force: true }); } finally { restoreEnv('LIBOLIPHAUNT_PATH', previousLibraryPath); restoreEnv('OLIPHAUNT_RUNTIME_DIR', previousRuntimeDir); + await removeFixtureFiles(createdFiles, [runtimePackageRoot, toolsPackageRoot]); + } +} + +async function nodeIcuResolverAcceptsValidPortablePackage(): Promise { + const root = await mkdtemp(join(tmpdir(), 'oliphaunt-js-icu-')); + try { + await writeFile( + join(root, 'package.json'), + JSON.stringify({ + name: root, + version: '9.9.9', + oliphaunt: { + product: 'oliphaunt-icu', + kind: 'icu-data', + target: 'portable', + dataRelativePath: 'share/icu', + }, + }), + 'utf8', + ); + await mkdir(join(root, 'share/icu'), { recursive: true }); + await writeFile(join(root, 'share/icu/icudt76l.dat'), 'icu'); + assert.equal(await resolveNodeIcuDataDirectory('9.9.9', root), join(root, 'share/icu')); + await assert.rejects( + () => resolveNodeIcuDataDirectory('9.9.8', root), + /does not match @oliphaunt\/ts icuVersion/, + ); + } finally { + await rm(root, { recursive: true, force: true }); + } +} + +async function nodeExtensionMaterializationValidatesSelections(): Promise { + const install: ResolvedNativeInstall = { libraryPath: '/tmp/liboliphaunt-test.so' }; + assert.equal(await materializeNodeExtensionInstall(install, []), install); + await assert.rejects( + () => materializeNodeExtensionInstall(install, ['not_a_real_extension']), + /unknown Oliphaunt extension id/, + ); + await assert.rejects( + () => materializeNodeExtensionInstall(install, ['hstore']), + /native extension packages require a package-managed runtime directory/, + ); +} + +async function nodeExtensionMaterializationCopiesPackagePayloads(): Promise { + const target = liboliphauntPackageTarget(platform(), arch()); + const basePackageName = '@oliphaunt/extension-hstore'; + const targetPackageName = `${basePackageName}-${target.id}`; + const payloadPackageName = `${basePackageName}-payload-${target.id}`; + const product = 'oliphaunt-extension-hstore'; + const createdPackageRoots: string[] = []; + const root = await mkdtemp(join(tmpdir(), 'oliphaunt-js-extension-install-')); + const libraryPath = join(root, 'lib/liboliphaunt.so'); + const installRuntime = join(root, 'runtime'); + let firstInstall: ResolvedNativeInstall | undefined; + try { + await writeFixturePackage(basePackageName, createdPackageRoots, { + name: basePackageName, + version: '0.1.0', + oliphaunt: { + product, + kind: 'exact-extension', + sqlName: 'hstore', + targetPackageNames: { [target.id]: targetPackageName }, + }, + }); + await writeFixturePackage(targetPackageName, createdPackageRoots, { + name: targetPackageName, + version: '0.1.0', + oliphaunt: { + product, + kind: 'exact-extension-target', + sqlName: 'hstore', + target: target.id, + liboliphauntVersion: '0.1.0', + payloadPackageNames: [payloadPackageName], + }, + }); + const payloadRoot = await writeFixturePackage(payloadPackageName, createdPackageRoots, { + name: payloadPackageName, + version: '0.1.0', + oliphaunt: { + product, + kind: 'exact-extension-payload', + sqlName: 'hstore', + target: target.id, + liboliphauntVersion: '0.1.0', + runtimeRelativePath: 'runtime', + moduleRelativePath: 'modules', + }, + }); + await mkdir(join(payloadRoot, 'runtime/share/extension'), { recursive: true }); + await mkdir(join(payloadRoot, 'modules'), { recursive: true }); + await writeFile(join(payloadRoot, 'runtime/share/extension/hstore.control'), 'extension'); + await writeFile(join(payloadRoot, 'modules/hstore.so'), 'module'); + await mkdir(installRuntime, { recursive: true }); + await mkdir(join(dirname(libraryPath), 'modules'), { recursive: true }); + await writeFile(join(installRuntime, 'base-runtime.txt'), 'base'); + await writeFile(join(dirname(libraryPath), 'modules/base-module.so'), 'base-module'); + + firstInstall = await materializeNodeExtensionInstall( + { libraryPath, runtimeDirectory: installRuntime }, + ['hstore'], + ); + const runtimeDirectory = firstInstall.runtimeDirectory; + const moduleDirectory = firstInstall.moduleDirectory; + if (runtimeDirectory === undefined || moduleDirectory === undefined) { + assert.fail('extension materialization should return runtime and module cache directories'); + } + assert.ok(runtimeDirectory.includes('oliphaunt-js-runtime-cache')); + assert.ok(moduleDirectory.includes('oliphaunt-js-runtime-cache')); + assert.equal(await readFile(join(runtimeDirectory, 'base-runtime.txt'), 'utf8'), 'base'); + assert.equal( + await readFile(join(runtimeDirectory, 'share/extension/hstore.control'), 'utf8'), + 'extension', + ); + assert.equal(await readFile(join(moduleDirectory, 'base-module.so'), 'utf8'), 'base-module'); + assert.equal(await readFile(join(moduleDirectory, 'hstore.so'), 'utf8'), 'module'); + + const cached = await materializeNodeExtensionInstall( + { libraryPath, runtimeDirectory: installRuntime }, + ['hstore'], + ); + assert.equal(cached.runtimeDirectory, firstInstall.runtimeDirectory); + assert.equal(cached.moduleDirectory, firstInstall.moduleDirectory); + } finally { + if (firstInstall?.runtimeDirectory !== undefined) { + await rm(dirname(firstInstall.runtimeDirectory), { recursive: true, force: true }); + } + await rm(root, { recursive: true, force: true }); + for (const packageRoot of createdPackageRoots.reverse()) { + await rm(packageRoot, { recursive: true, force: true }); + } + await removeEmptyParents(nativeResolverPackageScopeRoot(), [ + dirname(nativeResolverPackageScopeRoot()), + ]); } } @@ -270,10 +463,7 @@ async function brokerSupportUsesInstalledPackages(): Promise { try { const support = await brokerModeSupport({}); assert.equal(support.available, false); - assert.match( - support.unavailableReason ?? '', - /@oliphaunt\/broker-|@oliphaunt\/liboliphaunt-/, - ); + assert.match(support.unavailableReason ?? '', /@oliphaunt\/broker-|@oliphaunt\/liboliphaunt-/); } finally { restoreEnv('LIBOLIPHAUNT_PATH', previousLibraryPath); restoreEnv('OLIPHAUNT_RUNTIME_DIR', previousRuntimeDir); @@ -444,6 +634,81 @@ function restoreEnv(name: string, value: string | undefined): void { } } +const require = createRequire(import.meta.url); + +function packageRoot(packageName: string): string { + return dirname(require.resolve(`${packageName}/package.json`)); +} + +function nativeResolverPackageScopeRoot(): string { + return fileURLToPath(new URL('../native/node_modules/@oliphaunt/', import.meta.url)); +} + +function nativeResolverPackageRoot(packageName: string): string { + const prefix = '@oliphaunt/'; + if (!packageName.startsWith(prefix)) { + throw new Error(`test fixture package must use ${prefix}: ${packageName}`); + } + return join(nativeResolverPackageScopeRoot(), packageName.slice(prefix.length)); +} + +async function writeFixturePackage( + packageName: string, + createdPackageRoots: string[], + packageJson: Record, +): Promise { + const root = nativeResolverPackageRoot(packageName); + await rm(root, { recursive: true, force: true }); + await mkdir(root, { recursive: true }); + await writeFile(join(root, 'package.json'), JSON.stringify(packageJson, null, 2), 'utf8'); + createdPackageRoots.push(root); + return root; +} + +async function writeFixtureFile( + path: string, + contents: string, + createdFiles: string[], +): Promise { + try { + await readFile(path); + return; + } catch {} + await mkdir(dirname(path), { recursive: true }); + await writeFile(path, contents, 'utf8'); + createdFiles.push(path); +} + +async function removeFixtureFiles(files: string[], stopRoots: string[]): Promise { + for (const file of files.reverse()) { + await rm(file, { force: true }); + await removeEmptyParents(dirname(file), stopRoots); + } +} + +async function removeEmptyParents(directory: string, stopRoots: string[]): Promise { + const stops = new Set(stopRoots.map((root) => resolve(root))); + let current = resolve(directory); + while (!stops.has(current)) { + try { + await rmdir(current); + } catch { + return; + } + current = dirname(current); + } +} + +function nativeRuntimeToolsForTarget(target: string): string[] { + return target === 'windows-x64-msvc' + ? ['initdb.exe', 'pg_ctl.exe', 'postgres.exe'] + : ['initdb', 'pg_ctl', 'postgres']; +} + +function nativeClientToolsForTarget(target: string): string[] { + return target === 'windows-x64-msvc' ? ['pg_dump.exe', 'psql.exe'] : ['pg_dump', 'psql']; +} + async function readTypeScriptPackageJson(): Promise { return JSON.parse( await readFile(new URL('../../package.json', import.meta.url), 'utf8'), diff --git a/src/sdks/js/src/__tests__/jsr.test.ts b/src/sdks/js/src/__tests__/jsr.test.ts new file mode 100644 index 00000000..e33b9c82 --- /dev/null +++ b/src/sdks/js/src/__tests__/jsr.test.ts @@ -0,0 +1,18 @@ +import assert from 'node:assert/strict'; +import { test } from 'vitest'; + +import Oliphaunt, { Oliphaunt as namedOliphaunt, simpleQuery } from '../jsr.js'; + +test('jsr entry point exposes protocol helpers and rejects native runtime use', async () => { + assert.equal(Oliphaunt, namedOliphaunt); + assert.equal(simpleQuery('SELECT 1')[0], 0x51); + assert.deepEqual(await Oliphaunt.supportedModes(), []); + await assert.rejects( + () => Oliphaunt.open(), + /Native Oliphaunt runtimes are not available from jsr:@oliphaunt\/ts/, + ); + await assert.rejects( + () => Oliphaunt.restore(), + /Native Oliphaunt runtimes are not available from jsr:@oliphaunt\/ts/, + ); +}); From 0b4a83e2a769a096f4300dbf128c4c7a2d43e305 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 20:17:51 +0000 Subject: [PATCH 118/137] test: validate js extension payloads --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 30 ++++ src/extensions/tools/check-extension-model.py | 4 + .../js/src/__tests__/asset-resolver.test.ts | 117 ++++++++++++++- src/sdks/js/src/generated/extensions.ts | 80 ++++++++++ src/sdks/js/src/native/assets-node.ts | 139 +++++++++++++++++- .../react-native/src/generated/extensions.ts | 80 ++++++++++ tools/policy/check-sdk-parity.sh | 8 + 7 files changed, 443 insertions(+), 15 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 5567e653..93a4fbb7 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -54,6 +54,12 @@ until the current-state gates here are checked with fresh local evidence. shared preload metadata. - [ ] Add or adjust machine checks for any invariant currently enforced only by convention or docs. +- [ ] Harden TypeScript Node/Bun runtime cache publication so package-managed + runtime/tool/extension materialization publishes through a temp/marker or + equivalent atomic protocol instead of rebuilding cache roots in place. +- [ ] Add Swift and Kotlin negative tests for unsupported mobile + `runtimeFeatures`, and update maintainer docs so the shared runtime-resource + manifest field list includes `runtimeFeatures`. ### P2: Cleanup and Tooling Migration @@ -83,6 +89,30 @@ until the current-state gates here are checked with fresh local evidence. `tools/coverage/summarize --allow-missing --products-json '["oliphaunt-js"]'`, `bash tools/policy/check-coverage.sh oliphaunt-js`, and `tools/dev/bun.sh tools/coverage/coverage.mjs check-tools`. +- 2026-06-26: Tightened TypeScript Node/Bun exact-extension package + materialization to validate release-shaped extension payloads before copying + them into the runtime cache. Generated JS/React Native extension metadata now + exposes noncanonical SQL file prefixes/names, and the Node resolver requires + selected extension control files, SQL install files, declared data files, and + native module files across split payload packages. Fresh checks passed: + `python3 src/extensions/tools/check-extension-model.py --write`, + `python3 src/extensions/tools/check-extension-model.py --check`, + `pnpm --dir src/sdks/js test`, `pnpm --dir src/sdks/js typecheck`, + `bash src/sdks/js/tools/check-sdk.sh check-static`, + `pnpm --dir src/sdks/react-native test`, + `pnpm --dir src/sdks/react-native typecheck`, + `bash tools/policy/check-sdk-parity.sh`, + `bash tools/policy/check-sdk-mobile-extension-surface.sh`, + `python3 tools/release/check_consumer_shape.py`, + `python3 tools/release/check_release_metadata.py`, + `python3 tools/release/check_artifact_targets.py`, + `bash tools/policy/check-tooling-stack.sh`, + `tools/dev/bun.sh tools/policy/check-test-strategy.mjs`, + `tools/coverage/run-product oliphaunt-js`, + `tools/coverage/check-product oliphaunt-js`, + `tools/coverage/summarize --allow-missing --products-json '["oliphaunt-js"]'`, + `bash tools/policy/check-coverage.sh oliphaunt-js`, and `git diff --check`. + The coverage summary reported 81.61% line coverage against the 80% gate. - 2026-06-26: Current-state example e2e re-run passed against the staged local registries from commit `895ed8d`: `examples/tools/run-electron-driver-smoke.sh examples/electron`, `examples/tools/run-electron-driver-smoke.sh diff --git a/src/extensions/tools/check-extension-model.py b/src/extensions/tools/check-extension-model.py index dacfa218..cf991ebc 100755 --- a/src/extensions/tools/check-extension-model.py +++ b/src/extensions/tools/check-extension-model.py @@ -970,6 +970,8 @@ def camel(row: dict) -> dict: "sharedPreloadLibraries": row["shared-preload-libraries"], "dataFiles": row["data-files"], "runtimeShareDataFiles": row["runtime-share-data-files"], + "extensionSqlFilePrefixes": row["extension-sql-file-prefixes"], + "extensionSqlFileNames": row["extension-sql-file-names"], "public": row["public"], "stable": row["stable"], "desktopReleaseReady": row["desktop-release-ready"], @@ -997,6 +999,8 @@ def camel(row: dict) -> dict: " readonly sharedPreloadLibraries: readonly string[];\n" " readonly dataFiles: readonly string[];\n" " readonly runtimeShareDataFiles: readonly string[];\n" + " readonly extensionSqlFilePrefixes: readonly string[];\n" + " readonly extensionSqlFileNames: readonly string[];\n" " readonly public: boolean;\n" " readonly stable: boolean;\n" " readonly desktopReleaseReady: boolean;\n" diff --git a/src/sdks/js/src/__tests__/asset-resolver.test.ts b/src/sdks/js/src/__tests__/asset-resolver.test.ts index 44704bce..69093c91 100644 --- a/src/sdks/js/src/__tests__/asset-resolver.test.ts +++ b/src/sdks/js/src/__tests__/asset-resolver.test.ts @@ -43,6 +43,7 @@ async function main(): Promise { await nodeIcuResolverAcceptsValidPortablePackage(); await nodeExtensionMaterializationValidatesSelections(); await nodeExtensionMaterializationCopiesPackagePayloads(); + await nodeExtensionMaterializationRejectsIncompletePackagePayloads(); await typeScriptPackageMetadataMatchesRuntimePackages(); await brokerSupportUsesInstalledPackages(); } @@ -323,13 +324,21 @@ async function nodeExtensionMaterializationCopiesPackagePayloads(): Promise { + const target = liboliphauntPackageTarget(platform(), arch()); + const basePackageName = '@oliphaunt/extension-hstore'; + const targetPackageName = `${basePackageName}-${target.id}`; + const payloadPackageName = `${basePackageName}-payload-${target.id}`; + const product = 'oliphaunt-extension-hstore'; + const createdPackageRoots: string[] = []; + const root = await mkdtemp(join(tmpdir(), 'oliphaunt-js-extension-invalid-')); + const libraryPath = join(root, 'lib/liboliphaunt.so'); + const installRuntime = join(root, 'runtime'); + try { + await writeFixturePackage(basePackageName, createdPackageRoots, { + name: basePackageName, + version: '0.1.0', + oliphaunt: { + product, + kind: 'exact-extension', + sqlName: 'hstore', + targetPackageNames: { [target.id]: targetPackageName }, + }, + }); + await writeFixturePackage(targetPackageName, createdPackageRoots, { + name: targetPackageName, + version: '0.1.0', + oliphaunt: { + product, + kind: 'exact-extension-target', + sqlName: 'hstore', + target: target.id, + liboliphauntVersion: '0.1.0', + payloadPackageNames: [payloadPackageName], + }, + }); + const payloadRoot = await writeFixturePackage(payloadPackageName, createdPackageRoots, { + name: payloadPackageName, + version: '0.1.0', + oliphaunt: { + product, + kind: 'exact-extension-payload', + sqlName: 'hstore', + target: target.id, + liboliphauntVersion: '0.1.0', + runtimeRelativePath: 'runtime', + moduleRelativePath: 'runtime/lib/postgresql', + }, + }); + await mkdir(join(payloadRoot, 'runtime/share/postgresql/extension'), { recursive: true }); + await mkdir(join(payloadRoot, 'runtime/lib/postgresql'), { recursive: true }); + await writeFile( + join(payloadRoot, 'runtime/share/postgresql/extension/hstore.control'), + 'extension', + ); + await writeFile( + join( + payloadRoot, + 'runtime/lib/postgresql', + `hstore${nativeModuleSuffixForTarget(target.id)}`, + ), + 'module', + ); + await mkdir(installRuntime, { recursive: true }); + + await assert.rejects( + () => + materializeNodeExtensionInstall({ libraryPath, runtimeDirectory: installRuntime }, [ + 'hstore', + ]), + /missing SQL install files for hstore/, + ); + } finally { + await rm(root, { recursive: true, force: true }); + for (const packageRoot of createdPackageRoots.reverse()) { + await rm(packageRoot, { recursive: true, force: true }); + } + await removeEmptyParents(nativeResolverPackageScopeRoot(), [ + dirname(nativeResolverPackageScopeRoot()), + ]); + } +} + async function typeScriptPackageMetadataMatchesRuntimePackages(): Promise { const packageJson = await readTypeScriptPackageJson(); const liboliphauntVersion = packageMetadataVersion(packageJson, 'liboliphauntVersion'); @@ -709,6 +802,16 @@ function nativeClientToolsForTarget(target: string): string[] { return target === 'windows-x64-msvc' ? ['pg_dump.exe', 'psql.exe'] : ['pg_dump', 'psql']; } +function nativeModuleSuffixForTarget(target: string): string { + if (target.startsWith('macos-')) { + return '.dylib'; + } + if (target === 'windows-x64-msvc') { + return '.dll'; + } + return '.so'; +} + async function readTypeScriptPackageJson(): Promise { return JSON.parse( await readFile(new URL('../../package.json', import.meta.url), 'utf8'), diff --git a/src/sdks/js/src/generated/extensions.ts b/src/sdks/js/src/generated/extensions.ts index 4dc78a3e..28992e07 100644 --- a/src/sdks/js/src/generated/extensions.ts +++ b/src/sdks/js/src/generated/extensions.ts @@ -14,6 +14,8 @@ export type GeneratedExtensionMetadata = { readonly sharedPreloadLibraries: readonly string[]; readonly dataFiles: readonly string[]; readonly runtimeShareDataFiles: readonly string[]; + readonly extensionSqlFilePrefixes: readonly string[]; + readonly extensionSqlFileNames: readonly string[]; readonly public: boolean; readonly stable: boolean; readonly desktopReleaseReady: boolean; @@ -36,6 +38,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'amcheck', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'amcheck', mobileReleaseReady: true, nativeDependencies: [], @@ -62,6 +66,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'auto_explain', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'auto_explain', mobileReleaseReady: true, nativeDependencies: [], @@ -88,6 +94,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'bloom', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'bloom', mobileReleaseReady: true, nativeDependencies: [], @@ -114,6 +122,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'btree_gin', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'btree_gin', mobileReleaseReady: true, nativeDependencies: [], @@ -140,6 +150,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'btree_gist', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'btree_gist', mobileReleaseReady: true, nativeDependencies: [], @@ -166,6 +178,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'citext', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'citext', mobileReleaseReady: true, nativeDependencies: [], @@ -192,6 +206,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'cube', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'cube', mobileReleaseReady: true, nativeDependencies: [], @@ -218,6 +234,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'dict_int', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'dict_int', mobileReleaseReady: true, nativeDependencies: [], @@ -244,6 +262,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'dict_xsyn', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'dict_xsyn', mobileReleaseReady: true, nativeDependencies: [], @@ -270,6 +290,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: ['cube'], desktopReleaseReady: true, displayName: 'earthdistance', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'earthdistance', mobileReleaseReady: true, nativeDependencies: [], @@ -296,6 +318,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'file_fdw', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'file_fdw', mobileReleaseReady: true, nativeDependencies: [], @@ -322,6 +346,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'fuzzystrmatch', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'fuzzystrmatch', mobileReleaseReady: true, nativeDependencies: [], @@ -348,6 +374,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'hstore', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'hstore', mobileReleaseReady: true, nativeDependencies: [], @@ -374,6 +402,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'intarray', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'intarray', mobileReleaseReady: true, nativeDependencies: [], @@ -400,6 +430,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'isn', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'isn', mobileReleaseReady: true, nativeDependencies: [], @@ -426,6 +458,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'lo', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'lo', mobileReleaseReady: true, nativeDependencies: [], @@ -452,6 +486,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'ltree', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'ltree', mobileReleaseReady: true, nativeDependencies: [], @@ -478,6 +514,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'pageinspect', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'pageinspect', mobileReleaseReady: true, nativeDependencies: [], @@ -504,6 +542,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'pg_buffercache', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'pg_buffercache', mobileReleaseReady: true, nativeDependencies: [], @@ -530,6 +570,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'pg_freespacemap', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'pg_freespacemap', mobileReleaseReady: true, nativeDependencies: [], @@ -556,6 +598,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'pg_hashids', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'pg_hashids', mobileReleaseReady: true, nativeDependencies: [], @@ -582,6 +626,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'pg_ivm', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'pg_ivm', mobileReleaseReady: true, nativeDependencies: [], @@ -608,6 +654,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'pg_surgery', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'pg_surgery', mobileReleaseReady: true, nativeDependencies: [], @@ -634,6 +682,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'pg_textsearch', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'pg_textsearch', mobileReleaseReady: true, nativeDependencies: [], @@ -674,6 +724,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'pg_trgm', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'pg_trgm', mobileReleaseReady: true, nativeDependencies: [], @@ -700,6 +752,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'pg_uuidv7', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'pg_uuidv7', mobileReleaseReady: true, nativeDependencies: [], @@ -726,6 +780,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'pg_visibility', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'pg_visibility', mobileReleaseReady: true, nativeDependencies: [], @@ -752,6 +808,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'pg_walinspect', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'pg_walinspect', mobileReleaseReady: true, nativeDependencies: [], @@ -778,6 +836,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'pgcrypto', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'pgcrypto', mobileReleaseReady: true, nativeDependencies: ['openssl:3.5.6-libcrypto-wasix-static'], @@ -804,6 +864,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: ['plpgsql'], desktopReleaseReady: true, displayName: 'pgtap', + extensionSqlFileNames: ['uninstall_pgtap.sql'], + extensionSqlFilePrefixes: ['pgtap-core', 'pgtap-schema'], id: 'pgtap', mobileReleaseReady: true, nativeDependencies: [], @@ -854,6 +916,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'PostGIS', + extensionSqlFileNames: ['uninstall_postgis.sql'], + extensionSqlFilePrefixes: ['postgis_comments', 'postgis_proc_set_search_path', 'rtpostgis'], id: 'postgis', mobileReleaseReady: true, nativeDependencies: [ @@ -911,6 +975,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'seg', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'seg', mobileReleaseReady: true, nativeDependencies: [], @@ -937,6 +1003,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'tablefunc', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'tablefunc', mobileReleaseReady: true, nativeDependencies: [], @@ -963,6 +1031,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'tcn', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'tcn', mobileReleaseReady: true, nativeDependencies: [], @@ -989,6 +1059,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'tsm_system_rows', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'tsm_system_rows', mobileReleaseReady: true, nativeDependencies: [], @@ -1015,6 +1087,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'tsm_system_time', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'tsm_system_time', mobileReleaseReady: true, nativeDependencies: [], @@ -1041,6 +1115,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'unaccent', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'unaccent', mobileReleaseReady: true, nativeDependencies: [], @@ -1067,6 +1143,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'uuid-ossp', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'uuid_ossp', mobileReleaseReady: true, nativeDependencies: [], @@ -1093,6 +1171,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'pgvector', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'vector', mobileReleaseReady: true, nativeDependencies: [], diff --git a/src/sdks/js/src/native/assets-node.ts b/src/sdks/js/src/native/assets-node.ts index 7726bca2..1611c3d3 100644 --- a/src/sdks/js/src/native/assets-node.ts +++ b/src/sdks/js/src/native/assets-node.ts @@ -3,14 +3,16 @@ import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promi import { createRequire } from 'node:module'; import { arch, platform, tmpdir } from 'node:os'; import { dirname, isAbsolute, join, relative, resolve } from 'node:path'; - +import { + type GeneratedExtensionMetadata, + generatedExtensionBySqlName, +} from '../generated/extensions.js'; import { liboliphauntPackageTarget, type NativePackageTarget, resolveExplicitLibraryPath, resolveExplicitRuntimeDirectory, } from './common.js'; -import { generatedExtensionBySqlName } from '../generated/extensions.js'; export type ResolvedNativeInstall = { libraryPath: string; @@ -82,7 +84,10 @@ export async function resolveNodeNativeInstall( libraryPath?: string, ): Promise { const versions = await packageVersions(); - const icuDataDirectory = await resolveNodeIcuDataDirectory(versions.icuVersion, versions.icuPackage); + const icuDataDirectory = await resolveNodeIcuDataDirectory( + versions.icuVersion, + versions.icuPackage, + ); const explicit = resolveExplicitLibraryPath(libraryPath); if (explicit !== undefined) { return { @@ -113,7 +118,9 @@ export async function materializeNodeExtensionInstall( const versions = await packageVersions(); const target = liboliphauntPackageTarget(platform(), arch()); const packages = await Promise.all( - selected.map((sqlName) => resolveExtensionPackage(sqlName, target.id, versions.liboliphauntVersion)), + selected.map((sqlName) => + resolveExtensionPackage(sqlName, target.id, versions.liboliphauntVersion), + ), ); const cacheKey = runtimeCacheKey({ libraryPath: install.libraryPath, @@ -176,7 +183,9 @@ export async function resolveNodeIcuDataDirectory( packageName?: string, ): Promise { const versions = - expectedVersion === undefined || packageName === undefined ? await packageVersions() : undefined; + expectedVersion === undefined || packageName === undefined + ? await packageVersions() + : undefined; const expected = expectedVersion ?? versions?.icuVersion; const name = packageName ?? versions?.icuPackage ?? '@oliphaunt/icu'; const packageJsonPath = optionalResolvePackageJson(name); @@ -275,7 +284,9 @@ async function resolveExtensionPackage( throw new Error(`${targetPackageName} package metadata does not declare ${expectedProduct}`); } if (packageJson.oliphaunt?.sqlName !== sqlName) { - throw new Error(`${targetPackageName} package metadata does not declare SQL extension ${sqlName}`); + throw new Error( + `${targetPackageName} package metadata does not declare SQL extension ${sqlName}`, + ); } if (packageJson.oliphaunt?.target !== target) { throw new Error(`${targetPackageName} package metadata does not target ${target}`); @@ -290,6 +301,10 @@ async function resolveExtensionPackage( } const runtimeDirectories: string[] = []; const moduleDirectories: string[] = []; + const extension = generatedExtensionBySqlName(sqlName); + if (extension === undefined) { + throw new Error(`unknown Oliphaunt extension id '${sqlName}'`); + } const payloadPackageNames = packageJson.oliphaunt.payloadPackageNames ?? []; if (payloadPackageNames.length > 0) { for (const payloadPackageName of payloadPackageNames) { @@ -328,6 +343,13 @@ async function resolveExtensionPackage( moduleDirectories.push(moduleDirectory); } } + await requireExtensionPackagePayload({ + extension, + target, + source: targetPackageName, + runtimeDirectories, + moduleDirectories, + }); return { name: targetPackageName, version: packageJson.version, @@ -399,6 +421,93 @@ async function resolveExtensionPayloadPackage( return { runtimeDirectory, moduleDirectory }; } +async function requireExtensionPackagePayload(config: { + extension: GeneratedExtensionMetadata; + target: string; + source: string; + runtimeDirectories: readonly string[]; + moduleDirectories: readonly string[]; +}): Promise { + if (config.extension.createsExtension) { + const entries = await extensionSqlDirectoryEntries(config.runtimeDirectories); + const hasControl = entries.includes(`${config.extension.sqlName}.control`); + if (!hasControl) { + throw new Error( + `${config.source} extension runtime payload is missing ${config.extension.sqlName}.control`, + ); + } + const hasInstallSql = entries.some( + (entry) => entry.endsWith('.sql') && extensionSqlFileBelongs(config.extension, entry), + ); + if (!hasInstallSql) { + throw new Error( + `${config.source} extension runtime payload is missing SQL install files for ${config.extension.sqlName}`, + ); + } + } + + for (const dataFile of config.extension.dataFiles) { + await requireFileInAnyRoot( + config.runtimeDirectories, + dataFile, + `${config.source} extension runtime payload`, + ); + } + + if (config.extension.nativeModuleStem !== null) { + const moduleFile = `${config.extension.nativeModuleStem}${nativeModuleSuffixForTarget(config.target)}`; + await requireFileInAnyRoot( + config.moduleDirectories, + moduleFile, + `${config.source} extension module payload`, + ); + } +} + +async function extensionSqlDirectoryEntries( + runtimeDirectories: readonly string[], +): Promise { + const entries: string[] = []; + for (const runtimeDirectory of runtimeDirectories) { + const extensionDirectory = join(runtimeDirectory, 'share/postgresql/extension'); + if (!(await isDirectory(extensionDirectory))) { + continue; + } + for (const entry of await readdir(extensionDirectory, { withFileTypes: true })) { + if (entry.isFile()) { + entries.push(entry.name); + } + } + } + return entries; +} + +function extensionSqlFileBelongs(extension: GeneratedExtensionMetadata, fileName: string): boolean { + return ( + fileName === `${extension.sqlName}.control` || + fileName === `${extension.sqlName}.sql` || + (fileName.startsWith(`${extension.sqlName}--`) && fileName.endsWith('.sql')) || + extension.extensionSqlFileNames.includes(fileName) || + extension.extensionSqlFilePrefixes.some((prefix) => fileName.startsWith(prefix)) + ); +} + +async function requireFileInAnyRoot( + roots: readonly string[], + relativePath: string, + source: string, +): Promise { + for (const root of roots) { + const path = join(root, relativePath); + try { + if ((await stat(path)).isFile()) { + return; + } + } catch {} + } + throw new Error(`${source} is missing required file ${relativePath}`); +} + async function resolvePackageNativeInstall( target: NativePackageTarget, expectedVersion: string, @@ -471,7 +580,9 @@ async function resolveNativeToolsPackage( ); } const packageRoot = dirname(packageJsonPath); - const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')) as NativeToolsPackageMetadata; + const packageJson = JSON.parse( + await readFile(packageJsonPath, 'utf8'), + ) as NativeToolsPackageMetadata; if (packageJson.name !== target.toolsPackageName) { throw new Error( `${target.toolsPackageName} package metadata has name ${packageJson.name ?? ''}`, @@ -574,7 +685,9 @@ async function resolveExtensionTargetPackageJson( throw new Error(`${packageName} package metadata has name ${packageJson.name ?? ''}`); } if (packageJson.oliphaunt?.kind !== 'exact-extension') { - throw new Error(`${packageName} package metadata does not declare an exact Oliphaunt extension`); + throw new Error( + `${packageName} package metadata does not declare an exact Oliphaunt extension`, + ); } if (packageJson.oliphaunt?.product !== expectedProduct) { throw new Error(`${packageName} package metadata does not declare ${expectedProduct}`); @@ -742,6 +855,16 @@ function nativeClientToolsForTarget(target: string): string[] { return target === 'windows-x64-msvc' ? ['pg_dump.exe', 'psql.exe'] : ['pg_dump', 'psql']; } +function nativeModuleSuffixForTarget(target: string): string { + if (target.startsWith('macos-')) { + return '.dylib'; + } + if (target === 'windows-x64-msvc') { + return '.dll'; + } + return '.so'; +} + function runtimeCacheKey(value: unknown): string { return createHash('sha256').update(JSON.stringify(value)).digest('hex').slice(0, 32); } diff --git a/src/sdks/react-native/src/generated/extensions.ts b/src/sdks/react-native/src/generated/extensions.ts index 4dc78a3e..28992e07 100644 --- a/src/sdks/react-native/src/generated/extensions.ts +++ b/src/sdks/react-native/src/generated/extensions.ts @@ -14,6 +14,8 @@ export type GeneratedExtensionMetadata = { readonly sharedPreloadLibraries: readonly string[]; readonly dataFiles: readonly string[]; readonly runtimeShareDataFiles: readonly string[]; + readonly extensionSqlFilePrefixes: readonly string[]; + readonly extensionSqlFileNames: readonly string[]; readonly public: boolean; readonly stable: boolean; readonly desktopReleaseReady: boolean; @@ -36,6 +38,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'amcheck', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'amcheck', mobileReleaseReady: true, nativeDependencies: [], @@ -62,6 +66,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'auto_explain', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'auto_explain', mobileReleaseReady: true, nativeDependencies: [], @@ -88,6 +94,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'bloom', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'bloom', mobileReleaseReady: true, nativeDependencies: [], @@ -114,6 +122,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'btree_gin', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'btree_gin', mobileReleaseReady: true, nativeDependencies: [], @@ -140,6 +150,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'btree_gist', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'btree_gist', mobileReleaseReady: true, nativeDependencies: [], @@ -166,6 +178,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'citext', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'citext', mobileReleaseReady: true, nativeDependencies: [], @@ -192,6 +206,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'cube', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'cube', mobileReleaseReady: true, nativeDependencies: [], @@ -218,6 +234,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'dict_int', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'dict_int', mobileReleaseReady: true, nativeDependencies: [], @@ -244,6 +262,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'dict_xsyn', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'dict_xsyn', mobileReleaseReady: true, nativeDependencies: [], @@ -270,6 +290,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: ['cube'], desktopReleaseReady: true, displayName: 'earthdistance', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'earthdistance', mobileReleaseReady: true, nativeDependencies: [], @@ -296,6 +318,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'file_fdw', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'file_fdw', mobileReleaseReady: true, nativeDependencies: [], @@ -322,6 +346,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'fuzzystrmatch', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'fuzzystrmatch', mobileReleaseReady: true, nativeDependencies: [], @@ -348,6 +374,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'hstore', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'hstore', mobileReleaseReady: true, nativeDependencies: [], @@ -374,6 +402,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'intarray', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'intarray', mobileReleaseReady: true, nativeDependencies: [], @@ -400,6 +430,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'isn', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'isn', mobileReleaseReady: true, nativeDependencies: [], @@ -426,6 +458,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'lo', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'lo', mobileReleaseReady: true, nativeDependencies: [], @@ -452,6 +486,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'ltree', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'ltree', mobileReleaseReady: true, nativeDependencies: [], @@ -478,6 +514,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'pageinspect', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'pageinspect', mobileReleaseReady: true, nativeDependencies: [], @@ -504,6 +542,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'pg_buffercache', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'pg_buffercache', mobileReleaseReady: true, nativeDependencies: [], @@ -530,6 +570,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'pg_freespacemap', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'pg_freespacemap', mobileReleaseReady: true, nativeDependencies: [], @@ -556,6 +598,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'pg_hashids', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'pg_hashids', mobileReleaseReady: true, nativeDependencies: [], @@ -582,6 +626,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'pg_ivm', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'pg_ivm', mobileReleaseReady: true, nativeDependencies: [], @@ -608,6 +654,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'pg_surgery', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'pg_surgery', mobileReleaseReady: true, nativeDependencies: [], @@ -634,6 +682,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'pg_textsearch', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'pg_textsearch', mobileReleaseReady: true, nativeDependencies: [], @@ -674,6 +724,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'pg_trgm', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'pg_trgm', mobileReleaseReady: true, nativeDependencies: [], @@ -700,6 +752,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'pg_uuidv7', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'pg_uuidv7', mobileReleaseReady: true, nativeDependencies: [], @@ -726,6 +780,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'pg_visibility', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'pg_visibility', mobileReleaseReady: true, nativeDependencies: [], @@ -752,6 +808,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'pg_walinspect', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'pg_walinspect', mobileReleaseReady: true, nativeDependencies: [], @@ -778,6 +836,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'pgcrypto', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'pgcrypto', mobileReleaseReady: true, nativeDependencies: ['openssl:3.5.6-libcrypto-wasix-static'], @@ -804,6 +864,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: ['plpgsql'], desktopReleaseReady: true, displayName: 'pgtap', + extensionSqlFileNames: ['uninstall_pgtap.sql'], + extensionSqlFilePrefixes: ['pgtap-core', 'pgtap-schema'], id: 'pgtap', mobileReleaseReady: true, nativeDependencies: [], @@ -854,6 +916,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'PostGIS', + extensionSqlFileNames: ['uninstall_postgis.sql'], + extensionSqlFilePrefixes: ['postgis_comments', 'postgis_proc_set_search_path', 'rtpostgis'], id: 'postgis', mobileReleaseReady: true, nativeDependencies: [ @@ -911,6 +975,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'seg', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'seg', mobileReleaseReady: true, nativeDependencies: [], @@ -937,6 +1003,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'tablefunc', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'tablefunc', mobileReleaseReady: true, nativeDependencies: [], @@ -963,6 +1031,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'tcn', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'tcn', mobileReleaseReady: true, nativeDependencies: [], @@ -989,6 +1059,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'tsm_system_rows', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'tsm_system_rows', mobileReleaseReady: true, nativeDependencies: [], @@ -1015,6 +1087,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'tsm_system_time', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'tsm_system_time', mobileReleaseReady: true, nativeDependencies: [], @@ -1041,6 +1115,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'unaccent', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'unaccent', mobileReleaseReady: true, nativeDependencies: [], @@ -1067,6 +1143,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'uuid-ossp', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'uuid_ossp', mobileReleaseReady: true, nativeDependencies: [], @@ -1093,6 +1171,8 @@ export const GENERATED_EXTENSION_METADATA = [ dependencies: [], desktopReleaseReady: true, displayName: 'pgvector', + extensionSqlFileNames: [], + extensionSqlFilePrefixes: [], id: 'vector', mobileReleaseReady: true, nativeDependencies: [], diff --git a/tools/policy/check-sdk-parity.sh b/tools/policy/check-sdk-parity.sh index 5bebd1de..162a3752 100755 --- a/tools/policy/check-sdk-parity.sh +++ b/tools/policy/check-sdk-parity.sh @@ -260,6 +260,14 @@ require_text src/sdks/js/src/runtime/server.ts "requireServerClientTools" \ "TypeScript nativeServer startup must preflight split client tools for explicit and package-managed installs" require_text src/sdks/js/src/runtime/server.ts "requireTool(toolDirectory, 'psql')" \ "TypeScript nativeServer startup must validate psql alongside pg_dump" +require_text src/sdks/js/src/generated/extensions.ts "extensionSqlFilePrefixes" \ + "TypeScript generated extension metadata must expose noncanonical extension SQL file prefixes for package validation" +require_text src/sdks/js/src/native/assets-node.ts "requireExtensionPackagePayload" \ + "TypeScript Node/Bun exact-extension resolver must validate complete extension payload files before materialization" +require_text src/sdks/js/src/native/assets-node.ts "missing SQL install files" \ + "TypeScript Node/Bun exact-extension resolver must reject payloads missing selected extension install SQL" +require_text src/sdks/js/src/__tests__/asset-resolver.test.ts "nodeExtensionMaterializationRejectsIncompletePackagePayloads" \ + "TypeScript asset resolver tests must cover incomplete exact-extension payload rejection" require_text docs/maintainers/sdk-products-policy.md "These are product SDKs, not auxiliary bindings." \ "SDK maintainer policy must frame Rust/Swift/Kotlin/RN as product SDKs" require_text docs/maintainers/sdk-products-policy.md '`tools/policy/sdk-manifest.toml` is the repo-level SDK registry kept for' \ From de33332d278d80a1d6ed39688098e3d6b665ce4e Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 20:34:39 +0000 Subject: [PATCH 119/137] fix: publish js runtime caches atomically --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 14 +- .../js/src/__tests__/asset-resolver.test.ts | 23 +- .../js/src/__tests__/native-bindings.test.ts | 238 +++++++++++++++++- src/sdks/js/src/native/assets-deno.ts | 197 +++++++++++++-- src/sdks/js/src/native/assets-node.ts | 156 +++++++++--- src/sdks/js/tools/check-sdk.sh | 14 ++ tools/policy/check-sdk-parity.sh | 12 + 7 files changed, 592 insertions(+), 62 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 93a4fbb7..9bf7d7e8 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -54,9 +54,10 @@ until the current-state gates here are checked with fresh local evidence. shared preload metadata. - [ ] Add or adjust machine checks for any invariant currently enforced only by convention or docs. -- [ ] Harden TypeScript Node/Bun runtime cache publication so package-managed - runtime/tool/extension materialization publishes through a temp/marker or - equivalent atomic protocol instead of rebuilding cache roots in place. +- [x] Harden TypeScript Node/Bun/Deno runtime cache publication so + package-managed runtime/tool/extension materialization publishes through a + temp/marker or equivalent atomic protocol instead of rebuilding cache roots + in place. - [ ] Add Swift and Kotlin negative tests for unsupported mobile `runtimeFeatures`, and update maintainer docs so the shared runtime-resource manifest field list includes `runtimeFeatures`. @@ -386,6 +387,13 @@ until the current-state gates here are checked with fresh local evidence. and static-registry readiness through the manifest path, and return shared preload libraries from the proved runtime resources. React Native inherits those checks through its Kotlin/Swift SDK delegation. +- 2026-06-26: TypeScript package-managed runtime cache publication now stages + Node/Bun extension runtime merges, Node/Bun split tool merges, and Deno split + tool merges under unique `.build-*` roots, writes the manifest as the commit + marker, and renames the completed tree into place under a per-cache lock. + JS resolver tests cover leftover cleanup and Deno failed-publish preservation; + JS static checks and SDK parity checks require the staged publication helpers + to stay wired. ## Priority 0: Current Acceptance Gates diff --git a/src/sdks/js/src/__tests__/asset-resolver.test.ts b/src/sdks/js/src/__tests__/asset-resolver.test.ts index 69093c91..ab2f4048 100644 --- a/src/sdks/js/src/__tests__/asset-resolver.test.ts +++ b/src/sdks/js/src/__tests__/asset-resolver.test.ts @@ -1,8 +1,8 @@ import assert from 'node:assert/strict'; -import { chmod, mkdir, mkdtemp, readFile, rm, rmdir, writeFile } from 'node:fs/promises'; +import { chmod, mkdir, mkdtemp, readdir, readFile, rm, rmdir, writeFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; import { arch, platform, tmpdir } from 'node:os'; -import { dirname, join, resolve } from 'node:path'; +import { basename, dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { deflateRawSync, inflateRawSync } from 'node:zlib'; import { test } from 'vitest'; @@ -230,6 +230,7 @@ async function nodeResolverMergesPackageManagedRuntimeAndSplitTools(): Promise 0, `${tool} should be materialized into the runtime cache`); } + await assertNoRuntimeCacheTemporarySiblings(dirname(runtimeDirectory)); await rm(dirname(runtimeDirectory), { recursive: true, force: true }); } finally { restoreEnv('LIBOLIPHAUNT_PATH', previousLibraryPath); @@ -373,6 +374,7 @@ async function nodeExtensionMaterializationCopiesPackagePayloads(): Promise { + const parent = dirname(cacheRoot); + const name = basename(cacheRoot); + const entries = await readdir(parent); + assert.deepEqual( + entries + .filter( + (entry) => + entry.startsWith(`${name}.build-`) || + entry.startsWith(`${name}.old-`) || + entry === `${name}.lock`, + ) + .sort(), + [], + ); +} + function nativeRuntimeToolsForTarget(target: string): string[] { return target === 'windows-x64-msvc' ? ['initdb.exe', 'pg_ctl.exe', 'postgres.exe'] diff --git a/src/sdks/js/src/__tests__/native-bindings.test.ts b/src/sdks/js/src/__tests__/native-bindings.test.ts index 24f0f210..022993b1 100644 --- a/src/sdks/js/src/__tests__/native-bindings.test.ts +++ b/src/sdks/js/src/__tests__/native-bindings.test.ts @@ -1,11 +1,25 @@ import assert from 'node:assert/strict'; -import { test } from 'vitest'; -import { mkdtemp, rm, writeFile } from 'node:fs/promises'; -import { join } from 'node:path'; +import { + copyFile as fsCopyFile, + mkdir as fsMkdir, + rename as fsRename, + stat as fsStat, + mkdtemp, + readdir, + readFile, + rm, + rmdir, + writeFile, +} from 'node:fs/promises'; +import { createRequire } from 'node:module'; import { tmpdir } from 'node:os'; +import { basename, dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { test } from 'vitest'; -import Oliphaunt, { createNodeNativeBinding, simpleQuery, type OliphauntClient } from '../index.js'; +import Oliphaunt, { createNodeNativeBinding, type OliphauntClient, simpleQuery } from '../index.js'; import { resolveDenoNativeInstall } from '../native/assets-deno.js'; +import { liboliphauntPackageTarget } from '../native/common.js'; import { createDenoNativeBinding } from '../native/deno.js'; import { cString, @@ -25,6 +39,7 @@ async function main(): Promise { testFfiLayoutPackingAndBounds(); await testNodeNativeBindingUsesExplicitAssetsAndAddon(); await testDenoAssetResolverHonorsExplicitPaths(); + await testDenoPackageManagedResolverPublishesRuntimeCacheAtomically(); await testDenoNativeBindingRejectsPackageManagedExtensions(); } @@ -171,12 +186,13 @@ module.exports = { }); assert.equal(handle, 41n); assert.deepEqual([...(await binding.execProtocolRaw(handle, new Uint8Array([7, 8])))], [7, 8]); - assert.deepEqual( - [...(await binding.execSimpleQuery!(handle, 'SELECT 1'))], - [90, 0, 0, 0, 5, 73], - ); + const execSimpleQuery = binding.execSimpleQuery; + assert.ok(execSimpleQuery !== undefined); + assert.deepEqual([...(await execSimpleQuery(handle, 'SELECT 1'))], [90, 0, 0, 0, 5, 73]); const chunks: number[][] = []; - binding.execProtocolStream!(handle, new Uint8Array([9]), (chunk) => chunks.push([...chunk])); + const execProtocolStream = binding.execProtocolStream; + assert.ok(execProtocolStream !== undefined); + execProtocolStream(handle, new Uint8Array([9]), (chunk) => chunks.push([...chunk])); assert.deepEqual(chunks, [[1, 2], [3]]); assert.deepEqual([...(await binding.backup(handle, 'physicalArchive'))], [4, 5, 6]); assert.throws(() => binding.backup(handle, 'sql'), /not supported by nativeDirect/); @@ -369,6 +385,210 @@ async function testDenoNativeBindingRejectsPackageManagedExtensions(): Promise { + const previousDeno = (globalThis as { Deno?: unknown }).Deno; + const previousLibraryPath = process.env.LIBOLIPHAUNT_PATH; + const previousRuntimeDir = process.env.OLIPHAUNT_RUNTIME_DIR; + const target = liboliphauntPackageTarget('linux', 'x86_64'); + const runtimePackageRoot = packageRoot(target.packageName); + const toolsPackageRoot = packageRoot(target.toolsPackageName); + const root = await mkdtemp(join(tmpdir(), 'oliphaunt-js-deno-cache-')); + const createdFiles: string[] = []; + let failCopyTo: ((path: string) => boolean) | undefined; + try { + delete process.env.LIBOLIPHAUNT_PATH; + delete process.env.OLIPHAUNT_RUNTIME_DIR; + (globalThis as { Deno?: unknown }).Deno = fsBackedDenoRuntime(root, (path) => + failCopyTo?.(path), + ); + + await writeFixtureFile( + join(runtimePackageRoot, target.libraryRelativePath), + 'liboliphaunt-test', + createdFiles, + ); + const runtimeBin = join(runtimePackageRoot, target.runtimeRelativePath, 'bin'); + for (const tool of nativeRuntimeToolsForTarget(target.id)) { + await writeFixtureFile(join(runtimeBin, tool), `runtime:${tool}`, createdFiles); + } + const toolsBin = join(toolsPackageRoot, target.toolsRuntimeRelativePath, 'bin'); + for (const tool of nativeClientToolsForTarget(target.id)) { + await writeFixtureFile(join(toolsBin, tool), `tools:${tool}`, createdFiles); + } + + const install = await resolveDenoNativeInstall(); + assert.equal(install.libraryPath, join(runtimePackageRoot, target.libraryRelativePath)); + assert.equal(install.packageManaged, true); + const runtimeDirectory = install.runtimeDirectory; + if (runtimeDirectory === undefined) { + assert.fail('Deno resolver should materialize a package-managed runtime cache'); + } + assert.ok(runtimeDirectory.startsWith(root)); + for (const tool of [ + ...nativeRuntimeToolsForTarget(target.id), + ...nativeClientToolsForTarget(target.id), + ]) { + assert.ok((await readFile(join(runtimeDirectory, 'bin', tool))).byteLength > 0); + } + const cacheRoot = dirname(runtimeDirectory); + await assertNoRuntimeCacheTemporarySiblings(cacheRoot); + + const previousMarker = 'previous-valid-manifest'; + await writeFile(join(cacheRoot, 'manifest.json'), previousMarker, 'utf8'); + await writeFile(join(runtimeDirectory, 'bin/previous-only'), 'old-runtime', 'utf8'); + failCopyTo = (path) => path.endsWith('/runtime/bin/psql'); + await assert.rejects(() => resolveDenoNativeInstall(), /injected Deno copy failure/); + assert.equal(await readFile(join(cacheRoot, 'manifest.json'), 'utf8'), previousMarker); + assert.equal( + await readFile(join(runtimeDirectory, 'bin/previous-only'), 'utf8'), + 'old-runtime', + ); + await assertNoRuntimeCacheTemporarySiblings(cacheRoot); + } finally { + if (previousDeno === undefined) { + delete (globalThis as { Deno?: unknown }).Deno; + } else { + (globalThis as { Deno?: unknown }).Deno = previousDeno; + } + restoreEnv('LIBOLIPHAUNT_PATH', previousLibraryPath); + restoreEnv('OLIPHAUNT_RUNTIME_DIR', previousRuntimeDir); + await rm(root, { recursive: true, force: true }); + await removeFixtureFiles(createdFiles, [runtimePackageRoot, toolsPackageRoot]); + } +} + +function fsBackedDenoRuntime( + tempRoot: string, + shouldFailCopy: (path: string) => boolean | undefined, +): unknown { + return { + build: { os: 'linux', arch: 'x86_64' }, + env: { + get(name: string) { + return name === 'TMPDIR' ? tempRoot : undefined; + }, + }, + async readTextFile(path: string | URL) { + return readFile(fsPath(path), 'utf8'); + }, + async writeTextFile(path: string | URL, data: string) { + await writeFile(fsPath(path), data, 'utf8'); + }, + async *readDir(path: string | URL) { + for (const entry of await readdir(fsPath(path), { withFileTypes: true })) { + yield { + name: entry.name, + isFile: entry.isFile(), + isDirectory: entry.isDirectory(), + }; + } + }, + async stat(path: string | URL) { + const metadata = await fsStat(fsPath(path)); + return { + isFile: metadata.isFile(), + isDirectory: metadata.isDirectory(), + mtime: metadata.mtime, + }; + }, + async mkdir(path: string | URL, options?: { recursive?: boolean }) { + await fsMkdir(fsPath(path), options); + }, + async remove(path: string | URL, options?: { recursive?: boolean }) { + await rm(fsPath(path), { recursive: options?.recursive === true }); + }, + async copyFile(from: string | URL, to: string | URL) { + const destination = fsPath(to); + if (shouldFailCopy(destination) === true) { + throw new Error(`injected Deno copy failure for ${destination}`); + } + await fsCopyFile(fsPath(from), destination); + }, + async rename(from: string | URL, to: string | URL) { + await fsRename(fsPath(from), fsPath(to)); + }, + }; +} + +function fsPath(path: string | URL): string { + return path instanceof URL ? fileURLToPath(path) : path; +} + +const require = createRequire(import.meta.url); + +function packageRoot(packageName: string): string { + return dirname(require.resolve(`${packageName}/package.json`)); +} + +async function writeFixtureFile( + path: string, + contents: string, + createdFiles: string[], +): Promise { + try { + await readFile(path); + return; + } catch {} + await fsMkdir(dirname(path), { recursive: true }); + await writeFile(path, contents, 'utf8'); + createdFiles.push(path); +} + +async function removeFixtureFiles(files: string[], stopRoots: string[]): Promise { + for (const file of files.reverse()) { + await rm(file, { force: true }); + await removeEmptyParents(dirname(file), stopRoots); + } +} + +async function removeEmptyParents(directory: string, stopRoots: string[]): Promise { + const stops = new Set(stopRoots.map((stopRoot) => resolve(stopRoot))); + let current = resolve(directory); + while (!stops.has(current)) { + try { + await rmdir(current); + } catch { + return; + } + current = dirname(current); + } +} + +async function assertNoRuntimeCacheTemporarySiblings(cacheRoot: string): Promise { + const parent = dirname(cacheRoot); + const name = basename(cacheRoot); + const entries = await readdir(parent); + assert.deepEqual( + entries + .filter( + (entry) => + entry.startsWith(`${name}.build-`) || + entry.startsWith(`${name}.old-`) || + entry === `${name}.lock`, + ) + .sort(), + [], + ); +} + +function nativeRuntimeToolsForTarget(target: string): string[] { + return target === 'windows-x64-msvc' + ? ['initdb.exe', 'pg_ctl.exe', 'postgres.exe'] + : ['initdb', 'pg_ctl', 'postgres']; +} + +function nativeClientToolsForTarget(target: string): string[] { + return target === 'windows-x64-msvc' ? ['pg_dump.exe', 'psql.exe'] : ['pg_dump', 'psql']; +} + +function restoreEnv(name: string, value: string | undefined): void { + if (value === undefined) { + delete process.env[name]; + } else { + process.env[name] = value; + } +} + test('native bindings', async () => { await main(); }); diff --git a/src/sdks/js/src/native/assets-deno.ts b/src/sdks/js/src/native/assets-deno.ts index 8216a0ae..bd929951 100644 --- a/src/sdks/js/src/native/assets-deno.ts +++ b/src/sdks/js/src/native/assets-deno.ts @@ -1,5 +1,6 @@ -import { createHash } from 'node:crypto'; -import { join } from 'node:path'; +import { createHash, randomUUID } from 'node:crypto'; +import { createRequire } from 'node:module'; +import { dirname, join } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { @@ -21,13 +22,23 @@ type DenoRuntime = { env?: { get(name: string): string | undefined }; readTextFile(path: string | URL): Promise; writeTextFile(path: string | URL, data: string): Promise; - readDir(path: string | URL): AsyncIterable<{ name: string; isFile?: boolean; isDirectory?: boolean }>; - stat(path: string | URL): Promise<{ isFile?: boolean; isDirectory?: boolean }>; + readDir( + path: string | URL, + ): AsyncIterable<{ name: string; isFile?: boolean; isDirectory?: boolean }>; + stat( + path: string | URL, + ): Promise<{ isFile?: boolean; isDirectory?: boolean; mtime?: Date | null }>; mkdir(path: string | URL, options?: { recursive?: boolean }): Promise; remove(path: string | URL, options?: { recursive?: boolean }): Promise; copyFile(from: string | URL, to: string | URL): Promise; + rename(from: string | URL, to: string | URL): Promise; }; +const CACHE_LOCK_POLL_MS = 25; +const CACHE_LOCK_TIMEOUT_MS = 30_000; +const CACHE_LOCK_STALE_MS = 5 * 60_000; +const require = createRequire(import.meta.url); + type PackageMetadata = { name: string; oliphaunt?: { @@ -79,11 +90,7 @@ export async function resolveDenoNativeInstall( const icuDataDirectory = deno === undefined || versions === undefined ? undefined - : await resolveDenoIcuDataDirectory( - deno, - versions.icuVersion, - versions.icuPackage, - ); + : await resolveDenoIcuDataDirectory(deno, versions.icuVersion, versions.icuPackage); return { libraryPath: explicit, runtimeDirectory: resolveExplicitRuntimeDirectory(), @@ -284,14 +291,130 @@ async function materializeDenoToolsRuntime( return fileURLToPath(runtimeUrl); } - await removeTree(deno, root); - await deno.mkdir(root, { recursive: true }); - await copyDirectory(deno, config.runtimePackage.runtimeUrl, runtimeUrl); - await copyDirectory(deno, config.toolsPackage.runtimeUrl, runtimeUrl); - await deno.writeTextFile(marker, manifest); + await publishDenoRuntimeCache(deno, root, manifest, async (stageRoot) => { + const stageRuntimeUrl = pathToFileURL(join(fileURLToPath(stageRoot), 'runtime')); + await copyDirectory(deno, config.runtimePackage.runtimeUrl, stageRuntimeUrl); + await copyDirectory(deno, config.toolsPackage.runtimeUrl, stageRuntimeUrl); + }); return fileURLToPath(runtimeUrl); } +async function publishDenoRuntimeCache( + deno: DenoRuntime, + root: URL, + manifest: string, + build: (stageRoot: URL) => Promise, +): Promise { + const rootPath = fileURLToPath(root); + const marker = pathToFileURL(join(rootPath, 'manifest.json')); + if ((await optionalReadText(deno, marker)) === manifest) { + return; + } + await deno.mkdir(pathToFileURL(dirname(rootPath)), { recursive: true }); + await withDenoRuntimeCacheLock(deno, root, async () => { + if ((await optionalReadText(deno, marker)) === manifest) { + return; + } + const unique = randomUUID(); + const stageRoot = pathToFileURL(`${rootPath}.build-${unique}`); + const oldRoot = pathToFileURL(`${rootPath}.old-${unique}`); + await removeTree(deno, stageRoot); + await removeTree(deno, oldRoot); + let movedExistingRoot = false; + try { + await deno.mkdir(stageRoot, { recursive: true }); + await build(stageRoot); + await deno.writeTextFile( + pathToFileURL(join(fileURLToPath(stageRoot), 'manifest.json')), + manifest, + ); + try { + await deno.rename(root, oldRoot); + movedExistingRoot = true; + } catch (error) { + if (!isDenoFsError(error, 'ENOENT', 'NotFound')) { + throw error; + } + } + try { + await deno.rename(stageRoot, root); + } catch (error) { + if (movedExistingRoot) { + await deno.rename(oldRoot, root).catch(() => undefined); + movedExistingRoot = false; + } + throw error; + } + if (movedExistingRoot) { + await removeTree(deno, oldRoot); + } + } catch (error) { + await removeTree(deno, stageRoot); + await removeTree(deno, oldRoot); + throw error; + } + }); +} + +async function withDenoRuntimeCacheLock( + deno: DenoRuntime, + root: URL, + callback: () => Promise, +): Promise { + const lock = pathToFileURL(`${fileURLToPath(root)}.lock`); + const deadline = Date.now() + CACHE_LOCK_TIMEOUT_MS; + while (true) { + try { + await deno.mkdir(lock); + break; + } catch (error) { + if (!isDenoFsError(error, 'EEXIST', 'AlreadyExists')) { + throw error; + } + if (await denoRuntimeCacheLockIsStale(deno, lock)) { + await removeTree(deno, lock); + continue; + } + if (Date.now() >= deadline) { + throw new Error( + `timed out waiting for Oliphaunt runtime cache lock: ${fileURLToPath(lock)}`, + ); + } + await delay(CACHE_LOCK_POLL_MS); + } + } + + try { + return await callback(); + } finally { + await removeTree(deno, lock); + } +} + +async function denoRuntimeCacheLockIsStale(deno: DenoRuntime, lock: URL): Promise { + try { + const metadata = await deno.stat(lock); + if (metadata.mtime === undefined || metadata.mtime === null) { + return true; + } + return Date.now() - metadata.mtime.getTime() > CACHE_LOCK_STALE_MS; + } catch { + return true; + } +} + +function isDenoFsError(error: unknown, code: string, name: string): boolean { + return ( + typeof error === 'object' && + error !== null && + (('code' in error && error.code === code) || ('name' in error && error.name === name)) + ); +} + +async function delay(milliseconds: number): Promise { + await new Promise((resolve) => setTimeout(resolve, milliseconds)); +} + async function resolveDenoIcuDataDirectory( deno: DenoRuntime, expectedVersion: string, @@ -443,14 +566,18 @@ function encodePathSegment(value: string): string { } function resolvePackageJsonUrl(packageName: string): URL { + const specifier = `${packageName}/package.json`; const resolver = (import.meta as ImportMeta & { resolve?: (specifier: string) => string }) .resolve; if (resolver === undefined) { - throw new Error('Deno native resolution requires import.meta.resolve support'); + return resolvePackageJsonUrlWithRequire(packageName, specifier); } try { - return new URL(resolver(`${packageName}/package.json`)); + return new URL(resolver(specifier)); } catch (error) { + if (importMetaResolveUnsupported(error)) { + return resolvePackageJsonUrlWithRequire(packageName, specifier); + } throw new Error( `${packageName} is not installed; import Oliphaunt from npm:@oliphaunt/ts with optional dependencies enabled`, { cause: error }, @@ -459,18 +586,44 @@ function resolvePackageJsonUrl(packageName: string): URL { } function optionalResolvePackageJsonUrl(packageName: string): URL | undefined { + const specifier = `${packageName}/package.json`; const resolver = (import.meta as ImportMeta & { resolve?: (specifier: string) => string }) .resolve; if (resolver === undefined) { - throw new Error('Deno native resolution requires import.meta.resolve support'); + return optionalResolvePackageJsonUrlWithRequire(specifier); + } + try { + return new URL(resolver(specifier)); + } catch (error) { + if (importMetaResolveUnsupported(error)) { + return optionalResolvePackageJsonUrlWithRequire(specifier); + } + return undefined; + } +} + +function resolvePackageJsonUrlWithRequire(packageName: string, specifier: string): URL { + const resolved = optionalResolvePackageJsonUrlWithRequire(specifier); + if (resolved !== undefined) { + return resolved; } + throw new Error( + `${packageName} is not installed; import Oliphaunt from npm:@oliphaunt/ts with optional dependencies enabled`, + ); +} + +function optionalResolvePackageJsonUrlWithRequire(specifier: string): URL | undefined { try { - return new URL(resolver(`${packageName}/package.json`)); + return pathToFileURL(require.resolve(specifier)); } catch { return undefined; } } +function importMetaResolveUnsupported(error: unknown): boolean { + return error instanceof Error && error.message.includes('import.meta.resolve'); +} + async function requireFile(deno: DenoRuntime, path: URL, source: string): Promise { try { const info = await deno.stat(path); @@ -478,7 +631,9 @@ async function requireFile(deno: DenoRuntime, path: URL, source: string): Promis return; } } catch {} - throw new Error(`${source} does not point to an existing file: ${decodeURIComponent(path.pathname)}`); + throw new Error( + `${source} does not point to an existing file: ${decodeURIComponent(path.pathname)}`, + ); } async function requireDirectory(deno: DenoRuntime, path: URL, source: string): Promise { @@ -507,7 +662,9 @@ async function requireIcuDataDirectory( return; } } - throw new Error(`${source} does not contain ICU icudt data files: ${decodeURIComponent(path.pathname)}`); + throw new Error( + `${source} does not contain ICU icudt data files: ${decodeURIComponent(path.pathname)}`, + ); } function denoRuntime(): DenoRuntime { diff --git a/src/sdks/js/src/native/assets-node.ts b/src/sdks/js/src/native/assets-node.ts index 1611c3d3..4da3d558 100644 --- a/src/sdks/js/src/native/assets-node.ts +++ b/src/sdks/js/src/native/assets-node.ts @@ -1,8 +1,9 @@ -import { createHash } from 'node:crypto'; -import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises'; +import { createHash, randomUUID } from 'node:crypto'; +import { cp, mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; import { arch, platform, tmpdir } from 'node:os'; import { dirname, isAbsolute, join, relative, resolve } from 'node:path'; +import { setTimeout as delay } from 'node:timers/promises'; import { type GeneratedExtensionMetadata, generatedExtensionBySqlName, @@ -79,6 +80,9 @@ type ExtensionPackageMetadata = { }; const require = createRequire(import.meta.url); +const CACHE_LOCK_POLL_MS = 25; +const CACHE_LOCK_TIMEOUT_MS = 30_000; +const CACHE_LOCK_STALE_MS = 5 * 60_000; export async function resolveNodeNativeInstall( libraryPath?: string, @@ -114,6 +118,7 @@ export async function materializeNodeExtensionInstall( `native extension packages require a package-managed runtime directory; selected extensions: ${selected.join(', ')}`, ); } + const installRuntimeDirectory = install.runtimeDirectory; const versions = await packageVersions(); const target = liboliphauntPackageTarget(platform(), arch()); @@ -124,7 +129,7 @@ export async function materializeNodeExtensionInstall( ); const cacheKey = runtimeCacheKey({ libraryPath: install.libraryPath, - runtimeDirectory: install.runtimeDirectory, + runtimeDirectory: installRuntimeDirectory, target: target.id, packages: packages.map((entry) => ({ name: entry.name, @@ -139,7 +144,7 @@ export async function materializeNodeExtensionInstall( const marker = join(root, 'manifest.json'); const manifest = JSON.stringify( { - runtimeDirectory: install.runtimeDirectory, + runtimeDirectory: installRuntimeDirectory, libraryPath: install.libraryPath, target: target.id, packages: packages.map((entry) => ({ @@ -155,26 +160,27 @@ export async function materializeNodeExtensionInstall( return { ...install, runtimeDirectory, moduleDirectory }; } - await rm(root, { force: true, recursive: true }); - await mkdir(root, { recursive: true }); - await cp(install.runtimeDirectory, runtimeDirectory, { recursive: true }); - await mkdir(moduleDirectory, { recursive: true }); - for (const source of nativeModuleDirectoryCandidates(install.libraryPath)) { - if (await isDirectory(source)) { - await cp(source, moduleDirectory, { force: true, recursive: true }); - } - } - for (const entry of packages) { - for (const source of entry.runtimeDirectories) { - await cp(source, runtimeDirectory, { force: true, recursive: true }); - } - for (const source of entry.moduleDirectories) { + await publishRuntimeCache(root, manifest, async (stageRoot) => { + const stageRuntimeDirectory = join(stageRoot, 'runtime'); + const stageModuleDirectory = join(stageRoot, 'modules'); + await cp(installRuntimeDirectory, stageRuntimeDirectory, { recursive: true }); + await mkdir(stageModuleDirectory, { recursive: true }); + for (const source of nativeModuleDirectoryCandidates(install.libraryPath)) { if (await isDirectory(source)) { - await cp(source, moduleDirectory, { force: true, recursive: true }); + await cp(source, stageModuleDirectory, { force: true, recursive: true }); } } - } - await writeFile(marker, manifest, 'utf8'); + for (const entry of packages) { + for (const source of entry.runtimeDirectories) { + await cp(source, stageRuntimeDirectory, { force: true, recursive: true }); + } + for (const source of entry.moduleDirectories) { + if (await isDirectory(source)) { + await cp(source, stageModuleDirectory, { force: true, recursive: true }); + } + } + } + }); return { ...install, runtimeDirectory, moduleDirectory }; } @@ -644,17 +650,107 @@ async function materializeNativeToolsRuntime(config: { return runtimeDirectory; } - await rm(root, { force: true, recursive: true }); - await mkdir(root, { recursive: true }); - await cp(config.runtimePackage.runtimeDirectory, runtimeDirectory, { recursive: true }); - await cp(config.toolsPackage.runtimeDirectory, runtimeDirectory, { - force: true, - recursive: true, + await publishRuntimeCache(root, manifest, async (stageRoot) => { + const stageRuntimeDirectory = join(stageRoot, 'runtime'); + await cp(config.runtimePackage.runtimeDirectory, stageRuntimeDirectory, { recursive: true }); + await cp(config.toolsPackage.runtimeDirectory, stageRuntimeDirectory, { + force: true, + recursive: true, + }); }); - await writeFile(marker, manifest, 'utf8'); return runtimeDirectory; } +async function publishRuntimeCache( + root: string, + manifest: string, + build: (stageRoot: string) => Promise, +): Promise { + const marker = join(root, 'manifest.json'); + if ((await optionalRead(marker)) === manifest) { + return; + } + await mkdir(dirname(root), { recursive: true }); + await withRuntimeCacheLock(root, async () => { + if ((await optionalRead(marker)) === manifest) { + return; + } + const unique = `${process.pid}-${randomUUID()}`; + const stageRoot = `${root}.build-${unique}`; + const oldRoot = `${root}.old-${unique}`; + await rm(stageRoot, { force: true, recursive: true }); + await rm(oldRoot, { force: true, recursive: true }); + let movedExistingRoot = false; + try { + await mkdir(stageRoot, { recursive: true }); + await build(stageRoot); + await writeFile(join(stageRoot, 'manifest.json'), manifest, 'utf8'); + try { + await rename(root, oldRoot); + movedExistingRoot = true; + } catch (error) { + if (!isErrorCode(error, 'ENOENT')) { + throw error; + } + } + try { + await rename(stageRoot, root); + } catch (error) { + if (movedExistingRoot) { + await rename(oldRoot, root).catch(() => undefined); + movedExistingRoot = false; + } + throw error; + } + if (movedExistingRoot) { + await rm(oldRoot, { force: true, recursive: true }).catch(() => undefined); + } + } catch (error) { + await rm(stageRoot, { force: true, recursive: true }); + await rm(oldRoot, { force: true, recursive: true }); + throw error; + } + }); +} + +async function withRuntimeCacheLock(root: string, callback: () => Promise): Promise { + const lock = `${root}.lock`; + const deadline = Date.now() + CACHE_LOCK_TIMEOUT_MS; + while (true) { + try { + await mkdir(lock); + break; + } catch (error) { + if (!isErrorCode(error, 'EEXIST')) { + throw error; + } + if (await runtimeCacheLockIsStale(lock)) { + await rm(lock, { force: true, recursive: true }); + continue; + } + if (Date.now() >= deadline) { + throw new Error(`timed out waiting for Oliphaunt runtime cache lock: ${lock}`); + } + await delay(CACHE_LOCK_POLL_MS); + } + } + + try { + return await callback(); + } finally { + await rm(lock, { force: true, recursive: true }); + } +} + +async function runtimeCacheLockIsStale(lock: string): Promise { + try { + const metadata = await stat(lock); + return Date.now() - metadata.mtimeMs > CACHE_LOCK_STALE_MS; + } catch { + return true; + } +} + function resolvePackageJson(packageName: string): string { try { return require.resolve(`${packageName}/package.json`); @@ -812,6 +908,10 @@ async function optionalRead(path: string): Promise { } } +function isErrorCode(error: unknown, code: string): boolean { + return typeof error === 'object' && error !== null && 'code' in error && error.code === code; +} + function extensionPackageName(sqlName: string): string { return `@oliphaunt/extension-${sqlName.replaceAll('_', '-')}`; } diff --git a/src/sdks/js/tools/check-sdk.sh b/src/sdks/js/tools/check-sdk.sh index 598a3ce3..cdf64ab0 100755 --- a/src/sdks/js/tools/check-sdk.sh +++ b/src/sdks/js/tools/check-sdk.sh @@ -378,6 +378,12 @@ require_source_text "$package_dir/src/native/common.ts" "liboliphauntPackageTarg "TypeScript SDK must select the compatible liboliphaunt platform package" require_source_text "$package_dir/src/native/assets-node.ts" "runtimeRelativePath" \ "TypeScript Node/Bun native binding must resolve runtime resources from the selected liboliphaunt package" +require_source_text "$package_dir/src/native/assets-node.ts" "publishRuntimeCache" \ + "TypeScript Node/Bun native binding must publish package-managed runtime caches through a staged cache root" +require_source_text "$package_dir/src/native/assets-node.ts" "withRuntimeCacheLock" \ + "TypeScript Node/Bun native binding must serialize package-managed runtime cache publication" +require_source_text "$package_dir/src/native/assets-node.ts" ".build-" \ + "TypeScript Node/Bun native binding must build package-managed runtime caches outside the live root" require_source_text "$package_dir/src/native/node-addon.ts" "oliphaunt-node-direct" \ "TypeScript Node native-direct binding must resolve the installed prebuilt Node-API adapter package" require_source_text "$root/src/runtimes/node-direct/tools/build-node-addon.sh" "oliphaunt-node-direct-\$version-\$target.tar.gz" \ @@ -394,6 +400,14 @@ require_source_text "$package_dir/src/native/assets-deno.ts" "materializeDenoToo "TypeScript Deno native binding must merge liboliphaunt and oliphaunt-tools runtime trees" require_source_text "$package_dir/src/native/assets-deno.ts" "nativeClientToolsForTarget" \ "TypeScript Deno native binding must validate pg_dump and psql in the split tools package" +require_source_text "$package_dir/src/native/assets-deno.ts" "publishDenoRuntimeCache" \ + "TypeScript Deno native binding must publish package-managed runtime caches through a staged cache root" +require_source_text "$package_dir/src/native/assets-deno.ts" "withDenoRuntimeCacheLock" \ + "TypeScript Deno native binding must serialize package-managed runtime cache publication" +require_source_text "$package_dir/src/native/assets-deno.ts" ".build-" \ + "TypeScript Deno native binding must build package-managed runtime caches outside the live root" +require_source_text "$package_dir/src/native/assets-deno.ts" "deno.rename" \ + "TypeScript Deno native binding must install finished runtime caches with runtime-owned rename" require_source_text "$package_dir/src/native/deno.ts" "install.packageManaged" \ "TypeScript Deno nativeDirect must reject registry-managed extension materialization until it has a dedicated resolver" require_source_text "$package_dir/src/runtime/server.ts" "resolveDenoNativeInstall" \ diff --git a/tools/policy/check-sdk-parity.sh b/tools/policy/check-sdk-parity.sh index 162a3752..515459ff 100755 --- a/tools/policy/check-sdk-parity.sh +++ b/tools/policy/check-sdk-parity.sh @@ -246,6 +246,18 @@ require_text src/sdks/js/src/native/assets-deno.ts "materializeDenoToolsRuntime" "TypeScript Deno native resolver must merge liboliphaunt and oliphaunt-tools runtime trees" require_text src/sdks/js/src/native/assets-deno.ts "nativeClientToolsForTarget" \ "TypeScript Deno native resolver must validate pg_dump and psql in split tools packages" +require_text src/sdks/js/src/native/assets-node.ts "publishRuntimeCache" \ + "TypeScript Node/Bun native resolver must publish package-managed runtime caches through a staged cache root" +require_text src/sdks/js/src/native/assets-node.ts "withRuntimeCacheLock" \ + "TypeScript Node/Bun native resolver must serialize package-managed runtime cache publication" +require_text src/sdks/js/src/native/assets-node.ts ".build-" \ + "TypeScript Node/Bun native resolver must build package-managed runtime caches outside the live root" +require_text src/sdks/js/src/native/assets-deno.ts "publishDenoRuntimeCache" \ + "TypeScript Deno native resolver must publish package-managed runtime caches through a staged cache root" +require_text src/sdks/js/src/native/assets-deno.ts "withDenoRuntimeCacheLock" \ + "TypeScript Deno native resolver must serialize package-managed runtime cache publication" +require_text src/sdks/js/src/native/assets-deno.ts "deno.rename" \ + "TypeScript Deno native resolver must install finished runtime caches with runtime-owned rename" require_text src/sdks/js/src/native/deno.ts "install.packageManaged" \ "TypeScript Deno nativeDirect must keep registry-managed extension materialization explicitly unsupported" require_text src/sdks/js/src/runtime/server.ts "resolveDenoNativeInstall" \ From 2cd7e1887fa2288c8234e2f743f89c192f96ab78 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 20:44:42 +0000 Subject: [PATCH 120/137] test: cover mobile runtime feature validation --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 15 +++++++- .../maintainers/extension-packaging-policy.md | 1 + docs/maintainers/sdk-parity-policy.md | 7 ++-- docs/maintainers/sdk-products-policy.md | 4 +- .../OliphauntAndroidRuntimeAssetsTest.kt | 28 ++++++++++++++ .../Tests/OliphauntTests/OliphauntTests.swift | 37 +++++++++++++++++++ tools/policy/check-sdk-parity.sh | 6 +++ 7 files changed, 92 insertions(+), 6 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 9bf7d7e8..2b70ce43 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -58,7 +58,7 @@ until the current-state gates here are checked with fresh local evidence. package-managed runtime/tool/extension materialization publishes through a temp/marker or equivalent atomic protocol instead of rebuilding cache roots in place. -- [ ] Add Swift and Kotlin negative tests for unsupported mobile +- [x] Add Swift and Kotlin negative tests for unsupported mobile `runtimeFeatures`, and update maintainer docs so the shared runtime-resource manifest field list includes `runtimeFeatures`. @@ -114,6 +114,19 @@ until the current-state gates here are checked with fresh local evidence. `tools/coverage/summarize --allow-missing --products-json '["oliphaunt-js"]'`, `bash tools/policy/check-coverage.sh oliphaunt-js`, and `git diff --check`. The coverage summary reported 81.61% line coverage against the 80% gate. +- 2026-06-26: Added Swift and Kotlin negative coverage for unsupported + `runtimeFeatures` in shared runtime-resource manifests, kept positive + package-size report coverage for `runtimeFeatures=icu`, and updated maintainer + manifest field docs plus SDK parity policy checks. Fresh checks passed: + `bash tools/policy/check-sdk-parity.sh`, + `bash tools/policy/check-sdk-mobile-extension-surface.sh`, + `ANDROID_HOME=$PWD/target/android-sdk ANDROID_SDK_ROOT=$PWD/target/android-sdk bash src/sdks/kotlin/tools/check-sdk.sh check-static`, + `ANDROID_HOME=$PWD/target/android-sdk ANDROID_SDK_ROOT=$PWD/target/android-sdk bash src/sdks/kotlin/tools/check-sdk.sh test-unit`, + `python3 tools/release/check_release_metadata.py`, + `python3 tools/release/check_consumer_shape.py --products-json + '["oliphaunt-swift","oliphaunt-kotlin","oliphaunt-react-native"]'`, and + `git diff --check`. Swift executable validation could not run in this Linux + container because the `swift` command is not installed. - 2026-06-26: Current-state example e2e re-run passed against the staged local registries from commit `895ed8d`: `examples/tools/run-electron-driver-smoke.sh examples/electron`, `examples/tools/run-electron-driver-smoke.sh diff --git a/docs/maintainers/extension-packaging-policy.md b/docs/maintainers/extension-packaging-policy.md index 516031af..03a4d9ff 100644 --- a/docs/maintainers/extension-packaging-policy.md +++ b/docs/maintainers/extension-packaging-policy.md @@ -249,6 +249,7 @@ The runtime manifest records exact extension names: schema=oliphaunt-runtime-resources-v1 layout=postgres-runtime-files-v1 extensions=vector +runtimeFeatures= sharedPreloadLibraries= mobileStaticRegistryState=complete mobileStaticRegistryRegistered=vector diff --git a/docs/maintainers/sdk-parity-policy.md b/docs/maintainers/sdk-parity-policy.md index edc20934..6b43f884 100644 --- a/docs/maintainers/sdk-parity-policy.md +++ b/docs/maintainers/sdk-parity-policy.md @@ -68,9 +68,10 @@ SDK that native app developers also use. The Rust SDK owns the runtime-resource producer contract. Generated manifests must declare `schema=oliphaunt-runtime-resources-v1` and the expected -per-extension `layout`; Swift and Kotlin validate those fields before using -generated resources, and React Native inherits the same checks through those -platform SDKs. +per-package `layout`, `extensions`, `runtimeFeatures`, +`sharedPreloadLibraries`, and mobile static-registry metadata; Swift and Kotlin +validate those fields before using generated resources, and React Native +inherits the same checks through those platform SDKs. ## Artifact Resolution diff --git a/docs/maintainers/sdk-products-policy.md b/docs/maintainers/sdk-products-policy.md index 29f9793b..ae633378 100644 --- a/docs/maintainers/sdk-products-policy.md +++ b/docs/maintainers/sdk-products-policy.md @@ -101,8 +101,8 @@ before the first database open. Every SDK consumes the resulting runtime resources through the same manifest fields. Generated manifests record `schema=oliphaunt-runtime-resources-v1`, per-package `layout`, -`extensions`, and `sharedPreloadLibraries` so SDK-bound artifacts can be audited -independently of the local build path. +`extensions`, `runtimeFeatures`, and `sharedPreloadLibraries` so SDK-bound +artifacts can be audited independently of the local build path. Swift and Kotlin reject unknown package layouts rather than silently accepting stale app resources; React Native inherits those checks through the platform SDKs. diff --git a/src/sdks/kotlin/oliphaunt/src/androidUnitTest/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssetsTest.kt b/src/sdks/kotlin/oliphaunt/src/androidUnitTest/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssetsTest.kt index b36d72bc..f5b74158 100644 --- a/src/sdks/kotlin/oliphaunt/src/androidUnitTest/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssetsTest.kt +++ b/src/sdks/kotlin/oliphaunt/src/androidUnitTest/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssetsTest.kt @@ -18,6 +18,7 @@ class OliphauntAndroidRuntimeAssetsTest { "layout" to "postgres-runtime-files-v1", "cacheKey" to "runtime-smoke", "extensions" to "pg_trgm,vector", + "runtimeFeatures" to "icu", "sharedPreloadLibraries" to "auto_explain", "mobileStaticRegistryState" to "complete", "mobileStaticRegistryRegistered" to "vector", @@ -28,6 +29,7 @@ class OliphauntAndroidRuntimeAssetsTest { assertEquals("runtime-smoke", parsed.cacheKey) assertEquals(setOf("pg_trgm", "vector"), parsed.extensions) + assertEquals(setOf("icu"), parsed.runtimeFeatures) assertEquals(setOf("auto_explain"), parsed.sharedPreloadLibraries) assertEquals("complete", parsed.mobileStaticRegistryState) } @@ -118,6 +120,7 @@ class OliphauntAndroidRuntimeAssetsTest { layout=postgres-runtime-files-v1 cacheKey=runtime-smoke extensions=hstore,vector + runtimeFeatures=icu sharedPreloadLibraries= mobileStaticRegistryState=complete mobileStaticRegistryRegistered=vector,hstore @@ -134,6 +137,7 @@ class OliphauntAndroidRuntimeAssetsTest { assertEquals(listOf("hstore", "vector"), report?.mobileStaticRegistryRegistered) assertEquals(emptyList(), report?.mobileStaticRegistryPending) assertEquals(listOf("hstore", "vector"), report?.nativeModuleStems) + assertEquals(listOf("icu"), report?.runtimeFeatures) } finally { resourceRoot.deleteRecursively() } @@ -470,6 +474,29 @@ class OliphauntAndroidRuntimeAssetsTest { assertTrue(badExtension.message.orEmpty().contains("extension id")) } + @Test + fun rejectsUnsupportedRuntimeFeatures() { + val error = + assertFailsWith { + OliphauntAndroidRuntimeAssets.parseManifestProperties( + "oliphaunt/runtime", + manifestProperties( + "schema" to "oliphaunt-runtime-resources-v1", + "layout" to "postgres-runtime-files-v1", + "cacheKey" to "runtime-smoke", + "extensions" to "vector", + "runtimeFeatures" to "jit", + "mobileStaticRegistryState" to "complete", + "mobileStaticRegistryRegistered" to "vector", + "mobileStaticRegistryPending" to "", + "nativeModuleStems" to "vector", + ), + ) + } + + assertTrue(error.message.orEmpty().contains("runtime feature(s) jit are not supported")) + } + @Test fun rejectsUnsupportedRuntimeResourcesSchema() { val error = @@ -686,6 +713,7 @@ private fun writeReleaseShapedRuntime( layout=postgres-runtime-files-v1 cacheKey=runtime-smoke extensions=$extensions + runtimeFeatures=icu sharedPreloadLibraries=$sharedPreloadLibraries mobileStaticRegistryState=complete mobileStaticRegistryRegistered=$extensions diff --git a/src/sdks/swift/Tests/OliphauntTests/OliphauntTests.swift b/src/sdks/swift/Tests/OliphauntTests/OliphauntTests.swift index 7d08d2cd..b251b9c1 100644 --- a/src/sdks/swift/Tests/OliphauntTests/OliphauntTests.swift +++ b/src/sdks/swift/Tests/OliphauntTests/OliphauntTests.swift @@ -1297,6 +1297,7 @@ func runtimeResourcesExposePackageSizeReport() throws { #expect(report.templatePgdataBytes == 40) #expect(report.staticRegistryBytes == 45) #expect(report.selectedExtensionBytes == 30) + #expect(report.runtimeFeatures == ["icu"]) #expect(report.extensions == [ OliphauntExtensionSizeReport( name: "vector", @@ -1677,6 +1678,40 @@ func runtimeResourcesRejectMalformedSharedPreloadLibraryMetadata() throws { } } +@Test +func runtimeResourcesRejectUnsupportedRuntimeFeatures() throws { + let fixture = try makeRuntimeResourceFixture() + defer { + try? FileManager.default.removeItem(at: fixture.root) + } + try writeText( + fixture.resourceRoot.appendingPathComponent("runtime/manifest.properties"), + """ + schema=oliphaunt-runtime-resources-v1 + layout=postgres-runtime-files-v1 + cacheKey=test-runtime-v1 + extensions=vector + runtimeFeatures=jit + sharedPreloadLibraries= + mobileStaticRegistryState=complete + mobileStaticRegistryRegistered=vector + mobileStaticRegistryPending= + nativeModuleStems=vector + """ + ) + let resources = OliphauntRuntimeResources( + resourceRoot: fixture.resourceRoot, + cacheRoot: fixture.cacheRoot + ) + + do { + _ = try resources.materializeRuntime(requestedExtensions: ["vector"]) + Issue.record("runtime resources should reject unsupported runtime features") + } catch OliphauntError.engine(let message) { + #expect(message.contains("runtime feature(s) jit are not supported")) + } +} + @Test func runtimeResourcesRejectUnsupportedSchema() throws { let fixture = try makeRuntimeResourceFixture() @@ -2311,6 +2346,7 @@ private func makeRuntimeResourceFixture(sharedPreloadLibraries: String) throws - layout=postgres-runtime-files-v1 cacheKey=test-runtime-v1 extensions=vector + runtimeFeatures=icu sharedPreloadLibraries=\(sharedPreloadLibraries) mobileStaticRegistryState=complete mobileStaticRegistryRegistered=vector @@ -2337,6 +2373,7 @@ private func makeRuntimeResourceFixture(sharedPreloadLibraries: String) throws - layout=postgres-template-pgdata-v1 cacheKey=test-template-v1 extensions= + runtimeFeatures= sharedPreloadLibraries= mobileStaticRegistryState=not-required mobileStaticRegistryRegistered= diff --git a/tools/policy/check-sdk-parity.sh b/tools/policy/check-sdk-parity.sh index 515459ff..ad48bb9f 100755 --- a/tools/policy/check-sdk-parity.sh +++ b/tools/policy/check-sdk-parity.sh @@ -1389,6 +1389,12 @@ require_text src/sdks/kotlin/oliphaunt/src/androidMain/kotlin/dev/oliphaunt/Olip "Kotlin Android SDK must validate the shared runtime-resource schema" require_text src/sdks/kotlin/oliphaunt/src/androidUnitTest/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssetsTest.kt "unsupported runtime resource schema" \ "Kotlin Android SDK must test stale runtime-resource schema rejection" +require_text src/sdks/swift/Tests/OliphauntTests/OliphauntTests.swift "runtimeResourcesRejectUnsupportedRuntimeFeatures" \ + "Swift SDK tests must reject unsupported shared runtime-resource runtimeFeatures" +require_text src/sdks/kotlin/oliphaunt/src/androidUnitTest/kotlin/dev/oliphaunt/OliphauntAndroidRuntimeAssetsTest.kt "rejectsUnsupportedRuntimeFeatures" \ + "Kotlin Android SDK tests must reject unsupported shared runtime-resource runtimeFeatures" +require_text docs/maintainers/sdk-parity-policy.md 'runtimeFeatures' \ + "SDK parity docs must list runtimeFeatures in the shared runtime-resource manifest fields" require_text src/sdks/swift/Sources/Oliphaunt/OliphauntRuntimeResources.swift "OliphauntRuntimeResourceSizeReport" \ "Swift SDK must expose the shared package-size report" require_text src/sdks/swift/Tests/OliphauntTests/OliphauntTests.swift "runtimeResourcesExposePackageSizeReport" \ From cdd08a674cbb8820e1b2e0489ba678370f001e5f Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 20:57:30 +0000 Subject: [PATCH 121/137] test: enforce split tools parity checks --- .../internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 13 +++++++++++++ examples/moon.yml | 2 ++ examples/tools/check-examples.sh | 2 ++ .../tauri-sqlx-vanilla/src-tauri/src/bench.rs | 3 --- src/bindings/wasix-rust/moon.yml | 1 + src/sdks/kotlin/tools/check-sdk.sh | 16 ++++++++++++++++ src/sdks/react-native/tools/check-sdk.sh | 13 ++++++++++++- .../tools/expo-runner-runtime-resources.sh | 2 ++ .../policy/check-sdk-mobile-extension-surface.sh | 10 ++++++++++ 9 files changed, 58 insertions(+), 4 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 2b70ce43..f6423c42 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -148,6 +148,19 @@ until the current-state gates here are checked with fresh local evidence. --locked --bin profile_queries -- --fresh --rows 10 --json-out target/oliphaunt-wasix-rust/examples/tauri-sqlx-vanilla/profile-e2e-2026-06-26.json`; the generated report included startup phase `validate split WASIX tools`. +- 2026-06-26: Tightened fresh parity checks for runtime-resource metadata and + split WASIX example deps. Kotlin Android, React Native Android, and the React + Native Expo runtime-resource helper now emit or assert `runtimeFeatures=` in + generated manifests; the nested WASIX SQLx example policy now requires the + root runtime AOT crate alongside `oliphaunt-wasix-tools` and tools-AOT crates; + and the nested tool smoke can no longer skip `preflight_tools`, `dump_sql`, or + `psql` on non-TCP endpoints. +- 2026-06-26: React Native Android static-extension smoke now uses a per-run + link-evidence path so CMake cannot reuse an old configure result after the + harness deletes evidence. Fresh checks passed: + `ANDROID_HOME=$PWD/target/android-sdk ANDROID_SDK_ROOT=$PWD/target/android-sdk + OLIPHAUNT_SDK_CHECK_SCRATCH=$(mktemp -d /tmp/oliphaunt-rn-check.XXXXXX) bash + src/sdks/react-native/tools/check-sdk.sh build-android-bridge`. - 2026-06-26: Split root/tools package-shape checks passed with `python3 tools/release/check_release_metadata.py`, `python3 tools/release/check_consumer_shape.py`, diff --git a/examples/moon.yml b/examples/moon.yml index bbd71ec8..042c181e 100644 --- a/examples/moon.yml +++ b/examples/moon.yml @@ -26,6 +26,8 @@ tasks: - "!/src/sdks/react-native/examples/**/node_modules" - "!/src/sdks/react-native/examples/**/node_modules/**" - "/src/bindings/wasix-rust/examples/**/*" + - "/src/bindings/wasix-rust/moon.yml" + - "/src/bindings/wasix-rust/tools/check-examples.sh" - "/src/sdks/react-native/tools/mobile-e2e.sh" - "/src/sdks/react-native/tools/expo-android-runner.sh" - "/src/sdks/react-native/tools/expo-ios-runner.sh" diff --git a/examples/tools/check-examples.sh b/examples/tools/check-examples.sh index 115d488f..dbb0fb88 100755 --- a/examples/tools/check-examples.sh +++ b/examples/tools/check-examples.sh @@ -138,8 +138,10 @@ require_wasix_tools_smoke "examples/electron-wasix/src-wasix/src/main.rs" require_text "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml" 'registry = "oliphaunt-local"' require_text "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml" '"tools"' require_text "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml" 'oliphaunt-wasix-tools' +require_text "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml" 'liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu' require_text "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml" 'oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu' require_wasix_tools_smoke "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs" +reject_text "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs" 'tcp_addr\(\)\.is_none\(\)' reject_text "examples/electron/package.json" '"@oliphaunt/ts": "workspace:\*"' reject_text "examples/tauri/src-tauri/Cargo.toml" 'path = "../../../src/sdks/rust' reject_text "examples/tauri-wasix/src-tauri/Cargo.toml" 'path = "../../../src/bindings/wasix-rust' diff --git a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs index b191c312..20678a3a 100644 --- a/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs +++ b/src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs @@ -338,9 +338,6 @@ impl DatabaseHarness { } fn validate_wasix_tools(server: &OliphauntServer) -> Result<()> { - if server.tcp_addr().is_none() { - return Ok(()); - } server .preflight_tools() .context("preflight split WASIX pg_dump and psql tools")?; diff --git a/src/bindings/wasix-rust/moon.yml b/src/bindings/wasix-rust/moon.yml index 0a9d42c9..2d5c8c4c 100644 --- a/src/bindings/wasix-rust/moon.yml +++ b/src/bindings/wasix-rust/moon.yml @@ -144,6 +144,7 @@ tasks: - "/src/bindings/wasix-rust/examples/**/*" - "!/src/bindings/wasix-rust/examples/**/node_modules" - "!/src/bindings/wasix-rust/examples/**/node_modules/**" + - "/examples/tools/with-local-registries.sh" - "/src/bindings/wasix-rust/tools/check-examples.sh" - "/src/runtimes/liboliphaunt/wasix/**/*" options: diff --git a/src/sdks/kotlin/tools/check-sdk.sh b/src/sdks/kotlin/tools/check-sdk.sh index cafb13af..b3ad788c 100755 --- a/src/sdks/kotlin/tools/check-sdk.sh +++ b/src/sdks/kotlin/tools/check-sdk.sh @@ -316,6 +316,7 @@ schema=oliphaunt-runtime-resources-v1 cacheKey=runtime-smoke layout=postgres-runtime-files-v1 extensions=vector +runtimeFeatures= sharedPreloadLibraries= mobileStaticRegistryState=complete mobileStaticRegistryRegistered=vector @@ -328,6 +329,7 @@ schema=oliphaunt-runtime-resources-v1 cacheKey=template-smoke layout=postgres-template-pgdata-v1 extensions= +runtimeFeatures= sharedPreloadLibraries= mobileStaticRegistryState=not-required mobileStaticRegistryRegistered= @@ -410,6 +412,16 @@ REPORT rm -rf "$tmp_assets" "$tmp_static_jni" exit 1 fi + if ! grep -Fxq "runtimeFeatures=" "$generated/oliphaunt/runtime/manifest.properties"; then + echo "Kotlin Android generated runtime manifest did not preserve runtime feature metadata" >&2 + rm -rf "$tmp_assets" "$tmp_static_jni" + exit 1 + fi + if ! grep -Fxq "runtimeFeatures=" "$generated/oliphaunt/template-pgdata/manifest.properties"; then + echo "Kotlin Android generated template manifest did not preserve runtime feature metadata" >&2 + rm -rf "$tmp_assets" "$tmp_static_jni" + exit 1 + fi if ! grep -Fxq "mobileStaticRegistrySource=static-registry/oliphaunt_static_registry.c" "$generated/oliphaunt/runtime/manifest.properties"; then echo "Kotlin Android generated runtime manifest did not preserve mobile static-registry source" >&2 rm -rf "$tmp_assets" "$tmp_static_jni" @@ -596,6 +608,8 @@ if [ -n "${ANDROID_HOME:-}" ]; then "Kotlin Android split runtime manifest did not emit the runtime resources layout" require_manifest_line "$split_runtime_manifest" "extensions=vector" \ "Kotlin Android split runtime manifest did not record selected vector extension" + require_manifest_line "$split_runtime_manifest" "runtimeFeatures=" \ + "Kotlin Android split runtime manifest did not record runtime feature metadata" require_manifest_line "$split_runtime_manifest" "sharedPreloadLibraries=" \ "Kotlin Android split runtime manifest did not record shared preload libraries" require_manifest_line "$split_runtime_manifest" "mobileStaticRegistryState=pending" \ @@ -612,6 +626,8 @@ if [ -n "${ANDROID_HOME:-}" ]; then "Kotlin Android split template manifest should not require mobile static registry work" require_manifest_line "$split_template_manifest" "mobileStaticRegistryPending=" \ "Kotlin Android split template manifest should not list pending mobile static registry modules" + require_manifest_line "$split_template_manifest" "runtimeFeatures=" \ + "Kotlin Android split template manifest should not list runtime features" require_manifest_line "$split_template_manifest" "sharedPreloadLibraries=" \ "Kotlin Android split template manifest should not list shared preload libraries" require_manifest_line "$split_template_manifest" "nativeModuleStems=" \ diff --git a/src/sdks/react-native/tools/check-sdk.sh b/src/sdks/react-native/tools/check-sdk.sh index 728054c6..e0866132 100755 --- a/src/sdks/react-native/tools/check-sdk.sh +++ b/src/sdks/react-native/tools/check-sdk.sh @@ -700,6 +700,8 @@ if [ "$run_android_platform_checks" = "1" ]; then "React Native Android split runtime manifest did not emit the runtime resources layout" require_manifest_line "$split_runtime_manifest" "extensions=vector" \ "React Native Android split runtime manifest did not record selected vector extension" + require_manifest_line "$split_runtime_manifest" "runtimeFeatures=" \ + "React Native Android split runtime manifest did not record runtime feature metadata" require_manifest_line "$split_runtime_manifest" "sharedPreloadLibraries=" \ "React Native Android split runtime manifest did not record shared preload libraries" require_manifest_line "$split_runtime_manifest" "mobileStaticRegistryState=pending" \ @@ -716,6 +718,8 @@ if [ "$run_android_platform_checks" = "1" ]; then "React Native Android split template manifest should not require mobile static registry work" require_manifest_line "$split_template_manifest" "mobileStaticRegistryPending=" \ "React Native Android split template manifest should not list pending mobile static registry modules" + require_manifest_line "$split_template_manifest" "runtimeFeatures=" \ + "React Native Android split template manifest should not list runtime features" require_manifest_line "$split_template_manifest" "sharedPreloadLibraries=" \ "React Native Android split template manifest should not list shared preload libraries" require_manifest_line "$split_template_manifest" "nativeModuleStems=" \ @@ -851,6 +855,7 @@ schema=oliphaunt-runtime-resources-v1 cacheKey=runtime-smoke layout=postgres-runtime-files-v1 extensions=vector +runtimeFeatures= sharedPreloadLibraries= mobileStaticRegistryState=complete mobileStaticRegistryRegistered=vector @@ -863,6 +868,7 @@ schema=oliphaunt-runtime-resources-v1 cacheKey=template-smoke layout=postgres-template-pgdata-v1 extensions= +runtimeFeatures= sharedPreloadLibraries= mobileStaticRegistryState=not-required mobileStaticRegistryRegistered= @@ -906,7 +912,7 @@ REPORT rm -f "$runtime_resources_incomplete_log" rm -rf "$tmp_assets_incomplete" - android_link_evidence="$scratch_root/android-static-extension-link-$android_smoke_abi.tsv" + android_link_evidence="$scratch_root/android-static-extension-link-$android_smoke_abi-$$.tsv" rm -f "$android_link_evidence" run "$gradle_cmd" -p "$android_dir" assembleDebug \ "-PoliphauntRuntimeResourcesDir=$tmp_assets" \ @@ -992,6 +998,11 @@ REPORT rm -rf "$tmp_assets" "$tmp_static_jni" exit 1 fi + if ! grep -Fxq "runtimeFeatures=" "$tmp_aar_extract/assets/oliphaunt/runtime/manifest.properties"; then + echo "Android AAR runtime manifest did not preserve runtime feature metadata" >&2 + rm -rf "$tmp_assets" "$tmp_static_jni" + exit 1 + fi if ! grep -Fxq "mobileStaticRegistrySource=static-registry/oliphaunt_static_registry.c" "$tmp_aar_extract/assets/oliphaunt/runtime/manifest.properties"; then echo "Android AAR runtime manifest did not preserve mobile static-registry source" >&2 rm -rf "$tmp_assets" "$tmp_static_jni" diff --git a/src/sdks/react-native/tools/expo-runner-runtime-resources.sh b/src/sdks/react-native/tools/expo-runner-runtime-resources.sh index 5ae1a5d3..cd867cc3 100644 --- a/src/sdks/react-native/tools/expo-runner-runtime-resources.sh +++ b/src/sdks/react-native/tools/expo-runner-runtime-resources.sh @@ -143,6 +143,7 @@ cacheKey=$runtime_key layout=postgres-runtime-files-v1 source=runtime extensions=$manifest_extensions +runtimeFeatures= sharedPreloadLibraries= mobileStaticRegistryState=$mobile_static_state mobileStaticRegistryRegistered=$mobile_static_registered @@ -157,6 +158,7 @@ layout=postgres-template-pgdata-v1 source=template-pgdata walSegmentSizeMB=$wal_segsize_mb extensions= +runtimeFeatures= sharedPreloadLibraries= mobileStaticRegistryState=not-required mobileStaticRegistryRegistered= diff --git a/tools/policy/check-sdk-mobile-extension-surface.sh b/tools/policy/check-sdk-mobile-extension-surface.sh index 5744d21f..46a68d4b 100755 --- a/tools/policy/check-sdk-mobile-extension-surface.sh +++ b/tools/policy/check-sdk-mobile-extension-surface.sh @@ -12,6 +12,10 @@ require_text src/sdks/kotlin/oliphaunt/build.gradle.kts "mobileStaticRegistryPen "Kotlin Android Gradle packaging must emit mobile static-registry metadata" require_text src/sdks/kotlin/oliphaunt/build.gradle.kts "sharedPreloadLibraries=" \ "Kotlin Android Gradle packaging must emit shared-preload metadata" +require_text src/sdks/kotlin/oliphaunt/build.gradle.kts "runtimeFeatures=" \ + "Kotlin Android Gradle packaging must emit runtime-feature metadata" +require_text src/sdks/kotlin/tools/check-sdk.sh "runtimeFeatures=" \ + "Kotlin Android SDK checks must validate runtime-feature metadata" require_text src/sdks/kotlin/oliphaunt/build.gradle.kts "fun oliphauntProperty(name: String)" \ "Kotlin Android Gradle packaging must accept canonical and existing capitalized Oliphaunt property spellings" require_text src/sdks/kotlin/oliphaunt/build.gradle.kts 'project.findProperty("O${it.drop(1)}")' \ @@ -92,6 +96,8 @@ require_text src/sdks/react-native/android/build.gradle "mobileStaticRegistryPen "React Native Android Gradle packaging must emit mobile static-registry metadata" require_text src/sdks/react-native/android/build.gradle "sharedPreloadLibraries=" \ "React Native Android Gradle packaging must emit shared-preload metadata" +require_text src/sdks/react-native/android/build.gradle "runtimeFeatures=" \ + "React Native Android Gradle packaging must emit runtime-feature metadata" require_text src/sdks/react-native/android/build.gradle "def oliphauntProperty = { String name ->" \ "React Native Android Gradle packaging must accept canonical and existing capitalized Oliphaunt property spellings" require_text src/sdks/react-native/android/build.gradle 'project.findProperty("O${name.substring(1)}")' \ @@ -132,6 +138,10 @@ require_text src/sdks/react-native/android/src/main/cpp/CMakeLists.txt "oliphaun "React Native Android CMake must link selected mobile static dependency archives" require_text src/sdks/react-native/tools/check-sdk.sh "-PoliphauntReactNativePackageRuntime=true" \ "React Native Android bridge check must enable packaged runtime mode when asserting static-extension link evidence" +require_text src/sdks/react-native/tools/expo-runner-runtime-resources.sh "runtimeFeatures=" \ + "React Native example runtime-resource packaging must emit runtime-feature metadata" +require_text src/sdks/react-native/tools/check-sdk.sh "runtimeFeatures=" \ + "React Native SDK checks must validate runtime-feature metadata" require_text src/sdks/react-native/android/build.gradle "resolveExtensionSelection" \ "React Native Android Gradle packaging must resolve exact extension selections" require_text src/sdks/react-native/README.md "published React Native artifact does not carry base \`liboliphaunt\`" \ From 1abb7aa08db261f6abed0b96283592fdce29c22f Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 21:07:22 +0000 Subject: [PATCH 122/137] fix: harden local cargo registry artifact shape --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 5 ++ .../examples-ci-release-validation.md | 7 +- tools/release/check_consumer_shape.py | 24 ++++++ tools/release/check_release_metadata.py | 8 ++ tools/release/local_registry_publish.py | 82 ++++++++++++++++++- 5 files changed, 121 insertions(+), 5 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index f6423c42..2eefcd90 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -912,3 +912,8 @@ until the current-state gates here are checked with fresh local evidence. requiring generated runtime assets in the unit lane. The full runtime-smoke lane remains responsible for executing `pg_dump` and `psql` once assets are available. +- On 2026-06-26, strict local Cargo registry publishing was tightened to fail + when release-shaped target artifact crates are missing and to reject stale + legacy unsplit WASIX artifact crates. Non-strict local publishing still prunes + unavailable target dependency tables, but now also removes matching optional + `dep:` feature entries so generated source crates remain valid. diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index b4bbdb9f..62f41607 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -112,8 +112,11 @@ the release/tooling surface after the runtime tool crate split. tools crate, ICU crate, WASIX extension crates, and AOT crates are all below the 10 MiB crates.io package limit in the local generated artifact set. - The local Cargo publisher now ignores legacy `oliphaunt-wasix-assets` and - old `oliphaunt-wasix-aot-*` artifact crates when stale target directories are - present, so local registries expose the new split package surface. + old `oliphaunt-wasix-aot-*` artifact crates in non-strict mode, and rejects + them in strict mode so local registries expose the new split package surface. +- Strict local Cargo publishing also fails when WASIX runtime/tools-AOT artifact + crates are missing, while non-strict pruning removes matching optional + feature deps from generated source crates to avoid invalid manifests. - Cargo example checks passed through `examples/tools/with-local-registries.sh` for native Tauri, Electron WASIX, Tauri WASIX, and the nested WASIX SQLx Tauri example. The WASIX example lockfiles now pin the new diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index f088d9fc..2a5e10fa 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -1936,6 +1936,30 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: "tools/policy/check-wasix-release-dependency-invariants.mjs", severity="P0", ) + local_registry_publisher = read_text("tools/release/local_registry_publish.py") + require( + findings, + product, + "wasix-local-registry-rejects-legacy-tools", + "LEGACY_WASIX_ARTIFACT_CRATES" in local_registry_publisher + and "ignored legacy WASIX artifact crate" in local_registry_publisher + and "if strict:\n raise RuntimeError(message)" in local_registry_publisher, + "Strict local Cargo publishing must reject stale unsplit WASIX artifact crates so examples resolve the current split runtime/tools surface.", + "tools/release/local_registry_publish.py", + severity="P0", + ) + require( + findings, + product, + "wasix-local-registry-requires-target-artifacts", + "strict=strict" in local_registry_publisher + and "is missing local registry inputs for target artifact dependencies" in local_registry_publisher + and "prune_missing_feature_dependencies" in local_registry_publisher + and 'value.startswith("dep:")' in local_registry_publisher, + "Strict local Cargo publishing must fail when release-shaped WASIX target runtime/tools-AOT artifact crates are missing; non-strict pruning must also remove stale feature dep entries.", + "tools/release/local_registry_publish.py", + severity="P0", + ) require( findings, product, diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 45c56d44..a7b0a7d2 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -361,6 +361,12 @@ def validate_local_registry_publisher() -> None: fail("local registry npm publishing must include the declared @oliphaunt/icu sidecar package") if f'oliphaunt-tools-{{lib_version}}-*' not in publisher: fail("local registry publisher must copy split oliphaunt-tools release assets when staging liboliphaunt native packages") + if ( + "LEGACY_WASIX_ARTIFACT_CRATES" not in publisher + or "ignored legacy WASIX artifact crate" not in publisher + or "if strict:\n raise RuntimeError(message)" not in publisher + ): + fail("strict local Cargo publishing must reject legacy unsplit WASIX artifact crates") if 'ROOT / "target" / "oliphaunt-wasix" / "cargo-artifacts",' in publisher or ( 'ROOT / "target" / "oliphaunt-wasix" / "release-assets",' in publisher ): @@ -374,6 +380,8 @@ def validate_local_registry_publisher() -> None: or "package_liboliphaunt_wasix_cargo_artifacts.py" not in publisher or "host_cargo_release_target()" not in publisher or "stage_release_asset_cargo_packages(roots, registry_root, dry_run, result)" not in publisher + or "strict=strict" not in publisher + or "prune_missing_feature_dependencies" not in publisher ): fail("local registry Cargo publishing must generate runtime/tool artifact crates from staged release assets") artifacts = local_registry_publish.local_publish_artifacts() diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index 519a6f02..34422e3b 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -1332,6 +1332,8 @@ def prune_missing_local_artifact_target_dependencies( manifest: Path, available_package_names: set[str], result: SurfaceResult, + *, + strict: bool, ) -> None: text = manifest.read_text(encoding="utf-8") lines = text.splitlines() @@ -1368,13 +1370,81 @@ def prune_missing_local_artifact_target_dependencies( if not removed: return - manifest.write_text("\n".join(output).rstrip() + "\n", encoding="utf-8") + missing_packages = sorted({package for _header, missing in removed for package in missing}) + if strict: + raise RuntimeError( + f"{rel(manifest)} is missing local registry inputs for target artifact dependencies: " + + ", ".join(missing_packages) + ) + pruned_text = prune_missing_feature_dependencies( + "\n".join(output).rstrip() + "\n", + set(missing_packages), + ) + manifest.write_text(pruned_text, encoding="utf-8") for header, missing in removed: result.add_skip( f"{rel(manifest)} pruned {header} because local registry inputs are missing {', '.join(missing)}" ) +def prune_missing_feature_dependencies(text: str, missing_package_names: set[str]) -> str: + if not missing_package_names: + return text + lines = text.splitlines() + output: list[str] = [] + in_features = False + index = 0 + while index < len(lines): + line = lines[index] + if re.match(r"^\[features\]$", line): + in_features = True + output.append(line) + index += 1 + continue + if line.startswith("[") and not line.startswith("[["): + in_features = False + output.append(line) + index += 1 + continue + if not in_features: + output.append(line) + index += 1 + continue + + match = re.match(r"^([A-Za-z0-9_-]+)\s*=", line) + if match is None: + output.append(line) + index += 1 + continue + feature_name = match.group(1) + block = [line] + index += 1 + bracket_depth = line.count("[") - line.count("]") + while bracket_depth > 0 and index < len(lines): + block.append(lines[index]) + bracket_depth += lines[index].count("[") - lines[index].count("]") + index += 1 + feature_text = "[features]\n" + "\n".join(block) + "\n" + try: + values = tomllib.loads(feature_text)["features"][feature_name] + except (KeyError, tomllib.TOMLDecodeError): + output.extend(block) + continue + if not isinstance(values, list) or not all(isinstance(value, str) for value in values): + output.extend(block) + continue + filtered = [ + value + for value in values + if not (value.startswith("dep:") and value.removeprefix("dep:") in missing_package_names) + ] + if filtered == values: + output.extend(block) + continue + output.append(f"{feature_name} = [{', '.join(json.dumps(value) for value in filtered)}]") + return "\n".join(output).rstrip() + "\n" + + def cargo_metadata_package_from_manifest(manifest: Path) -> dict[str, Any]: completed = run( [ @@ -1451,6 +1521,7 @@ def stage_cargo_source_crates( registry_root: Path, dry_run: bool, result: SurfaceResult, + strict: bool, ) -> list[Path]: output_dir = registry_root / "cargo-generated" / "source-crates" if dry_run: @@ -1483,6 +1554,7 @@ def stage_cargo_source_crates( oliphaunt_manifest, available_package_names, result, + strict=strict, ) generated.append(manual_cargo_package_source(oliphaunt_manifest, output_dir)) @@ -1493,6 +1565,7 @@ def stage_cargo_source_crates( wasix_manifest, available_package_names, result, + strict=strict, ) generated.append(manual_cargo_package_source(wasix_manifest, output_dir)) @@ -2472,7 +2545,7 @@ def publish_cargo(roots: list[Path], registry_root: Path, dry_run: bool, strict: release_asset_roots = stage_release_asset_cargo_packages(roots, registry_root, dry_run, result) if release_asset_roots: roots = [*roots, *release_asset_roots] - generated_roots = stage_cargo_source_crates(roots, registry_root, dry_run, result) + generated_roots = stage_cargo_source_crates(roots, registry_root, dry_run, result, strict) generated_roots.extend( package_native_extension_cargo_crates( roots, @@ -2519,7 +2592,10 @@ def publish_cargo(roots: list[Path], registry_root: Path, dry_run: bool, strict: raise continue if package.get("name") in LEGACY_WASIX_ARTIFACT_CRATES: - result.add_skip(f"ignored legacy WASIX artifact crate {crate_path.name}") + message = f"ignored legacy WASIX artifact crate {crate_path.name}" + result.add_skip(message) + if strict: + raise RuntimeError(message) continue target_name = f"{package['name']}-{package['version']}.crate" packages_by_target_name[target_name] = (crate_path, package) From 80ffd097e278da7394bee06f8c29b8ee08436ec3 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 21:26:24 +0000 Subject: [PATCH 123/137] fix: validate js explicit extension runtimes --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 7 + docs/maintainers/sdk-parity-policy.md | 8 +- src/sdks/js/README.md | 12 +- .../js/src/__tests__/asset-resolver.test.ts | 62 ++++++ .../js/src/__tests__/native-bindings.test.ts | 34 +++- .../js/src/__tests__/runtime-modes.test.ts | 65 ++++++ src/sdks/js/src/native/assets-deno.ts | 49 ++++- src/sdks/js/src/native/assets-node.ts | 180 +++++++---------- src/sdks/js/src/native/bun.ts | 8 +- src/sdks/js/src/native/deno.ts | 29 ++- src/sdks/js/src/native/extension-runtime.ts | 185 ++++++++++++++++++ src/sdks/js/src/native/node.ts | 8 +- src/sdks/js/src/runtime/broker.ts | 33 +++- src/sdks/js/tools/check-sdk.sh | 10 +- tools/policy/check-sdk-parity.sh | 18 +- tools/policy/sdk-manifest.toml | 2 +- tools/release/check_release_metadata.py | 25 +++ 17 files changed, 579 insertions(+), 156 deletions(-) create mode 100644 src/sdks/js/src/native/extension-runtime.ts diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 2eefcd90..067de469 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -917,3 +917,10 @@ until the current-state gates here are checked with fresh local evidence. legacy unsplit WASIX artifact crates. Non-strict local publishing still prunes unavailable target dependency tables, but now also removes matching optional `dep:` feature entries so generated source crates remain valid. +- On 2026-06-26, TypeScript native explicit `runtimeDirectory` handling was + aligned across Node, Bun, Deno, and nativeBroker. Package-managed Node/Bun + still materialize exact extension npm packages, but explicit runtime + overrides now validate selected extension control files, install SQL, data + files, and native modules before opening or launching. Deno keeps its + package-managed extension limitation, but explicit prepared runtimes are now + proven instead of merely accepted by path. diff --git a/docs/maintainers/sdk-parity-policy.md b/docs/maintainers/sdk-parity-policy.md index 6b43f884..b0334456 100644 --- a/docs/maintainers/sdk-parity-policy.md +++ b/docs/maintainers/sdk-parity-policy.md @@ -83,7 +83,7 @@ those overrides are not the consumer install path. | --- | --- | --- | --- | --- | | Rust | Cargo-resolved `liboliphaunt-native-*` artifact crates staged by `oliphaunt-build` | split `oliphaunt-tools-*` Cargo artifact crates copied into the runtime cache | exact `oliphaunt-extension-*` Cargo artifact crates | `OLIPHAUNT_RESOURCES_DIR` | | WASIX Rust | Cargo-resolved `liboliphaunt-wasix-portable`, `oliphaunt-icu`, and target AOT artifact crates | optional `oliphaunt-wasix-tools` plus target tools-AOT artifact crates behind the `tools` feature | exact `oliphaunt-extension-*-wasix` and extension AOT Cargo artifact crates selected by feature | `OLIPHAUNT_WASM_GENERATED_ASSETS_DIR` | -| TypeScript | npm optional platform packages such as `@oliphaunt/liboliphaunt-*` and `@oliphaunt/node-direct-*` | split `@oliphaunt/tools-*` npm packages | Node/Bun exact extension npm packages; Deno requires an explicit prepared `runtimeDirectory` for extension materialization | `libraryPath` and `runtimeDirectory` | +| TypeScript | npm optional platform packages such as `@oliphaunt/liboliphaunt-*` and `@oliphaunt/node-direct-*` | split `@oliphaunt/tools-*` npm packages | Node/Bun exact extension npm packages for package-managed installs; explicit prepared `runtimeDirectory` values are validated for selected extension files across Node/Bun/Deno | `libraryPath` and `runtimeDirectory` | | Swift | SwiftPM release assets and packaged runtime resources | not exposed in mobile native-direct mode | exact extension XCFramework artifacts selected by SQL extension name | `runtimeDirectory` or `resourceRoot` | | Kotlin | Maven runtime artifacts applied through the Android Gradle plugin | not exposed in Android native-direct mode | exact extension Maven artifacts selected by SQL extension name | `runtimeDirectory` or `resourceRoot` | | React Native | delegated SwiftPM and Maven platform SDK resolution | delegated to the platform SDK; no separate RN tool runtime | delegated exact extension artifacts through Swift/Kotlin integrations | `runtimeDirectory` or `resourceRoot` | @@ -167,8 +167,10 @@ table above: - Native runtime artifacts come from `@oliphaunt/liboliphaunt-*` optional npm packages, PostgreSQL client tools come from split `@oliphaunt/tools-*` optional npm packages, and Node/Bun extensions come from exact extension npm - packages. Deno requires an explicit prepared `runtimeDirectory` for extension - materialization. + packages. Explicit prepared `runtimeDirectory` values are validated for + selected extension files across Node/Bun/Deno before nativeDirect opens or + nativeBroker launches. Deno still requires an explicit prepared + `runtimeDirectory` for extension materialization. ### WASIX Rust Deltas diff --git a/src/sdks/js/README.md b/src/sdks/js/README.md index 4c37edf3..0914a668 100644 --- a/src/sdks/js/README.md +++ b/src/sdks/js/README.md @@ -77,11 +77,13 @@ pnpm add @oliphaunt/extension-hstore @oliphaunt/extension-pg-trgm At startup the Node and Bun bindings resolve the current platform package, validate that it was built for the same liboliphaunt version as `@oliphaunt/ts`, and materialize a runtime tree containing the selected -extension SQL files and native modules. Deno nativeDirect does not yet -materialize extension packages automatically; pass an explicit -`runtimeDirectory` that already contains the selected extension assets, or use -Node/Bun for registry-managed extension resolution. Deno nativeServer has the -same limitation for package-managed extension resolution; pass a prepared +extension SQL files and native modules. When `runtimeDirectory` is supplied +explicitly, Node, Bun, and Deno validate that the prepared runtime contains the +selected extension control files, install SQL, data files, and native modules +before opening. Deno nativeDirect does not yet materialize extension packages +automatically; pass an explicit prepared `runtimeDirectory`, or use Node/Bun +for registry-managed extension resolution. Deno nativeServer has the same +limitation for package-managed extension resolution; pass a prepared `serverToolDirectory` when server mode needs extension assets. Do not copy extension release assets into the application bundle by hand. diff --git a/src/sdks/js/src/__tests__/asset-resolver.test.ts b/src/sdks/js/src/__tests__/asset-resolver.test.ts index ab2f4048..a4a78889 100644 --- a/src/sdks/js/src/__tests__/asset-resolver.test.ts +++ b/src/sdks/js/src/__tests__/asset-resolver.test.ts @@ -9,10 +9,12 @@ import { test } from 'vitest'; import { resolvePackageRelativeUrl } from '../native/assets-deno.js'; import { materializeNodeExtensionInstall, + prepareNodeExtensionInstall, type ResolvedNativeInstall, resolveNodeIcuDataDirectory, resolveNodeNativeInstall, resolvePackageRelativePath, + validatePreparedNodeRuntimeExtensions, } from '../native/assets-node.js'; import { liboliphauntPackageTarget } from '../native/common.js'; import { extractTarArchive } from '../native/tar.js'; @@ -42,6 +44,7 @@ async function main(): Promise { await nodeResolverMergesPackageManagedRuntimeAndSplitTools(); await nodeIcuResolverAcceptsValidPortablePackage(); await nodeExtensionMaterializationValidatesSelections(); + await explicitRuntimeExtensionValidationUsesPreparedFiles(); await nodeExtensionMaterializationCopiesPackagePayloads(); await nodeExtensionMaterializationRejectsIncompletePackagePayloads(); await typeScriptPackageMetadataMatchesRuntimePackages(); @@ -281,6 +284,48 @@ async function nodeExtensionMaterializationValidatesSelections(): Promise ); } +async function explicitRuntimeExtensionValidationUsesPreparedFiles(): Promise { + const target = liboliphauntPackageTarget(platform(), arch()); + const root = await mkdtemp(join(tmpdir(), 'oliphaunt-js-explicit-runtime-')); + const directRuntime = join(root, 'runtime'); + const releaseRoot = join(root, 'release-shaped'); + const releaseRuntime = join(releaseRoot, 'oliphaunt/runtime/files'); + const invalidRuntime = join(root, 'invalid-runtime'); + const libraryPath = join(root, 'lib/liboliphaunt.so'); + try { + await writePreparedHstoreRuntime(directRuntime, target.id); + await writePreparedHstoreRuntime(releaseRuntime, target.id); + await mkdir(join(invalidRuntime, 'share/postgresql/extension'), { recursive: true }); + await mkdir(join(invalidRuntime, 'lib/postgresql'), { recursive: true }); + + const direct = await validatePreparedNodeRuntimeExtensions( + { libraryPath, runtimeDirectory: directRuntime }, + ['hstore'], + ); + assert.equal(direct.runtimeDirectory, directRuntime); + assert.equal(direct.moduleDirectory, join(directRuntime, 'lib/postgresql')); + + const releaseShaped = await prepareNodeExtensionInstall( + { libraryPath, runtimeDirectory: releaseRoot }, + ['hstore'], + { explicitRuntimeDirectory: true }, + ); + assert.equal(releaseShaped.runtimeDirectory, releaseRuntime); + assert.equal(releaseShaped.moduleDirectory, join(releaseRuntime, 'lib/postgresql')); + + await assert.rejects( + () => + validatePreparedNodeRuntimeExtensions( + { libraryPath, runtimeDirectory: invalidRuntime }, + ['hstore'], + ), + /explicit native runtimeDirectory is missing hstore.control/, + ); + } finally { + await rm(root, { recursive: true, force: true }); + } +} + async function nodeExtensionMaterializationCopiesPackagePayloads(): Promise { const target = liboliphauntPackageTarget(platform(), arch()); const basePackageName = '@oliphaunt/extension-hstore'; @@ -389,6 +434,23 @@ async function nodeExtensionMaterializationCopiesPackagePayloads(): Promise { + await mkdir(join(runtimeDirectory, 'share/postgresql/extension'), { recursive: true }); + await mkdir(join(runtimeDirectory, 'lib/postgresql'), { recursive: true }); + await writeFile( + join(runtimeDirectory, 'share/postgresql/extension/hstore.control'), + 'extension', + ); + await writeFile( + join(runtimeDirectory, 'share/postgresql/extension/hstore--1.0.sql'), + 'install', + ); + await writeFile( + join(runtimeDirectory, 'lib/postgresql', `hstore${nativeModuleSuffixForTarget(target)}`), + 'module', + ); +} + async function nodeExtensionMaterializationRejectsIncompletePackagePayloads(): Promise { const target = liboliphauntPackageTarget(platform(), arch()); const basePackageName = '@oliphaunt/extension-hstore'; diff --git a/src/sdks/js/src/__tests__/native-bindings.test.ts b/src/sdks/js/src/__tests__/native-bindings.test.ts index 022993b1..812b747c 100644 --- a/src/sdks/js/src/__tests__/native-bindings.test.ts +++ b/src/sdks/js/src/__tests__/native-bindings.test.ts @@ -353,18 +353,34 @@ async function testDenoNativeBindingRejectsPackageManagedExtensions(): Promise - binding.open({ - pgdata: '/tmp/deno-pgdata', - runtimeDirectory: undefined, - username: 'postgres', - database: 'postgres', - extensions: ['hstore'], - startupArgs: [], - }), + Promise.resolve( + binding.open({ + pgdata: '/tmp/deno-pgdata', + runtimeDirectory: undefined, + username: 'postgres', + database: 'postgres', + extensions: ['hstore'], + startupArgs: [], + }), + ), /Deno nativeDirect does not automatically materialize extension packages/, ); + await assert.rejects( + () => + Promise.resolve( + binding.open({ + pgdata: '/tmp/deno-pgdata', + runtimeDirectory: '/tmp/deno-prepared-runtime', + username: 'postgres', + database: 'postgres', + extensions: ['hstore'], + startupArgs: [], + }), + ), + /Deno nativeDirect explicit runtimeDirectory is missing hstore.control/, + ); assert.deepEqual(calls, ['dlopen:/tmp/liboliphaunt-deno-test.so']); } finally { if (previousDeno === undefined) { diff --git a/src/sdks/js/src/__tests__/runtime-modes.test.ts b/src/sdks/js/src/__tests__/runtime-modes.test.ts index 34b3fe0d..ead66a6e 100644 --- a/src/sdks/js/src/__tests__/runtime-modes.test.ts +++ b/src/sdks/js/src/__tests__/runtime-modes.test.ts @@ -37,6 +37,7 @@ async function main(): Promise { await testBrokerRestorePassesNativeInstallEnv(); await testBrokerStartupTimeoutEnvIsValidatedBeforeNativeInstall(); await testDenoBrokerModeRejectsPackageManagedExtensions(); + await testDenoBrokerModeValidatesExplicitExtensionRuntime(); testServerCapabilitiesAndConnectionString(); await testServerSupportReportsMissingExecutable(); await testServerSupportRequiresSplitClientTools(); @@ -205,6 +206,70 @@ async function testDenoBrokerModeRejectsPackageManagedExtensions(): Promise { + const root = await mkdtemp(join(tmpdir(), 'oliphaunt-js-deno-broker-prepared-runtime-')); + const executable = join(root, process.platform === 'win32' ? 'broker.cmd' : 'broker'); + const previousDeno = (globalThis as { Deno?: unknown }).Deno; + try { + await writeFile(executable, process.platform === 'win32' ? '@echo off\r\n' : '#!/bin/sh\n'); + await chmod(executable, 0o700); + (globalThis as { Deno?: unknown }).Deno = { + build: { os: 'linux', arch: 'x86_64' }, + async readTextFile(path: string | URL) { + const text = String(path); + if (text.includes('@oliphaunt/icu')) { + return JSON.stringify({ + name: '@oliphaunt/icu', + version: '0.1.0', + oliphaunt: { + product: 'oliphaunt-icu', + kind: 'icu-data', + target: 'portable', + dataRelativePath: 'share/icu', + }, + }); + } + return JSON.stringify({ + name: '@oliphaunt/ts', + oliphaunt: { + liboliphauntVersion: '0.1.0', + icuPackage: '@oliphaunt/icu', + icuVersion: '0.1.0', + }, + }); + }, + async stat() { + return { isDirectory: true }; + }, + async *readDir() { + yield { name: 'icudt76l.dat', isFile: true }; + }, + }; + const binding = createBrokerRuntimeBinding({ executable }); + await assert.rejects( + () => + Promise.resolve( + binding.open( + normalizedTestConfig(join(root, 'db'), { + engine: 'nativeBroker', + extensions: ['hstore'], + libraryPath: join(root, 'liboliphaunt.so'), + runtimeDirectory: join(root, 'prepared-runtime'), + }), + ), + ), + /Deno nativeBroker explicit runtimeDirectory is missing hstore.control/, + ); + } finally { + if (previousDeno === undefined) { + delete (globalThis as { Deno?: unknown }).Deno; + } else { + (globalThis as { Deno?: unknown }).Deno = previousDeno; + } + await rm(root, { recursive: true, force: true }); + } +} + function testServerCapabilitiesAndConnectionString(): void { const binding = createServerRuntimeBinding(); assert.equal(binding.runtime, 'node'); diff --git a/src/sdks/js/src/native/assets-deno.ts b/src/sdks/js/src/native/assets-deno.ts index bd929951..000a01a9 100644 --- a/src/sdks/js/src/native/assets-deno.ts +++ b/src/sdks/js/src/native/assets-deno.ts @@ -9,6 +9,10 @@ import { resolveExplicitLibraryPath, resolveExplicitRuntimeDirectory, } from './common.js'; +import { + type RuntimeFileHost, + validatePreparedRuntimeExtensions, +} from './extension-runtime.js'; export type ResolvedDenoNativeInstall = { libraryPath: string; @@ -17,7 +21,7 @@ export type ResolvedDenoNativeInstall = { packageManaged: boolean; }; -type DenoRuntime = { +export type DenoRuntime = { build: { os: string; arch: string }; env?: { get(name: string): string | undefined }; readTextFile(path: string | URL): Promise; @@ -110,6 +114,22 @@ export async function resolveDenoNativeInstall( return resolvePackageNativeInstall(deno, target, versions.liboliphauntVersion, icuDataDirectory); } +export async function validatePreparedDenoRuntimeExtensions(config: { + deno: DenoRuntime; + runtimeDirectory?: string; + extensions: ReadonlyArray; + source: string; +}): Promise<{ runtimeDirectory: string; moduleDirectory?: string }> { + const target = liboliphauntPackageTarget(config.deno.build.os, config.deno.build.arch); + return validatePreparedRuntimeExtensions({ + runtimeDirectory: config.runtimeDirectory, + extensions: config.extensions, + target: target.id, + source: config.source, + host: denoRuntimeFileHost(config.deno), + }); +} + async function packageVersions(deno: DenoRuntime): Promise<{ liboliphauntVersion: string; icuPackage: string; @@ -679,3 +699,30 @@ function optionalDenoRuntime(): DenoRuntime | undefined { const deno = (globalThis as { Deno?: DenoRuntime }).Deno; return deno; } + +function denoRuntimeFileHost(deno: DenoRuntime): RuntimeFileHost { + return { + join, + async readDir(path: string) { + const entries: Array<{ name: string; isFile?: boolean }> = []; + for await (const entry of deno.readDir(path)) { + entries.push({ name: entry.name, isFile: entry.isFile }); + } + return entries; + }, + async isDirectory(path: string) { + try { + return (await deno.stat(path)).isDirectory === true; + } catch { + return false; + } + }, + async isFile(path: string) { + try { + return (await deno.stat(path)).isFile === true; + } catch { + return false; + } + }, + }; +} diff --git a/src/sdks/js/src/native/assets-node.ts b/src/sdks/js/src/native/assets-node.ts index 4da3d558..2239f872 100644 --- a/src/sdks/js/src/native/assets-node.ts +++ b/src/sdks/js/src/native/assets-node.ts @@ -5,8 +5,8 @@ import { arch, platform, tmpdir } from 'node:os'; import { dirname, isAbsolute, join, relative, resolve } from 'node:path'; import { setTimeout as delay } from 'node:timers/promises'; import { - type GeneratedExtensionMetadata, generatedExtensionBySqlName, + type GeneratedExtensionMetadata, } from '../generated/extensions.js'; import { liboliphauntPackageTarget, @@ -14,12 +14,20 @@ import { resolveExplicitLibraryPath, resolveExplicitRuntimeDirectory, } from './common.js'; +import { + nativeModuleSuffixForTarget, + requireExtensionRuntimePayload, + selectedExtensionClosure, + type RuntimeFileHost, + validatePreparedRuntimeExtensions, +} from './extension-runtime.js'; export type ResolvedNativeInstall = { libraryPath: string; runtimeDirectory?: string; icuDataDirectory?: string; moduleDirectory?: string; + packageManaged?: boolean; }; type PackageMetadata = { @@ -98,6 +106,7 @@ export async function resolveNodeNativeInstall( libraryPath: explicit, runtimeDirectory: resolveExplicitRuntimeDirectory(), icuDataDirectory, + packageManaged: false, }; } @@ -105,6 +114,36 @@ export async function resolveNodeNativeInstall( return resolvePackageNativeInstall(target, versions.liboliphauntVersion, icuDataDirectory); } +export async function prepareNodeExtensionInstall( + install: ResolvedNativeInstall, + extensions: ReadonlyArray = [], + options: { explicitRuntimeDirectory?: boolean } = {}, +): Promise { + if (options.explicitRuntimeDirectory === true && extensions.length > 0) { + return validatePreparedNodeRuntimeExtensions(install, extensions); + } + return materializeNodeExtensionInstall(install, extensions); +} + +export async function validatePreparedNodeRuntimeExtensions( + install: ResolvedNativeInstall, + extensions: ReadonlyArray = [], +): Promise { + const target = liboliphauntPackageTarget(platform(), arch()); + const validated = await validatePreparedRuntimeExtensions({ + runtimeDirectory: install.runtimeDirectory, + extensions, + target: target.id, + source: 'explicit native runtimeDirectory', + host: nodeRuntimeFileHost, + }); + return { + ...install, + runtimeDirectory: validated.runtimeDirectory, + moduleDirectory: validated.moduleDirectory, + }; +} + export async function materializeNodeExtensionInstall( install: ResolvedNativeInstall, extensions: ReadonlyArray = [], @@ -434,84 +473,15 @@ async function requireExtensionPackagePayload(config: { runtimeDirectories: readonly string[]; moduleDirectories: readonly string[]; }): Promise { - if (config.extension.createsExtension) { - const entries = await extensionSqlDirectoryEntries(config.runtimeDirectories); - const hasControl = entries.includes(`${config.extension.sqlName}.control`); - if (!hasControl) { - throw new Error( - `${config.source} extension runtime payload is missing ${config.extension.sqlName}.control`, - ); - } - const hasInstallSql = entries.some( - (entry) => entry.endsWith('.sql') && extensionSqlFileBelongs(config.extension, entry), - ); - if (!hasInstallSql) { - throw new Error( - `${config.source} extension runtime payload is missing SQL install files for ${config.extension.sqlName}`, - ); - } - } - - for (const dataFile of config.extension.dataFiles) { - await requireFileInAnyRoot( - config.runtimeDirectories, - dataFile, - `${config.source} extension runtime payload`, - ); - } - - if (config.extension.nativeModuleStem !== null) { - const moduleFile = `${config.extension.nativeModuleStem}${nativeModuleSuffixForTarget(config.target)}`; - await requireFileInAnyRoot( - config.moduleDirectories, - moduleFile, - `${config.source} extension module payload`, - ); - } -} - -async function extensionSqlDirectoryEntries( - runtimeDirectories: readonly string[], -): Promise { - const entries: string[] = []; - for (const runtimeDirectory of runtimeDirectories) { - const extensionDirectory = join(runtimeDirectory, 'share/postgresql/extension'); - if (!(await isDirectory(extensionDirectory))) { - continue; - } - for (const entry of await readdir(extensionDirectory, { withFileTypes: true })) { - if (entry.isFile()) { - entries.push(entry.name); - } - } - } - return entries; -} - -function extensionSqlFileBelongs(extension: GeneratedExtensionMetadata, fileName: string): boolean { - return ( - fileName === `${extension.sqlName}.control` || - fileName === `${extension.sqlName}.sql` || - (fileName.startsWith(`${extension.sqlName}--`) && fileName.endsWith('.sql')) || - extension.extensionSqlFileNames.includes(fileName) || - extension.extensionSqlFilePrefixes.some((prefix) => fileName.startsWith(prefix)) - ); -} - -async function requireFileInAnyRoot( - roots: readonly string[], - relativePath: string, - source: string, -): Promise { - for (const root of roots) { - const path = join(root, relativePath); - try { - if ((await stat(path)).isFile()) { - return; - } - } catch {} - } - throw new Error(`${source} is missing required file ${relativePath}`); + await requireExtensionRuntimePayload({ + extension: config.extension, + target: config.target, + runtimeDirectories: config.runtimeDirectories, + moduleDirectories: config.moduleDirectories, + runtimeSource: `${config.source} extension runtime payload`, + moduleSource: `${config.source} extension module payload`, + host: nodeRuntimeFileHost, + }); } async function resolvePackageNativeInstall( @@ -566,7 +536,7 @@ async function resolvePackageNativeInstall( }, toolsPackage: tools, }); - return { libraryPath, runtimeDirectory: mergedRuntimeDirectory, icuDataDirectory }; + return { libraryPath, runtimeDirectory: mergedRuntimeDirectory, icuDataDirectory, packageManaged: true }; } async function resolveNativeToolsPackage( @@ -920,26 +890,6 @@ function extensionTargetPackageName(sqlName: string, target: string): string { return `${extensionPackageName(sqlName)}-${target}`; } -function selectedExtensionClosure(extensions: ReadonlyArray): string[] { - const seen = new Set(); - const queue = [...extensions]; - while (queue.length > 0) { - const sqlName = queue.shift(); - if (sqlName === undefined || seen.has(sqlName)) { - continue; - } - seen.add(sqlName); - const metadata = generatedExtensionBySqlName(sqlName); - if (metadata === undefined) { - throw new Error(`unknown Oliphaunt extension id '${sqlName}'`); - } - for (const dependency of metadata.selectedExtensionDependencies) { - queue.push(dependency); - } - } - return [...seen].sort(); -} - function nativeModuleDirectoryCandidates(libraryPath: string): string[] { const libraryDir = dirname(libraryPath); return [join(libraryDir, 'modules'), join(dirname(libraryDir), 'lib', 'modules')]; @@ -955,16 +905,26 @@ function nativeClientToolsForTarget(target: string): string[] { return target === 'windows-x64-msvc' ? ['pg_dump.exe', 'psql.exe'] : ['pg_dump', 'psql']; } -function nativeModuleSuffixForTarget(target: string): string { - if (target.startsWith('macos-')) { - return '.dylib'; - } - if (target === 'windows-x64-msvc') { - return '.dll'; - } - return '.so'; -} - function runtimeCacheKey(value: unknown): string { return createHash('sha256').update(JSON.stringify(value)).digest('hex').slice(0, 32); } + +const nodeRuntimeFileHost: RuntimeFileHost = { + join, + async readDir(path: string) { + return (await readdir(path, { withFileTypes: true })).map((entry) => ({ + name: entry.name, + isFile: entry.isFile(), + })); + }, + async isDirectory(path: string) { + return isDirectory(path); + }, + async isFile(path: string) { + try { + return (await stat(path)).isFile(); + } catch { + return false; + } + }, +}; diff --git a/src/sdks/js/src/native/bun.ts b/src/sdks/js/src/native/bun.ts index 411d7f54..09c15c67 100644 --- a/src/sdks/js/src/native/bun.ts +++ b/src/sdks/js/src/native/bun.ts @@ -5,7 +5,7 @@ import { errorMessage, nativeBackupFormat, } from './common.js'; -import { materializeNodeExtensionInstall, resolveNodeNativeInstall } from './assets-node.js'; +import { prepareNodeExtensionInstall, resolveNodeNativeInstall } from './assets-node.js'; import type { BackupFormat } from '../types.js'; import { packConfigPointers, @@ -56,12 +56,16 @@ export async function createBunNativeBinding( return BigInt(symbols.oliphaunt_capabilities() as number | bigint); }, async open(config: NativeOpenConfig): Promise { - const extensionInstall = await materializeNodeExtensionInstall( + const extensionInstall = await prepareNodeExtensionInstall( { ...install, runtimeDirectory: config.runtimeDirectory ?? install.runtimeDirectory, }, config.extensions, + { + explicitRuntimeDirectory: + config.runtimeDirectory !== undefined || install.packageManaged === false, + }, ); applyNativeModuleEnvironment(extensionInstall.moduleDirectory); const packed = packConfigPointers( diff --git a/src/sdks/js/src/native/deno.ts b/src/sdks/js/src/native/deno.ts index 48accf37..4a07401f 100644 --- a/src/sdks/js/src/native/deno.ts +++ b/src/sdks/js/src/native/deno.ts @@ -1,10 +1,11 @@ import { applyNativeIcuDataEnvironment, + applyNativeModuleEnvironment, assertSupportedDirectBackupFormat, errorMessage, nativeBackupFormat, } from './common.js'; -import { resolveDenoNativeInstall } from './assets-deno.js'; +import { resolveDenoNativeInstall, validatePreparedDenoRuntimeExtensions } from './assets-deno.js'; import type { BackupFormat } from '../types.js'; import { packConfigPointers, @@ -74,17 +75,31 @@ export async function createDenoNativeBinding( capabilities(): bigint { return BigInt(symbols.oliphaunt_capabilities() as bigint | number); }, - open(config: NativeOpenConfig): NativeHandle { + async open(config: NativeOpenConfig): Promise { + let openConfig = { + ...config, + runtimeDirectory: config.runtimeDirectory ?? install.runtimeDirectory, + }; if ( - config.extensions.length > 0 && - (config.runtimeDirectory === undefined || - (install.packageManaged && config.runtimeDirectory === install.runtimeDirectory)) + openConfig.extensions.length > 0 && + (openConfig.runtimeDirectory === undefined || + (install.packageManaged && openConfig.runtimeDirectory === install.runtimeDirectory)) ) { throw new Error( - `Deno nativeDirect does not automatically materialize extension packages; pass runtimeDirectory with the selected extension assets or use Node/Bun nativeDirect. Selected extensions: ${config.extensions.join(', ')}`, + `Deno nativeDirect does not automatically materialize extension packages; pass runtimeDirectory with the selected extension assets or use Node/Bun nativeDirect. Selected extensions: ${openConfig.extensions.join(', ')}`, ); } - const packed = packConfigPointers(config, (value) => pointerOf(deno, value)); + if (openConfig.extensions.length > 0) { + const validated = await validatePreparedDenoRuntimeExtensions({ + deno, + runtimeDirectory: openConfig.runtimeDirectory, + extensions: openConfig.extensions, + source: 'Deno nativeDirect explicit runtimeDirectory', + }); + openConfig = { ...openConfig, runtimeDirectory: validated.runtimeDirectory }; + applyNativeModuleEnvironment(validated.moduleDirectory); + } + const packed = packConfigPointers(openConfig, (value) => pointerOf(deno, value)); const out = new Uint8Array(8); const rc = symbols.oliphaunt_init(packed.config, out) as number; keepAlive(packed.keepAlive); diff --git a/src/sdks/js/src/native/extension-runtime.ts b/src/sdks/js/src/native/extension-runtime.ts new file mode 100644 index 00000000..086ad2c2 --- /dev/null +++ b/src/sdks/js/src/native/extension-runtime.ts @@ -0,0 +1,185 @@ +import { + type GeneratedExtensionMetadata, + generatedExtensionBySqlName, +} from '../generated/extensions.js'; + +export type RuntimeFileHost = { + join(...parts: string[]): string; + readDir(path: string): Promise>; + isDirectory(path: string): Promise; + isFile(path: string): Promise; +}; + +export type PreparedRuntimeExtensions = { + runtimeDirectory: string; + moduleDirectory?: string; +}; + +export async function validatePreparedRuntimeExtensions(config: { + runtimeDirectory?: string; + extensions: ReadonlyArray; + target: string; + source: string; + host: RuntimeFileHost; +}): Promise { + const selected = selectedExtensionClosure(config.extensions); + if (selected.length === 0) { + return { runtimeDirectory: config.runtimeDirectory ?? '' }; + } + if (config.runtimeDirectory === undefined) { + throw new Error( + `${config.source} requires runtimeDirectory with selected extension assets: ${selected.join(', ')}`, + ); + } + + const runtimeDirectory = await preparedRuntimeDirectory(config.runtimeDirectory, config.host); + const moduleDirectory = config.host.join(runtimeDirectory, 'lib/postgresql'); + for (const sqlName of selected) { + const extension = generatedExtensionBySqlName(sqlName); + if (extension === undefined) { + throw new Error(`unknown Oliphaunt extension id '${sqlName}'`); + } + await requireExtensionRuntimePayload({ + extension, + target: config.target, + runtimeDirectories: [runtimeDirectory], + moduleDirectories: [moduleDirectory], + runtimeSource: config.source, + moduleSource: `${config.source} module directory`, + host: config.host, + }); + } + + return { runtimeDirectory, moduleDirectory }; +} + +export async function requireExtensionRuntimePayload(config: { + extension: GeneratedExtensionMetadata; + target: string; + runtimeDirectories: readonly string[]; + moduleDirectories: readonly string[]; + runtimeSource: string; + moduleSource: string; + host: RuntimeFileHost; +}): Promise { + if (config.extension.createsExtension) { + const entries = await extensionSqlDirectoryEntries(config.runtimeDirectories, config.host); + const hasControl = entries.includes(`${config.extension.sqlName}.control`); + if (!hasControl) { + throw new Error(`${config.runtimeSource} is missing ${config.extension.sqlName}.control`); + } + const hasInstallSql = entries.some( + (entry) => entry.endsWith('.sql') && extensionSqlFileBelongs(config.extension, entry), + ); + if (!hasInstallSql) { + throw new Error( + `${config.runtimeSource} is missing SQL install files for ${config.extension.sqlName}`, + ); + } + } + + for (const dataFile of config.extension.dataFiles) { + await requireFileInAnyRoot( + config.runtimeDirectories, + dataFile, + config.runtimeSource, + config.host, + ); + } + + if (config.extension.nativeModuleStem !== null) { + const moduleFile = `${config.extension.nativeModuleStem}${nativeModuleSuffixForTarget( + config.target, + )}`; + await requireFileInAnyRoot( + config.moduleDirectories, + moduleFile, + config.moduleSource, + config.host, + ); + } +} + +export function selectedExtensionClosure(extensions: ReadonlyArray): string[] { + const seen = new Set(); + const queue = [...extensions]; + while (queue.length > 0) { + const sqlName = queue.shift(); + if (sqlName === undefined || seen.has(sqlName)) { + continue; + } + seen.add(sqlName); + const metadata = generatedExtensionBySqlName(sqlName); + if (metadata === undefined) { + throw new Error(`unknown Oliphaunt extension id '${sqlName}'`); + } + for (const dependency of metadata.selectedExtensionDependencies) { + queue.push(dependency); + } + } + return [...seen].sort(); +} + +export function nativeModuleSuffixForTarget(target: string): string { + if (target.startsWith('macos-')) { + return '.dylib'; + } + if (target === 'windows-x64-msvc') { + return '.dll'; + } + return '.so'; +} + +async function preparedRuntimeDirectory( + runtimeDirectory: string, + host: RuntimeFileHost, +): Promise { + const releaseShapedRuntime = host.join(runtimeDirectory, 'oliphaunt/runtime/files'); + if (await host.isDirectory(releaseShapedRuntime)) { + return releaseShapedRuntime; + } + return runtimeDirectory; +} + +async function extensionSqlDirectoryEntries( + runtimeDirectories: readonly string[], + host: RuntimeFileHost, +): Promise { + const entries: string[] = []; + for (const runtimeDirectory of runtimeDirectories) { + const extensionDirectory = host.join(runtimeDirectory, 'share/postgresql/extension'); + if (!(await host.isDirectory(extensionDirectory))) { + continue; + } + for (const entry of await host.readDir(extensionDirectory)) { + if (entry.isFile !== false) { + entries.push(entry.name); + } + } + } + return entries; +} + +function extensionSqlFileBelongs(extension: GeneratedExtensionMetadata, fileName: string): boolean { + return ( + fileName === `${extension.sqlName}.control` || + fileName === `${extension.sqlName}.sql` || + (fileName.startsWith(`${extension.sqlName}--`) && fileName.endsWith('.sql')) || + extension.extensionSqlFileNames.includes(fileName) || + extension.extensionSqlFilePrefixes.some((prefix) => fileName.startsWith(prefix)) + ); +} + +async function requireFileInAnyRoot( + roots: readonly string[], + relativePath: string, + source: string, + host: RuntimeFileHost, +): Promise { + for (const root of roots) { + if (await host.isFile(host.join(root, relativePath))) { + return; + } + } + throw new Error(`${source} is missing required file ${relativePath}`); +} diff --git a/src/sdks/js/src/native/node.ts b/src/sdks/js/src/native/node.ts index 4cfc13f8..c92b42b5 100644 --- a/src/sdks/js/src/native/node.ts +++ b/src/sdks/js/src/native/node.ts @@ -5,7 +5,7 @@ import { nativeBackupFormat, } from './common.js'; import { loadNodeDirectAddon } from './node-addon.js'; -import { materializeNodeExtensionInstall, resolveNodeNativeInstall } from './assets-node.js'; +import { prepareNodeExtensionInstall, resolveNodeNativeInstall } from './assets-node.js'; import type { BackupFormat } from '../types.js'; import type { NativeBinding, @@ -34,12 +34,16 @@ export async function createNodeNativeBinding( return BigInt(addon.capabilities(install.libraryPath)); }, async open(config: NativeOpenConfig): Promise { - const extensionInstall = await materializeNodeExtensionInstall( + const extensionInstall = await prepareNodeExtensionInstall( { ...install, runtimeDirectory: config.runtimeDirectory ?? install.runtimeDirectory, }, config.extensions, + { + explicitRuntimeDirectory: + config.runtimeDirectory !== undefined || install.packageManaged === false, + }, ); applyNativeModuleEnvironment(extensionInstall.moduleDirectory); return addon.open({ diff --git a/src/sdks/js/src/runtime/broker.ts b/src/sdks/js/src/runtime/broker.ts index a6fddf76..cd77c7c1 100644 --- a/src/sdks/js/src/runtime/broker.ts +++ b/src/sdks/js/src/runtime/broker.ts @@ -6,6 +6,7 @@ import { arch, platform } from 'node:os'; import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'; import type { NormalizedOpenConfig } from '../config.js'; +import type { DenoRuntime } from '../native/assets-deno.js'; import type { BackupFormat, EngineCapabilities, EngineModeSupport } from '../types.js'; import { ICU_DATA_ENV, @@ -409,27 +410,41 @@ async function resolveBrokerNativeInstall(config: { }): Promise { const extensions = config.extensions ?? []; if (runtimeName() === 'deno') { - if (extensions.length > 0 && config.runtimeDirectory === undefined) { + if ( + extensions.length > 0 && + config.runtimeDirectory === undefined && + envVar(LIBOLIPHAUNT_RUNTIME_DIR_ENV) === undefined + ) { throw new Error( `Deno nativeBroker does not automatically materialize extension packages; pass runtimeDirectory with the selected extension assets or use Node/Bun nativeBroker. Selected extensions: ${extensions.join(', ')}`, ); } - const install = await import('../native/assets-deno.js').then((module) => - module.resolveDenoNativeInstall(config.libraryPath), - ); + const assets = await import('../native/assets-deno.js'); + const deno = (globalThis as { Deno?: unknown }).Deno; + const install = await assets.resolveDenoNativeInstall(config.libraryPath); + const runtimeDirectory = config.runtimeDirectory ?? install.runtimeDirectory; if ( extensions.length > 0 && - install.packageManaged && - config.runtimeDirectory === install.runtimeDirectory + (runtimeDirectory === undefined || (install.packageManaged && config.runtimeDirectory === undefined)) ) { throw new Error( `Deno nativeBroker does not automatically materialize extension packages; pass runtimeDirectory with the selected extension assets or use Node/Bun nativeBroker. Selected extensions: ${extensions.join(', ')}`, ); } + const validated = + extensions.length === 0 + ? { runtimeDirectory, moduleDirectory: undefined } + : await assets.validatePreparedDenoRuntimeExtensions({ + deno: deno as DenoRuntime, + runtimeDirectory, + extensions, + source: 'Deno nativeBroker explicit runtimeDirectory', + }); return { libraryPath: install.libraryPath, - runtimeDirectory: config.runtimeDirectory ?? install.runtimeDirectory, + runtimeDirectory: validated.runtimeDirectory, icuDataDirectory: install.icuDataDirectory, + moduleDirectory: validated.moduleDirectory, }; } @@ -440,7 +455,9 @@ async function resolveBrokerNativeInstall(config: { runtimeDirectory: config.runtimeDirectory ?? install.runtimeDirectory, icuDataDirectory: install.icuDataDirectory, }; - return assets.materializeNodeExtensionInstall(resolved, extensions); + return assets.prepareNodeExtensionInstall(resolved, extensions, { + explicitRuntimeDirectory: config.runtimeDirectory !== undefined || install.packageManaged === false, + }); } function brokerSpawnEnv( diff --git a/src/sdks/js/tools/check-sdk.sh b/src/sdks/js/tools/check-sdk.sh index cdf64ab0..59c70423 100755 --- a/src/sdks/js/tools/check-sdk.sh +++ b/src/sdks/js/tools/check-sdk.sh @@ -410,6 +410,12 @@ require_source_text "$package_dir/src/native/assets-deno.ts" "deno.rename" \ "TypeScript Deno native binding must install finished runtime caches with runtime-owned rename" require_source_text "$package_dir/src/native/deno.ts" "install.packageManaged" \ "TypeScript Deno nativeDirect must reject registry-managed extension materialization until it has a dedicated resolver" +require_source_text "$package_dir/src/native/extension-runtime.ts" "validatePreparedRuntimeExtensions" \ + "TypeScript native bindings must share prepared runtimeDirectory extension validation" +require_source_text "$package_dir/src/native/assets-deno.ts" "validatePreparedDenoRuntimeExtensions" \ + "TypeScript Deno native binding must validate explicit prepared runtimeDirectory extension files" +require_source_text "$package_dir/src/runtime/broker.ts" "Deno nativeBroker explicit runtimeDirectory" \ + "TypeScript Deno nativeBroker must validate explicit prepared runtimeDirectory extension files" require_source_text "$package_dir/src/runtime/server.ts" "resolveDenoNativeInstall" \ "TypeScript Deno nativeServer must resolve package-managed server tools through the Deno native resolver" require_source_text "$package_dir/src/runtime/server.ts" "Deno nativeServer does not automatically materialize extension packages" \ @@ -436,8 +442,8 @@ require_source_text "$package_dir/src/config.ts" "generatedExtensionBySqlName(tr "TypeScript SDK must validate selected extensions against the generated extension catalog" require_source_text "$package_dir/src/config.ts" "unknown Oliphaunt extension id" \ "TypeScript SDK must fail clearly for unknown selected extensions" -require_source_text "$package_dir/src/native/assets-node.ts" "metadata.selectedExtensionDependencies" \ - "TypeScript Node/Bun native extension materialization must use generated package-materialization dependencies" +require_source_text "$package_dir/src/native/extension-runtime.ts" "metadata.selectedExtensionDependencies" \ + "TypeScript native extension materialization must use generated package-materialization dependencies" require_source_text "$package_dir/src/types.ts" "backupFormats: BackupFormat[]" \ "TypeScript SDK capabilities must expose backup formats" require_source_text "$package_dir/src/types.ts" "restoreFormats: BackupFormat[]" \ diff --git a/tools/policy/check-sdk-parity.sh b/tools/policy/check-sdk-parity.sh index ad48bb9f..a59fb5f8 100755 --- a/tools/policy/check-sdk-parity.sh +++ b/tools/policy/check-sdk-parity.sh @@ -236,8 +236,8 @@ require_manifest_text typescript 'artifact_resolution = "npm-optional-platform-p "SDK manifest must declare TypeScript npm optional platform package resolution" require_manifest_text typescript 'tool_resolution = "split-oliphaunt-tools-npm-packages"' \ "SDK manifest must declare TypeScript split oliphaunt-tools npm resolution" -require_manifest_text typescript 'extension_resolution = "node-bun-exact-extension-npm-packages-deno-explicit-runtimeDirectory"' \ - "SDK manifest must declare TypeScript Node/Bun registry extension resolution and Deno's explicit-runtimeDirectory gap" +require_manifest_text typescript 'extension_resolution = "node-bun-exact-extension-npm-packages-prepared-runtimeDirectory-validation"' \ + "SDK manifest must declare TypeScript registry extension resolution plus prepared runtimeDirectory validation" require_manifest_text typescript 'resource_override = "libraryPath-runtimeDirectory"' \ "SDK manifest must declare TypeScript's explicit local native override paths" require_text src/sdks/js/src/native/assets-deno.ts "target.toolsPackageName" \ @@ -260,6 +260,12 @@ require_text src/sdks/js/src/native/assets-deno.ts "deno.rename" \ "TypeScript Deno native resolver must install finished runtime caches with runtime-owned rename" require_text src/sdks/js/src/native/deno.ts "install.packageManaged" \ "TypeScript Deno nativeDirect must keep registry-managed extension materialization explicitly unsupported" +require_text src/sdks/js/src/native/extension-runtime.ts "validatePreparedRuntimeExtensions" \ + "TypeScript native bindings must share prepared runtimeDirectory extension validation" +require_text src/sdks/js/src/native/assets-deno.ts "validatePreparedDenoRuntimeExtensions" \ + "TypeScript Deno native resolver must validate explicit prepared runtimeDirectory extension files" +require_text src/sdks/js/src/runtime/broker.ts "Deno nativeBroker explicit runtimeDirectory" \ + "TypeScript Deno nativeBroker must validate explicit prepared runtimeDirectory extension files" require_text src/sdks/js/src/runtime/server.ts "resolveDenoNativeInstall" \ "TypeScript Deno nativeServer must resolve package-managed server tools through the Deno native resolver" require_text src/sdks/js/src/runtime/server.ts "Deno nativeServer does not automatically materialize extension packages" \ @@ -276,8 +282,8 @@ require_text src/sdks/js/src/generated/extensions.ts "extensionSqlFilePrefixes" "TypeScript generated extension metadata must expose noncanonical extension SQL file prefixes for package validation" require_text src/sdks/js/src/native/assets-node.ts "requireExtensionPackagePayload" \ "TypeScript Node/Bun exact-extension resolver must validate complete extension payload files before materialization" -require_text src/sdks/js/src/native/assets-node.ts "missing SQL install files" \ - "TypeScript Node/Bun exact-extension resolver must reject payloads missing selected extension install SQL" +require_text src/sdks/js/src/native/extension-runtime.ts "missing SQL install files" \ + "TypeScript exact-extension resolver must reject payloads missing selected extension install SQL" require_text src/sdks/js/src/__tests__/asset-resolver.test.ts "nodeExtensionMaterializationRejectsIncompletePackagePayloads" \ "TypeScript asset resolver tests must cover incomplete exact-extension payload rejection" require_text docs/maintainers/sdk-products-policy.md "These are product SDKs, not auxiliary bindings." \ @@ -386,8 +392,8 @@ require_text docs/maintainers/sdk-parity-policy.md "split \`@oliphaunt/tools-*\` "SDK parity docs must describe TypeScript split tools npm resolution" require_text docs/maintainers/sdk-parity-policy.md "\`libraryPath\` and \`runtimeDirectory\`" \ "SDK parity docs must document TypeScript's explicit local native override paths" -require_text docs/maintainers/sdk-parity-policy.md "Deno requires an explicit prepared \`runtimeDirectory\` for extension materialization" \ - "SDK parity docs must document the Deno extension-resolution deviation" +require_text docs/maintainers/sdk-parity-policy.md "explicit prepared \`runtimeDirectory\` values are validated for selected extension files" \ + "SDK parity docs must document TypeScript prepared runtimeDirectory extension validation" require_text docs/maintainers/sdk-parity-policy.md "\`runtimeDirectory\` or \`resourceRoot\`" \ "SDK parity docs must document mobile SDK explicit local runtime-resource overrides" require_text docs/maintainers/sdk-parity-policy.md "### Desktop TypeScript Deltas" \ diff --git a/tools/policy/sdk-manifest.toml b/tools/policy/sdk-manifest.toml index 8878e0e0..a05eb51c 100644 --- a/tools/policy/sdk-manifest.toml +++ b/tools/policy/sdk-manifest.toml @@ -108,5 +108,5 @@ depends_on_rust_broker_helper = true broker_helper_product = "oliphaunt-rust" artifact_resolution = "npm-optional-platform-packages" tool_resolution = "split-oliphaunt-tools-npm-packages" -extension_resolution = "node-bun-exact-extension-npm-packages-deno-explicit-runtimeDirectory" +extension_resolution = "node-bun-exact-extension-npm-packages-prepared-runtimeDirectory-validation" resource_override = "libraryPath-runtimeDirectory" diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index a7b0a7d2..c40038c6 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1285,11 +1285,36 @@ def validate_typescript( "Deno nativeDirect does not automatically materialize extension packages", "TypeScript Deno native binding must fail clearly for package-managed extension materialization", ) + require_text( + "src/sdks/js/src/native/extension-runtime.ts", + "validatePreparedRuntimeExtensions", + "TypeScript native bindings must share explicit runtimeDirectory extension-file validation", + ) + require_text( + "src/sdks/js/src/native/assets-deno.ts", + "validatePreparedDenoRuntimeExtensions", + "TypeScript Deno native binding must validate explicit prepared runtimeDirectory extension files", + ) require_text( "src/sdks/js/src/__tests__/native-bindings.test.ts", "testDenoNativeBindingRejectsPackageManagedExtensions", "TypeScript SDK tests must cover Deno package-managed extension rejection", ) + require_text( + "src/sdks/js/src/__tests__/native-bindings.test.ts", + "Deno nativeDirect explicit runtimeDirectory", + "TypeScript SDK tests must reject Deno explicit runtimeDirectory extensions missing prepared files", + ) + require_text( + "src/sdks/js/src/__tests__/asset-resolver.test.ts", + "explicitRuntimeExtensionValidationUsesPreparedFiles", + "TypeScript asset resolver tests must cover explicit prepared runtimeDirectory extension validation", + ) + require_text( + "src/sdks/js/src/__tests__/runtime-modes.test.ts", + "testDenoBrokerModeValidatesExplicitExtensionRuntime", + "TypeScript broker tests must cover Deno explicit prepared runtimeDirectory extension validation", + ) require_text( "src/sdks/js/src/runtime/broker.ts", "restorePhysicalArchiveWithBroker", From 466a4faa9d2173ac261b1104bad472201f95eecb Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 21:41:34 +0000 Subject: [PATCH 124/137] chore: port swiftpm tag publisher to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 15 ++ docs/maintainers/release-setup.md | 2 +- .../fixtures/consumer-shape/products.json | 4 +- tools/policy/python-entrypoints.allowlist | 1 - tools/release/check_release_metadata.py | 4 +- tools/release/product-version.mjs | 6 +- tools/release/publish_swiftpm_source_tag.mjs | 235 +++++++++++++++++ tools/release/publish_swiftpm_source_tag.py | 242 ------------------ tools/release/release.py | 3 +- 9 files changed, 261 insertions(+), 251 deletions(-) create mode 100644 tools/release/publish_swiftpm_source_tag.mjs delete mode 100755 tools/release/publish_swiftpm_source_tag.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 067de469..1e7a8f26 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -233,6 +233,21 @@ until the current-state gates here are checked with fresh local evidence. removed from `tools/policy/python-entrypoints.allowlist`, and `check-tooling-stack.sh` now rejects stale references to the retired checker path. +- 2026-06-26: SwiftPM source-tag publishing now runs through + `tools/release/publish_swiftpm_source_tag.mjs` and the pinned Bun launcher + instead of the retired Python entrypoint. The reusable + `tools/release/product-version.mjs` helper now exports `currentVersion()` for + release helpers while preserving its CLI. Fresh checks passed: + `tools/dev/bun.sh tools/release/product-version.mjs version oliphaunt-swift`, + `tools/dev/bun.sh tools/release/publish_swiftpm_source_tag.mjs --help`, + `tools/dev/bun.sh tools/release/publish_swiftpm_source_tag.mjs --target + 0.1.0`, `tools/dev/bun.sh tools/policy/check-python-entrypoints.mjs`, + `bash tools/policy/check-tooling-stack.sh`, + `python3 tools/release/check_release_metadata.py`, + `python3 tools/release/check_consumer_shape.py --products-json + '["oliphaunt-swift"]'`, `python3 tools/release/check_consumer_shape.py`, + `python3 tools/release/check_artifact_targets.py`, and + `git diff --cached --check`. - 2026-06-26: Coverage orchestration now runs through `tools/coverage/coverage.mjs` and the pinned Bun launcher while keeping the stable wrapper API (`tools/coverage/run-product`, `check-product`, and diff --git a/docs/maintainers/release-setup.md b/docs/maintainers/release-setup.md index a1336959..f854749c 100644 --- a/docs/maintainers/release-setup.md +++ b/docs/maintainers/release-setup.md @@ -340,7 +340,7 @@ tools/release/render_swiftpm_release_package.py \ ``` The release workflow passes that generated manifest to -`tools/release/publish_swiftpm_source_tag.py --manifest ...`. The publisher creates +`tools/dev/bun.sh tools/release/publish_swiftpm_source_tag.mjs --manifest ...`. The publisher creates a release-only commit parented by the source release commit with only `Package.swift` replaced, then tags that commit with the semver tag SwiftPM resolves. The source checkout still keeps `src/sdks/swift/Package.swift` diff --git a/src/shared/fixtures/consumer-shape/products.json b/src/shared/fixtures/consumer-shape/products.json index 7ffb454b..2c427446 100644 --- a/src/shared/fixtures/consumer-shape/products.json +++ b/src/shared/fixtures/consumer-shape/products.json @@ -771,7 +771,7 @@ "Package.swift", "src/sdks/swift/README.md", "tools/release/render_swiftpm_release_package.py", - "tools/release/publish_swiftpm_source_tag.py" + "tools/release/publish_swiftpm_source_tag.mjs" ], "requiredText": { "Package.swift": [ @@ -787,7 +787,7 @@ "binaryTarget(", "liboliphaunt-native-v" ], - "tools/release/publish_swiftpm_source_tag.py": [ + "tools/release/publish_swiftpm_source_tag.mjs": [ "commit-tree", "--manifest" ] diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index 17893854..60df9793 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -23,7 +23,6 @@ tools/release/optimize_native_runtime_payload.py tools/release/package_liboliphaunt_cargo_artifacts.py tools/release/package_liboliphaunt_wasix_cargo_artifacts.py tools/release/product_metadata.py -tools/release/publish_swiftpm_source_tag.py tools/release/release.py tools/release/release_plan.py tools/release/render_swiftpm_release_package.py diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index c40038c6..abb5ac90 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -569,12 +569,12 @@ def validate_swift(swift_version: str, liboliphaunt_version: str) -> None: if forbidden in renderer: fail(f"SwiftPM release manifest renderer must not synthesize base-package extension products: {forbidden}") require_text( - "tools/release/publish_swiftpm_source_tag.py", + "tools/release/publish_swiftpm_source_tag.mjs", "commit-tree", "SwiftPM source-tag publisher must create a release-only manifest commit", ) require_text( - "tools/release/publish_swiftpm_source_tag.py", + "tools/release/publish_swiftpm_source_tag.mjs", "--include-tree", "SwiftPM source-tag publisher must be able to include generated release-tree files", ) diff --git a/tools/release/product-version.mjs b/tools/release/product-version.mjs index 5766d577..585adaa9 100644 --- a/tools/release/product-version.mjs +++ b/tools/release/product-version.mjs @@ -167,7 +167,7 @@ function ensureSemver(product, version) { return version; } -async function currentVersion(product) { +export async function currentVersion(product) { const { packagePath, packageConfig } = await findPackageConfig(product); const versionFile = canonicalVersionFile(product, packagePath, packageConfig); const parser = parserForVersionFile(product, versionFile); @@ -192,4 +192,6 @@ async function main(argv) { console.log(await currentVersion(argv[1])); } -await main(Bun.argv.slice(2)); +if (import.meta.main) { + await main(Bun.argv.slice(2)); +} diff --git a/tools/release/publish_swiftpm_source_tag.mjs b/tools/release/publish_swiftpm_source_tag.mjs new file mode 100644 index 00000000..fd83c7d6 --- /dev/null +++ b/tools/release/publish_swiftpm_source_tag.mjs @@ -0,0 +1,235 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; +import { + mkdtempSync, + readdirSync, + readFileSync, + rmSync, + statSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; + +import { currentVersion } from "./product-version.mjs"; + +const ROOT = path.resolve(import.meta.dir, "../.."); +const SEMVER_RE = /^(0|[1-9][0-9]*)[.](0|[1-9][0-9]*)[.](0|[1-9][0-9]*)(?:[-+][0-9A-Za-z.-]+)?$/u; +const decoder = new TextDecoder(); + +function fail(message) { + console.error(`publish_swiftpm_source_tag.mjs: ${message}`); + process.exit(1); +} + +function usage(status = 1) { + const message = + "usage: tools/release/publish_swiftpm_source_tag.mjs [--target COMMITISH] [--manifest PACKAGE_SWIFT] [--include-tree TREE]... [--push]"; + if (status === 0) { + console.log(message); + process.exit(0); + } + fail(message); +} + +function valueArg(argv, index, name) { + const value = argv[index + 1]; + if (value === undefined || value.startsWith("--")) { + fail(`${name} requires a value`); + } + return value; +} + +function parseArgs(argv) { + const args = { + target: process.env.GITHUB_SHA || "HEAD", + manifest: undefined, + includeTrees: [], + push: false, + }; + for (let index = 0; index < argv.length; ) { + const arg = argv[index]; + if (arg === "--target") { + args.target = valueArg(argv, index, arg); + index += 2; + } else if (arg === "--manifest") { + args.manifest = valueArg(argv, index, arg); + index += 2; + } else if (arg === "--include-tree") { + args.includeTrees.push(valueArg(argv, index, arg)); + index += 2; + } else if (arg === "--push") { + args.push = true; + index += 1; + } else if (arg === "--help" || arg === "-h") { + usage(0); + } else { + usage(); + } + } + if (!args.target) { + fail("--target must not be empty"); + } + return args; +} + +function git(args, { env = process.env, check = true, input = undefined } = {}) { + const result = spawnSync("git", args, { + cwd: ROOT, + env, + input, + encoding: input instanceof Buffer ? "buffer" : "utf8", + stdout: "pipe", + stderr: "pipe", + }); + if (check && result.status !== 0) { + const stderr = Buffer.isBuffer(result.stderr) + ? decoder.decode(result.stderr).trim() + : String(result.stderr).trim(); + fail(`git ${args.join(" ")} failed${stderr ? `: ${stderr}` : ""}`); + } + const stdout = Buffer.isBuffer(result.stdout) + ? decoder.decode(result.stdout) + : String(result.stdout); + return { + status: result.status ?? 0, + stdout: stdout.trim(), + }; +} + +function commitForRef(ref) { + return git(["rev-parse", `${ref}^{commit}`]).stdout; +} + +function tagRef(tag) { + return `refs/tags/${tag}`; +} + +function tagCommit(tag) { + const result = git(["rev-parse", "--verify", "--quiet", `${tagRef(tag)}^{commit}`], { + check: false, + }); + return result.status === 0 ? result.stdout : null; +} + +async function swiftpmTag() { + const version = await currentVersion("oliphaunt-swift"); + if (!SEMVER_RE.test(version)) { + fail(`SwiftPM requires a semantic version tag; oliphaunt-swift version is ${JSON.stringify(version)}`); + } + return version; +} + +function commitParents(commit) { + const parts = git(["rev-list", "--parents", "-n", "1", commit]).stdout.split(/\s+/u).filter(Boolean); + return parts.slice(1); +} + +function treeForCommit(commit) { + return git(["rev-parse", `${commit}^{tree}`]).stdout; +} + +function syntheticCommitMatches(commit, parent, expectedTree) { + const parents = commitParents(commit); + return parents.length === 1 && parents[0] === parent && treeForCommit(commit) === expectedTree; +} + +function iterTreeFiles(root) { + const files = []; + function visit(directory) { + for (const entry of readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) { + const file = path.join(directory, entry.name); + if (entry.isDirectory()) { + visit(file); + } else if (entry.isFile()) { + files.push(file); + } else { + fail(`SwiftPM generated release tree contains unsupported file type: ${file}`); + } + } + } + visit(root); + return files.sort(); +} + +function addBlobToIndex(env, indexPath, data) { + const result = git(["hash-object", "-w", "--stdin"], { env, input: data }); + git(["update-index", "--add", "--cacheinfo", `100644,${result.stdout},${indexPath}`], { env }); +} + +function createSwiftpmReleaseTree(targetCommit, manifest, includeTrees) { + const baseTree = treeForCommit(targetCommit); + const tempRoot = mkdtempSync(path.join(tmpdir(), "oliphaunt-swiftpm-index.")); + try { + const env = { ...process.env, GIT_INDEX_FILE: path.join(tempRoot, "index") }; + git(["read-tree", baseTree], { env }); + addBlobToIndex(env, "Package.swift", manifest); + for (const includeTree of includeTrees) { + const root = path.resolve(ROOT, includeTree); + if (!statSync(root, { throwIfNoEntry: false })?.isDirectory()) { + fail(`SwiftPM generated release tree does not exist: ${includeTree}`); + } + for (const file of iterTreeFiles(root)) { + const relative = path.relative(root, file).split(path.sep).join("/"); + if (relative === "Package.swift" || relative.startsWith(".git/") || relative.includes("/.git/")) { + fail(`SwiftPM generated release tree contains forbidden path: ${relative}`); + } + addBlobToIndex(env, relative, readFileSync(file)); + } + } + return git(["write-tree"], { env }).stdout; + } finally { + rmSync(tempRoot, { recursive: true, force: true }); + } +} + +function createSwiftpmManifestCommit(targetCommit, tree, version) { + return git([ + "commit-tree", + tree, + "-p", + targetCommit, + "-m", + `Release Oliphaunt Swift ${version} SwiftPM manifest`, + ]).stdout; +} + +async function ensureTag({ target, manifest, includeTrees, push }) { + const tag = await swiftpmTag(); + const version = await currentVersion("oliphaunt-swift"); + const targetCommit = commitForRef(target); + let tagTarget = targetCommit; + let expectedTree = treeForCommit(targetCommit); + let manifestText = null; + + if (manifest !== undefined) { + manifestText = readFileSync(path.resolve(ROOT, manifest), "utf8"); + if (!manifestText.includes("binaryTarget(") || !manifestText.includes("liboliphaunt-native-v")) { + fail("SwiftPM release manifest must contain a checksum-pinned liboliphaunt binaryTarget"); + } + expectedTree = createSwiftpmReleaseTree(targetCommit, manifestText, includeTrees); + tagTarget = createSwiftpmManifestCommit(targetCommit, expectedTree, version); + } + + const existing = tagCommit(tag); + if (existing !== null) { + if (manifestText !== null && syntheticCommitMatches(existing, targetCommit, expectedTree)) { + console.log(`SwiftPM version tag ${tag} already points at a release manifest commit for ${targetCommit}`); + tagTarget = existing; + } else if (existing !== tagTarget) { + fail(`SwiftPM version tag ${tag} already points at ${existing}, not expected SwiftPM release commit ${tagTarget}`); + } else { + console.log(`SwiftPM version tag ${tag} already points at ${tagTarget}`); + } + } else { + git(["tag", tag, tagTarget]); + console.log(`created SwiftPM version tag ${tag} at ${tagTarget}`); + } + + if (push) { + git(["push", "origin", tagRef(tag)]); + console.log(`pushed SwiftPM version tag ${tag} to origin`); + } + return tag; +} + +await ensureTag(parseArgs(Bun.argv.slice(2))); diff --git a/tools/release/publish_swiftpm_source_tag.py b/tools/release/publish_swiftpm_source_tag.py deleted file mode 100755 index 8462439e..00000000 --- a/tools/release/publish_swiftpm_source_tag.py +++ /dev/null @@ -1,242 +0,0 @@ -#!/usr/bin/env python3 -"""Publish or verify the semver source tag SwiftPM needs for the Apple SDK.""" - -from __future__ import annotations - -import argparse -import os -import re -import subprocess -import sys -import tempfile -from pathlib import Path -from typing import NoReturn - -import product_metadata - - -ROOT = Path(__file__).resolve().parents[2] -SEMVER_RE = re.compile( - r"^(0|[1-9][0-9]*)[.](0|[1-9][0-9]*)[.](0|[1-9][0-9]*)(?:[-+][0-9A-Za-z.-]+)?$" -) - - -def fail(message: str) -> NoReturn: - print(f"publish_swiftpm_source_tag.py: {message}", file=sys.stderr) - raise SystemExit(1) - - -def git_output(args: list[str]) -> str: - return subprocess.check_output(["git", *args], cwd=ROOT, text=True).strip() - - -def git_run(args: list[str], *, env: dict[str, str] | None = None) -> None: - subprocess.run(["git", *args], cwd=ROOT, env=env, check=True) - - -def commit_for_ref(ref: str) -> str: - return git_output(["rev-parse", f"{ref}^{{commit}}"]) - - -def tag_ref(tag: str) -> str: - return f"refs/tags/{tag}" - - -def tag_commit(tag: str) -> str | None: - result = subprocess.run( - ["git", "rev-parse", "--verify", "--quiet", f"{tag_ref(tag)}^{{commit}}"], - cwd=ROOT, - check=False, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - ) - if result.returncode == 0: - return result.stdout.strip() - return None - - -def swiftpm_tag() -> str: - version = product_metadata.read_current_version("oliphaunt-swift") - if SEMVER_RE.fullmatch(version) is None: - fail(f"SwiftPM requires a semantic version tag; oliphaunt-swift version is {version!r}") - return version - - -def commit_parents(commit: str) -> list[str]: - parts = git_output(["rev-list", "--parents", "-n", "1", commit]).split() - return parts[1:] - - -def file_at_ref(ref: str, path: str) -> str | None: - result = subprocess.run( - ["git", "show", f"{ref}:{path}"], - cwd=ROOT, - check=False, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - ) - return result.stdout if result.returncode == 0 else None - - -def tree_for_commit(commit: str) -> str: - return git_output(["rev-parse", f"{commit}^{{tree}}"]) - - -def synthetic_commit_matches(commit: str, parent: str, expected_tree: str) -> bool: - return commit_parents(commit) == [parent] and tree_for_commit(commit) == expected_tree - - -def iter_tree_files(root: Path) -> list[Path]: - files: list[Path] = [] - for path in sorted(root.rglob("*")): - if path.is_file(): - files.append(path) - elif not path.is_dir(): - fail(f"SwiftPM generated release tree contains unsupported file type: {path}") - return files - - -def add_blob_to_index(env: dict[str, str], path: str, data: str | bytes) -> None: - binary = isinstance(data, bytes) - blob_output = subprocess.run( - ["git", "hash-object", "-w", "--stdin"], - cwd=ROOT, - env=env, - check=True, - text=not binary, - input=data, - stdout=subprocess.PIPE, - ).stdout - blob = blob_output.decode("utf-8").strip() if binary else blob_output.strip() - git_run(["update-index", "--add", "--cacheinfo", f"100644,{blob},{path}"], env=env) - - -def create_swiftpm_release_tree( - target_commit: str, - manifest: str, - include_trees: list[Path], -) -> str: - base_tree = git_output(["rev-parse", f"{target_commit}^{{tree}}"]) - with tempfile.TemporaryDirectory(prefix="oliphaunt-swiftpm-index.") as tmp: - env = {**os.environ, "GIT_INDEX_FILE": str(Path(tmp) / "index")} - git_run(["read-tree", base_tree], env=env) - add_blob_to_index(env, "Package.swift", manifest) - for include_tree in include_trees: - root = include_tree.resolve() - if not root.is_dir(): - fail(f"SwiftPM generated release tree does not exist: {include_tree}") - for file in iter_tree_files(root): - relative = file.relative_to(root).as_posix() - if relative == "Package.swift" or relative.startswith(".git/") or "/.git/" in relative: - fail(f"SwiftPM generated release tree contains forbidden path: {relative}") - add_blob_to_index(env, relative, file.read_bytes()) - return subprocess.run( - ["git", "write-tree"], - cwd=ROOT, - env=env, - check=True, - text=True, - stdout=subprocess.PIPE, - ).stdout.strip() - - -def create_swiftpm_manifest_commit(target_commit: str, tree: str, version: str) -> str: - return subprocess.run( - [ - "git", - "commit-tree", - tree, - "-p", - target_commit, - "-m", - f"Release Oliphaunt Swift {version} SwiftPM manifest", - ], - cwd=ROOT, - check=True, - text=True, - stdout=subprocess.PIPE, - ).stdout.strip() - - -def ensure_tag(target: str, *, manifest_path: str | None, include_trees: list[str], push: bool) -> str: - tag = swiftpm_tag() - version = product_metadata.read_current_version("oliphaunt-swift") - target_commit = commit_for_ref(target) - manifest = None - tag_target = target_commit - expected_tree = tree_for_commit(target_commit) - - if manifest_path is not None: - manifest = (ROOT / manifest_path).read_text(encoding="utf-8") - if "binaryTarget(" not in manifest or "liboliphaunt-native-v" not in manifest: - fail("SwiftPM release manifest must contain a checksum-pinned liboliphaunt binaryTarget") - expected_tree = create_swiftpm_release_tree( - target_commit, - manifest, - [(ROOT / include_tree) for include_tree in include_trees], - ) - tag_target = create_swiftpm_manifest_commit(target_commit, expected_tree, version) - - existing = tag_commit(tag) - if existing is not None: - if manifest is not None and synthetic_commit_matches(existing, target_commit, expected_tree): - print(f"SwiftPM version tag {tag} already points at a release manifest commit for {target_commit}") - tag_target = existing - elif existing != tag_target: - fail( - f"SwiftPM version tag {tag} already points at {existing}, " - f"not expected SwiftPM release commit {tag_target}" - ) - else: - print(f"SwiftPM version tag {tag} already points at {tag_target}") - else: - git_run(["tag", tag, tag_target]) - print(f"created SwiftPM version tag {tag} at {tag_target}") - - if push: - git_run(["push", "origin", tag_ref(tag)]) - print(f"pushed SwiftPM version tag {tag} to origin") - return tag - - -def parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--target", - default=os.environ.get("GITHUB_SHA", "HEAD"), - help="commitish that the SwiftPM version tag must derive from", - ) - parser.add_argument( - "--manifest", - help=( - "generated public SwiftPM Package.swift to place in a release-only " - "tag commit; when omitted, the semver tag points directly at --target" - ), - ) - parser.add_argument( - "--include-tree", - action="append", - default=[], - help=( - "generated repository-relative file tree to include in the release-only " - "SwiftPM tag commit; may be passed multiple times" - ), - ) - parser.add_argument( - "--push", - action="store_true", - help="push the tag to origin after creating or verifying it locally", - ) - return parser.parse_args(argv) - - -def main(argv: list[str]) -> int: - args = parse_args(argv) - ensure_tag(args.target, manifest_path=args.manifest, include_trees=args.include_tree, push=args.push) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) diff --git a/tools/release/release.py b/tools/release/release.py index 1bb2a778..03a5221c 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -1795,7 +1795,8 @@ def publish_swift_release(head_ref: str) -> None: manifest = prepare_staged_swift_release_manifest() run( [ - "tools/release/publish_swiftpm_source_tag.py", + "tools/dev/bun.sh", + "tools/release/publish_swiftpm_source_tag.mjs", "--target", head_ref, "--manifest", From 7f02906905dccc33d940a1688f191d903eca6061 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 21:59:46 +0000 Subject: [PATCH 125/137] chore: port maven artifact manifest to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 21 + ...2026-06-07-transitional-catalog-smoke.json | 2 +- .../generated/docs/extension-evidence.json | 80 +-- .../assets/generated/asset-inputs.sha256 | 2 +- tools/policy/python-entrypoints.allowlist | 1 - .../release/build_maven_artifact_manifest.mjs | 551 ++++++++++++++++++ .../release/build_maven_artifact_manifest.py | 205 ------- tools/release/check_consumer_shape.py | 2 +- tools/release/check_release_metadata.py | 10 +- tools/release/release.py | 4 +- 10 files changed, 622 insertions(+), 256 deletions(-) create mode 100644 tools/release/build_maven_artifact_manifest.mjs delete mode 100644 tools/release/build_maven_artifact_manifest.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 1e7a8f26..a6773a35 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -248,6 +248,27 @@ until the current-state gates here are checked with fresh local evidence. '["oliphaunt-swift"]'`, `python3 tools/release/check_consumer_shape.py`, `python3 tools/release/check_artifact_targets.py`, and `git diff --cached --check`. +- 2026-06-26: Maven runtime and exact-extension artifact TSV generation now + runs through `tools/release/build_maven_artifact_manifest.mjs` and the + pinned Bun launcher instead of the retired Python entrypoint. The Bun port + derives versions from `product-version.mjs`, release products and published + targets from Moon release metadata, Maven coordinates and extension SQL names + from `release.toml`, and exact-extension Android rows from the same default + target rules plus `targets/artifacts.toml` overrides as the retired Python + helper. The release PR sync gate also refreshed the WASIX asset input + fingerprint and extension evidence source digests. Fresh checks passed: + runtime TSV smoke against `target/tools-split-fixture-assets`, PostGIS + extension TSV smoke against a two-file Android Maven fixture, + `tools/dev/bun.sh tools/policy/check-python-entrypoints.mjs`, + `bash tools/policy/check-tooling-stack.sh`, + `python3 tools/release/check_release_metadata.py`, + `python3 tools/release/check_consumer_shape.py --products-json + '["liboliphaunt-native","oliphaunt-kotlin"]'`, + `python3 tools/release/check_consumer_shape.py`, + `python3 tools/release/check_artifact_targets.py`, + `python3 tools/policy/check-release-policy.py`, + `python3 tools/release/sync_release_pr.py --check`, + `tools/release/release.py check`, and `git diff --cached --check`. - 2026-06-26: Coverage orchestration now runs through `tools/coverage/coverage.mjs` and the pinned Bun launcher while keeping the stable wrapper API (`tools/coverage/run-product`, `check-product`, and diff --git a/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json b/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json index 20f7549a..04c7a770 100644 --- a/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json +++ b/src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json @@ -514,7 +514,7 @@ } ], "schema": "oliphaunt-extension-evidence-v1", - "sourceDigest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67", + "sourceDigest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d", "sourceDigestInputs": [ "src/postgres/versions/18/source.toml", "src/extensions/catalog/extensions.promoted.toml", diff --git a/src/extensions/generated/docs/extension-evidence.json b/src/extensions/generated/docs/extension-evidence.json index 9777420e..57b985e5 100644 --- a/src/extensions/generated/docs/extension-evidence.json +++ b/src/extensions/generated/docs/extension-evidence.json @@ -20,7 +20,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -56,7 +56,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -92,7 +92,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -128,7 +128,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -164,7 +164,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -200,7 +200,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -236,7 +236,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -272,7 +272,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -308,7 +308,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -344,7 +344,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -380,7 +380,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -416,7 +416,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -452,7 +452,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -488,7 +488,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -524,7 +524,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -560,7 +560,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -596,7 +596,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -632,7 +632,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -668,7 +668,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -704,7 +704,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -740,7 +740,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -776,7 +776,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -812,7 +812,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -848,7 +848,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -884,7 +884,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -920,7 +920,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -956,7 +956,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -992,7 +992,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -1028,7 +1028,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -1064,7 +1064,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -1100,7 +1100,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -1136,7 +1136,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -1172,7 +1172,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -1208,7 +1208,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -1244,7 +1244,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -1280,7 +1280,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -1316,7 +1316,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -1352,7 +1352,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -1388,7 +1388,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67" + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" } ], "platform-targets": [ @@ -1420,7 +1420,7 @@ "path": "src/extensions/evidence/runs" } ], - "source-digest": "sha256:df4c618e0a121c314856fbcbab0268079b19eb6766354ab0524f5da024c72e67", + "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d", "source-digest-inputs": [ "src/postgres/versions/18/source.toml", "src/extensions/catalog/extensions.promoted.toml", diff --git a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 index 2d96c62e..2668898d 100644 --- a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 +++ b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 @@ -1 +1 @@ -8ce51e356a666dcebe4be6fba8c685b6e76f7b0c2c3ed49862df2a2df7adf33a +cddc150a6d1d2817d4856b7a439c3ab81a3b92c1d4f85504b281e8c30e81c50e diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index 60df9793..bad822a4 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -7,7 +7,6 @@ tools/policy/check-release-policy.py tools/release/artifact_target_matrix.py tools/release/artifact_targets.py tools/release/build-extension-ci-artifacts.py -tools/release/build_maven_artifact_manifest.py tools/release/check_artifact_targets.py tools/release/check_consumer_shape.py tools/release/check_cratesio_publication.py diff --git a/tools/release/build_maven_artifact_manifest.mjs b/tools/release/build_maven_artifact_manifest.mjs new file mode 100644 index 00000000..7a68d330 --- /dev/null +++ b/tools/release/build_maven_artifact_manifest.mjs @@ -0,0 +1,551 @@ +#!/usr/bin/env bun +import { existsSync } from "node:fs"; +import fs from "node:fs/promises"; +import path from "node:path"; + +import { runMoon } from "../policy/moon.mjs"; +import { currentVersion } from "./product-version.mjs"; + +const ROOT = path.resolve(import.meta.dir, "../.."); +const PREFIX = "build_maven_artifact_manifest.mjs"; +const EXTENSION_ARTIFACT_SCHEMA = "oliphaunt-extension-artifact-targets-v1"; +const EXTENSION_FAMILIES = new Set(["native", "wasix"]); +const EXTENSION_KINDS = new Set(["native-dynamic", "native-static-registry", "wasix-runtime"]); +const EXTENSION_STATUSES = new Set(["supported", "planned", "unsupported"]); +const NATIVE_RUNTIME_TARGETS = new Set([ + "android-arm64-v8a", + "android-x86_64", + "ios-xcframework", + "linux-arm64-gnu", + "linux-x64-gnu", + "macos-arm64", + "macos-x64", + "windows-x64-msvc", +]); +const WASIX_TARGETS = new Set(["portable", "linux-arm64-gnu", "linux-x64-gnu", "macos-arm64", "windows-x64-msvc"]); + +function fail(message) { + console.error(`${PREFIX}: ${message}`); + process.exit(1); +} + +function rel(file) { + return path.relative(ROOT, file).split(path.sep).join("/"); +} + +function repoPath(value) { + return path.isAbsolute(value) ? value : path.join(ROOT, value); +} + +async function readToml(file) { + let text; + try { + text = await fs.readFile(file, "utf8"); + } catch (error) { + fail(`missing ${rel(file)}: ${error.message}`); + } + try { + return Bun.TOML.parse(text); + } catch (error) { + fail(`${rel(file)} is invalid TOML: ${error.message}`); + } +} + +async function readReleaseToml(product) { + const metadata = moonReleaseMetadata(product); + return readToml(path.join(ROOT, metadata.packagePath, "release.toml")); +} + +let releaseProducts; + +function moonReleaseProducts() { + if (releaseProducts !== undefined) { + return releaseProducts; + } + const value = JSON.parse(runMoon(["query", "projects"])); + if (!Array.isArray(value.projects)) { + fail("moon query projects did not return a projects array"); + } + releaseProducts = new Map(); + for (const project of value.projects) { + const id = project?.id; + const release = project?.config?.project?.metadata?.release; + if (release === undefined) { + continue; + } + if (typeof id !== "string" || release === null || typeof release !== "object" || Array.isArray(release)) { + fail("Moon release metadata returned an invalid product row"); + } + if (release.component !== id) { + fail(`Moon release metadata for ${id} must use matching component`); + } + if (typeof release.packagePath !== "string" || release.packagePath.length === 0) { + fail(`Moon release metadata for ${id} must declare packagePath`); + } + releaseProducts.set(id, release); + } + if (releaseProducts.size === 0) { + fail("Moon project graph does not contain release products"); + } + return releaseProducts; +} + +function moonReleaseMetadata(product) { + const release = moonReleaseProducts().get(product); + if (release === undefined) { + fail(`unknown release product ${product}`); + } + return release; +} + +function stringList(config, key, product) { + const value = config[key] ?? []; + if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) { + fail(`${product}.${key} must be a string list`); + } + return value; +} + +async function registryPackageNames(product, packageKind) { + const config = await readReleaseToml(product); + const names = []; + for (const raw of stringList(config, "registry_packages", product)) { + const separator = raw.indexOf(":"); + if (separator <= 0 || separator === raw.length - 1) { + fail(`${product}.registry_packages entry ${JSON.stringify(raw)} must use kind:name`); + } + const kind = raw.slice(0, separator); + const name = raw.slice(separator + 1); + if (kind === packageKind) { + names.push(name); + } + } + const duplicates = names.filter((name, index) => names.indexOf(name) !== index); + if (duplicates.length > 0) { + fail(`${product} declares duplicate ${packageKind} registry packages: ${[...new Set(duplicates)].join(", ")}`); + } + return names; +} + +function publishedTargets(product, expectedPreset) { + const release = moonReleaseMetadata(product); + const config = release.artifactTargets; + if (config === null || typeof config !== "object" || Array.isArray(config)) { + fail(`Moon release metadata for ${product} must declare artifactTargets`); + } + if (config.preset !== expectedPreset) { + fail(`Moon release metadata for ${product} artifactTargets.preset must be ${JSON.stringify(expectedPreset)}`); + } + const targets = config.publishedTargets; + if (!Array.isArray(targets) || !targets.every((target) => typeof target === "string" && target.length > 0)) { + fail(`Moon release metadata for ${product} artifactTargets.publishedTargets must be a string list`); + } + const seen = new Set(); + for (const target of targets) { + if (seen.has(target)) { + fail(`Moon release metadata for ${product} artifactTargets.publishedTargets contains duplicate target ${target}`); + } + seen.add(target); + } + return [...targets].sort(); +} + +function checkedPublishedTargets(product, expectedPreset, knownTargets) { + const targets = publishedTargets(product, expectedPreset); + const unknown = targets.filter((target) => !knownTargets.has(target)); + if (unknown.length > 0) { + fail(`Moon release metadata for ${product} declares unknown artifact target(s): ${unknown.join(", ")}`); + } + return targets; +} + +function nativeRuntimeArtifactTargets(version) { + const rows = [ + { + id: "liboliphaunt-native.runtime-resources", + kind: "runtime-resources", + target: "portable", + asset: `liboliphaunt-${version}-runtime-resources.tar.gz`, + }, + { + id: "liboliphaunt-native.icu-data", + kind: "icu-data", + target: "portable", + asset: `liboliphaunt-${version}-icu-data.tar.gz`, + }, + ]; + for (const target of checkedPublishedTargets("liboliphaunt-native", "liboliphaunt-native", NATIVE_RUNTIME_TARGETS)) { + if (!target.startsWith("android-")) { + continue; + } + rows.push({ + id: `liboliphaunt-native.${target}`, + kind: "native-runtime", + target, + asset: `liboliphaunt-${version}-${target}.tar.gz`, + }); + } + return rows.sort((left, right) => left.id.localeCompare(right.id)); +} + +function runtimeMavenArtifactId(target) { + if (target.kind === "runtime-resources") { + return "liboliphaunt-runtime-resources"; + } + if (target.kind === "icu-data") { + return "oliphaunt-icu"; + } + if (target.kind === "native-runtime" && target.target.startsWith("android-")) { + return `liboliphaunt-${target.target}`; + } + return undefined; +} + +function runtimeMavenArtifactMetadata(target) { + if (target.kind === "runtime-resources") { + return { + name: "Oliphaunt runtime resources", + description: "Package-managed Oliphaunt PostgreSQL runtime resources for Android app builds.", + }; + } + if (target.kind === "icu-data") { + return { + name: "Oliphaunt ICU data", + description: "Package-managed optional ICU data files for Oliphaunt app builds.", + }; + } + if (target.kind === "native-runtime" && target.target.startsWith("android-")) { + const abi = target.target.slice("android-".length); + return { + name: `Oliphaunt Android runtime ${abi}`, + description: `Package-managed liboliphaunt Android runtime for ${abi} app builds.`, + }; + } + fail(`unsupported liboliphaunt-native Maven artifact target ${target.id}`); +} + +function runtimeMavenArtifacts(version) { + const artifacts = new Map(); + for (const target of nativeRuntimeArtifactTargets(version)) { + const artifactId = runtimeMavenArtifactId(target); + if (artifactId === undefined) { + continue; + } + if (artifacts.has(artifactId)) { + fail(`duplicate liboliphaunt-native Maven artifact mapping for ${artifactId}`); + } + artifacts.set(artifactId, { + filename: target.asset, + ...runtimeMavenArtifactMetadata(target), + }); + } + if (artifacts.size === 0) { + fail("liboliphaunt-native artifact targets did not produce any Maven runtime artifacts"); + } + return artifacts; +} + +function splitMavenCoordinate(coordinate) { + const separator = coordinate.indexOf(":"); + if (separator <= 0 || separator === coordinate.length - 1) { + fail(`invalid Maven coordinate ${JSON.stringify(coordinate)}; expected group:artifact`); + } + return [coordinate.slice(0, separator), coordinate.slice(separator + 1)]; +} + +async function requireFile(file, label) { + try { + const stat = await fs.stat(file); + if (stat.isFile()) { + return file; + } + } catch { + // Fall through to the shared diagnostic below. + } + fail(`missing ${label}: ${rel(file)}`); +} + +function tsvRow({ groupId, artifactId, version, file, name, description }) { + const values = [groupId, artifactId, version, rel(file), name, description]; + if (values.some((value) => value.includes("\t") || value.includes("\n"))) { + fail(`Maven artifact manifest value contains a tab or newline: ${JSON.stringify(values)}`); + } + return values.join("\t"); +} + +async function runtimeRows(assetRoot) { + const version = await currentVersion("liboliphaunt-native"); + const artifacts = runtimeMavenArtifacts(version); + const rows = []; + for (const coordinate of await registryPackageNames("liboliphaunt-native", "maven")) { + const [groupId, artifactId] = splitMavenCoordinate(coordinate); + if (groupId !== "dev.oliphaunt.runtime") { + fail(`liboliphaunt-native Maven artifact ${coordinate} must use dev.oliphaunt.runtime`); + } + const artifact = artifacts.get(artifactId); + if (artifact === undefined) { + fail(`liboliphaunt-native Maven artifact ${coordinate} has no release asset mapping`); + } + rows.push( + tsvRow({ + groupId, + artifactId, + version, + file: await requireFile(path.join(assetRoot, artifact.filename), artifactId), + name: artifact.name, + description: artifact.description, + }), + ); + } + return rows; +} + +function defaultNativeExtensionKind(target) { + if (target === "ios-xcframework" || target.startsWith("android-")) { + return "native-static-registry"; + } + return "native-dynamic"; +} + +function wasixExtensionTargetId(runtimeTarget) { + return runtimeTarget === "portable" ? "wasix-portable" : runtimeTarget; +} + +function defaultExtensionTargetRows(product) { + const rows = []; + for (const target of checkedPublishedTargets("liboliphaunt-native", "liboliphaunt-native", NATIVE_RUNTIME_TARGETS)) { + rows.push({ + target, + family: "native", + kind: defaultNativeExtensionKind(target), + status: "supported", + published: true, + sourceFile: `${moonReleaseMetadata(product).packagePath}/release.toml`, + }); + } + for (const target of checkedPublishedTargets("liboliphaunt-wasix", "liboliphaunt-wasix", WASIX_TARGETS)) { + if (target === "portable") { + rows.push({ + target: wasixExtensionTargetId(target), + family: "wasix", + kind: "wasix-runtime", + status: "supported", + published: true, + sourceFile: `${moonReleaseMetadata(product).packagePath}/release.toml`, + }); + } + } + if (rows.length === 0) { + fail(`${product} could not derive any exact-extension artifact targets`); + } + return rows; +} + +function boolValue(value, label) { + if (typeof value === "boolean") { + return value; + } + fail(`${label} must be true or false`); +} + +function stringValue(value, label) { + if (typeof value === "string" && value.length > 0) { + return value; + } + fail(`${label} must be a non-empty string`); +} + +async function extensionArtifactTargets(product) { + const productPath = moonReleaseMetadata(product).packagePath; + const overridePath = path.join(ROOT, productPath, "targets", "artifacts.toml"); + const defaultRows = defaultExtensionTargetRows(product); + let rows; + let sourceLabel; + const hasOverride = existsSync(overridePath); + if (hasOverride) { + const data = await readToml(overridePath); + if (data.schema !== EXTENSION_ARTIFACT_SCHEMA) { + fail(`${rel(overridePath)} must use schema = ${JSON.stringify(EXTENSION_ARTIFACT_SCHEMA)}`); + } + if (!Array.isArray(data.targets) || data.targets.length === 0) { + fail(`${rel(overridePath)} must define [[targets]] rows`); + } + rows = data.targets; + sourceLabel = rel(overridePath); + } else { + rows = defaultRows; + sourceLabel = `${productPath}/release.toml`; + } + + const allowedOverrideKeys = new Set( + defaultRows.map((row) => JSON.stringify([row.target, row.family, row.kind])), + ); + const seen = new Set(); + return rows.map((row, index) => { + if (row === null || typeof row !== "object" || Array.isArray(row)) { + fail(`${sourceLabel} targets[${index}] must be a table`); + } + const target = stringValue(row.target, `${sourceLabel} targets[${index}].target`); + const family = stringValue(row.family, `${sourceLabel} targets[${index}].family`); + const kind = stringValue(row.kind, `${sourceLabel} targets[${index}].kind`); + const status = stringValue(row.status, `${sourceLabel} targets[${index}].status`); + const published = boolValue(row.published, `${sourceLabel} targets[${index}].published`); + if (!EXTENSION_FAMILIES.has(family)) { + fail(`${sourceLabel} target ${target} has invalid family ${JSON.stringify(family)}`); + } + if (!EXTENSION_KINDS.has(kind)) { + fail(`${sourceLabel} target ${target} has invalid kind ${JSON.stringify(kind)}`); + } + if (!EXTENSION_STATUSES.has(status)) { + fail(`${sourceLabel} target ${target} has invalid status ${JSON.stringify(status)}`); + } + if (family === "wasix" && kind !== "wasix-runtime") { + fail(`${sourceLabel} target ${target} must use kind wasix-runtime for wasix family`); + } + if (family === "native" && kind === "wasix-runtime") { + fail(`${sourceLabel} target ${target} cannot use wasix-runtime for native family`); + } + if (published && status !== "supported") { + fail(`${sourceLabel} target ${target} cannot be published with status ${status}`); + } + if (!published && (typeof row.unsupported_reason !== "string" || row.unsupported_reason.length === 0)) { + fail(`${sourceLabel} unpublished target ${target} must explain unsupported_reason`); + } + const key = JSON.stringify([target, family, kind]); + if (seen.has(key)) { + fail(`${sourceLabel} has duplicate target row ${key}`); + } + if (hasOverride && !allowedOverrideKeys.has(key)) { + fail(`${sourceLabel} target row ${key} is not backed by runtime artifact metadata`); + } + seen.add(key); + return { target, family, kind, status, published }; + }); +} + +async function publishedAndroidMavenTargets(product) { + return (await extensionArtifactTargets(product)) + .filter( + (target) => + target.family === "native" && + target.published && + target.kind === "native-static-registry" && + target.target.startsWith("android-"), + ) + .sort((left, right) => left.target.localeCompare(right.target)); +} + +async function exactExtensionProducts() { + const products = []; + for (const product of [...moonReleaseProducts().keys()].sort()) { + const config = await readReleaseToml(product); + if (config.kind === "exact-extension-artifact") { + products.push(product); + } + } + return products; +} + +async function extensionRows(extensionRoot, selectedProducts) { + const products = selectedProducts.length > 0 ? selectedProducts : await exactExtensionProducts(); + const rows = []; + for (const product of [...products].sort()) { + const config = await readReleaseToml(product); + if (config.kind !== "exact-extension-artifact") { + fail(`${product} is not an exact-extension-artifact product`); + } + const sqlName = config.extension_sql_name; + if (typeof sqlName !== "string" || sqlName.length === 0) { + fail(`${product} release metadata must declare extension_sql_name`); + } + const version = await currentVersion(product); + const productRoot = path.join(extensionRoot, product, "release-assets"); + const targets = await publishedAndroidMavenTargets(product); + if (targets.length === 0) { + fail(`${product} has no published Android Maven extension targets`); + } + for (const target of targets) { + const filename = `${product}-${version}-native-${target.target}-runtime.tar.gz`; + rows.push( + tsvRow({ + groupId: "dev.oliphaunt.extensions", + artifactId: `${product}-${target.target}`, + version, + file: await requireFile(path.join(productRoot, filename), `${product} ${target.target} Maven artifact`), + name: `Oliphaunt extension ${sqlName} ${target.target}`, + description: `Package-managed Oliphaunt Android runtime and static-link artifacts for the ${sqlName} PostgreSQL extension on ${target.target}.`, + }), + ); + } + } + return rows; +} + +function valueArg(argv, index, name) { + const value = argv[index + 1]; + if (value === undefined || value.startsWith("--")) { + fail(`${name} requires a value`); + } + return value; +} + +function parseArgs(argv) { + const args = { + output: undefined, + runtimeAssetRoot: "target/liboliphaunt/release-assets", + extensionArtifactRoot: "target/extension-artifacts", + runtime: false, + extensions: false, + extensionProducts: [], + }; + for (let index = 0; index < argv.length; ) { + const arg = argv[index]; + if (arg === "--output") { + args.output = valueArg(argv, index, arg); + index += 2; + } else if (arg === "--runtime-asset-root") { + args.runtimeAssetRoot = valueArg(argv, index, arg); + index += 2; + } else if (arg === "--extension-artifact-root") { + args.extensionArtifactRoot = valueArg(argv, index, arg); + index += 2; + } else if (arg === "--runtime") { + args.runtime = true; + index += 1; + } else if (arg === "--extensions") { + args.extensions = true; + index += 1; + } else if (arg === "--extension-product") { + args.extensionProducts.push(valueArg(argv, index, arg)); + index += 2; + } else { + fail(`unknown argument: ${arg}`); + } + } + if (!args.output) { + fail("--output is required"); + } + return args; +} + +async function main(argv) { + const args = parseArgs(argv); + const includeRuntime = args.runtime || !args.extensions; + const includeExtensions = args.extensions || args.extensionProducts.length > 0; + const rows = []; + if (includeRuntime) { + rows.push(...(await runtimeRows(repoPath(args.runtimeAssetRoot)))); + } + if (includeExtensions) { + rows.push(...(await extensionRows(repoPath(args.extensionArtifactRoot), args.extensionProducts))); + } + if (rows.length === 0) { + fail("manifest would be empty"); + } + const output = repoPath(args.output); + await fs.mkdir(path.dirname(output), { recursive: true }); + await fs.writeFile(output, `${rows.join("\n")}\n`, "utf8"); + console.log(`Wrote ${rows.length} Maven artifact publication row(s) to ${rel(output)}`); +} + +await main(Bun.argv.slice(2)); diff --git a/tools/release/build_maven_artifact_manifest.py b/tools/release/build_maven_artifact_manifest.py deleted file mode 100644 index b3c1ac1c..00000000 --- a/tools/release/build_maven_artifact_manifest.py +++ /dev/null @@ -1,205 +0,0 @@ -#!/usr/bin/env python3 -"""Build a manifest for Oliphaunt tarball Maven artifact publications.""" - -from __future__ import annotations - -import argparse -import sys -from pathlib import Path -from typing import NoReturn - -import artifact_targets -import extension_artifact_targets -import product_metadata - - -ROOT = Path(__file__).resolve().parents[2] - - -def fail(message: str) -> NoReturn: - print(f"build_maven_artifact_manifest.py: {message}", file=sys.stderr) - raise SystemExit(1) - - -def repo_path(value: str) -> Path: - path = Path(value) - if not path.is_absolute(): - path = ROOT / path - return path - - -def require_file(path: Path, label: str) -> Path: - if not path.is_file(): - fail(f"missing {label}: {path.relative_to(ROOT)}") - return path - - -def tsv_row( - *, - group_id: str, - artifact_id: str, - version: str, - file: Path, - name: str, - description: str, -) -> str: - values = [group_id, artifact_id, version, str(file.relative_to(ROOT)), name, description] - if any("\t" in value or "\n" in value for value in values): - fail(f"Maven artifact manifest value contains a tab or newline: {values}") - return "\t".join(values) - - -def split_maven_coordinate(coordinate: str) -> tuple[str, str]: - group_id, separator, artifact_id = coordinate.partition(":") - if not separator or not group_id or not artifact_id: - fail(f"invalid Maven coordinate {coordinate!r}; expected group:artifact") - return group_id, artifact_id - - -def runtime_maven_artifact_id(target: artifact_targets.ArtifactTarget) -> str | None: - if target.kind == "runtime-resources": - return "liboliphaunt-runtime-resources" - if target.kind == "icu-data": - return "oliphaunt-icu" - if target.kind == "native-runtime" and target.target.startswith("android-"): - return f"liboliphaunt-{target.target}" - return None - - -def runtime_maven_artifact_metadata(target: artifact_targets.ArtifactTarget) -> tuple[str, str]: - if target.kind == "runtime-resources": - return ( - "Oliphaunt runtime resources", - "Package-managed Oliphaunt PostgreSQL runtime resources for Android app builds.", - ) - if target.kind == "icu-data": - return ( - "Oliphaunt ICU data", - "Package-managed optional ICU data files for Oliphaunt app builds.", - ) - if target.kind == "native-runtime" and target.target.startswith("android-"): - abi = target.target.removeprefix("android-") - return ( - f"Oliphaunt Android runtime {abi}", - f"Package-managed liboliphaunt Android runtime for {abi} app builds.", - ) - fail(f"unsupported liboliphaunt-native Maven artifact target {target.id}") - - -def runtime_maven_artifacts(version: str) -> dict[str, dict[str, str]]: - artifacts: dict[str, dict[str, str]] = {} - for target in artifact_targets.artifact_targets( - product="liboliphaunt-native", - surface="maven", - published_only=True, - ): - artifact_id = runtime_maven_artifact_id(target) - if artifact_id is None: - continue - if artifact_id in artifacts: - fail(f"duplicate liboliphaunt-native Maven artifact mapping for {artifact_id}") - name, description = runtime_maven_artifact_metadata(target) - artifacts[artifact_id] = { - "filename": target.asset_name(version), - "name": name, - "description": description, - } - if not artifacts: - fail("liboliphaunt-native artifact targets did not produce any Maven runtime artifacts") - return artifacts - - -def runtime_rows(asset_root: Path) -> list[str]: - version = product_metadata.read_current_version("liboliphaunt-native") - artifacts = runtime_maven_artifacts(version) - rows = [] - for coordinate in product_metadata.registry_package_names("liboliphaunt-native", "maven"): - group_id, artifact_id = split_maven_coordinate(coordinate) - if group_id != "dev.oliphaunt.runtime": - fail(f"liboliphaunt-native Maven artifact {coordinate} must use dev.oliphaunt.runtime") - artifact = artifacts.get(artifact_id) - if artifact is None: - fail(f"liboliphaunt-native Maven artifact {coordinate} has no release asset mapping") - rows.append( - tsv_row( - group_id=group_id, - artifact_id=artifact_id, - version=version, - file=require_file(asset_root / artifact["filename"], artifact_id), - name=artifact["name"], - description=artifact["description"], - ) - ) - return rows - - -def extension_rows(extension_root: Path, selected_products: list[str]) -> list[str]: - products = selected_products or [ - product - for product in product_metadata.product_ids() - if product_metadata.product_config(product).get("kind") == "exact-extension-artifact" - ] - rows: list[str] = [] - for product in sorted(products): - config = product_metadata.product_config(product) - if config.get("kind") != "exact-extension-artifact": - fail(f"{product} is not an exact-extension-artifact product") - sql_name = config.get("extension_sql_name") - if not isinstance(sql_name, str) or not sql_name: - fail(f"{product} release metadata must declare extension_sql_name") - version = product_metadata.read_current_version(product) - product_root = extension_root / product / "release-assets" - targets = extension_artifact_targets.published_android_maven_targets(product) - if not targets: - fail(f"{product} has no published Android Maven extension targets") - for target in targets: - filename = f"{product}-{version}-native-{target.target}-runtime.tar.gz" - rows.append( - tsv_row( - group_id="dev.oliphaunt.extensions", - artifact_id=f"{product}-{target.target}", - version=version, - file=require_file(product_root / filename, f"{product} {target.target} Maven artifact"), - name=f"Oliphaunt extension {sql_name} {target.target}", - description=f"Package-managed Oliphaunt Android runtime and static-link artifacts for the {sql_name} PostgreSQL extension on {target.target}.", - ) - ) - return rows - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--output", required=True, help="TSV manifest path to write") - parser.add_argument( - "--runtime-asset-root", - default="target/liboliphaunt/release-assets", - help="Directory containing liboliphaunt runtime release assets", - ) - parser.add_argument( - "--extension-artifact-root", - default="target/extension-artifacts", - help="Directory containing staged exact-extension package artifacts", - ) - parser.add_argument("--runtime", action="store_true", help="include base liboliphaunt Android runtime artifacts") - parser.add_argument("--extensions", action="store_true", help="include Android exact-extension artifacts") - parser.add_argument("--extension-product", action="append", default=[], help="exact-extension product to include") - args = parser.parse_args() - - include_runtime = args.runtime or not args.extensions - include_extensions = args.extensions or bool(args.extension_product) - rows: list[str] = [] - if include_runtime: - rows.extend(runtime_rows(repo_path(args.runtime_asset_root))) - if include_extensions: - rows.extend(extension_rows(repo_path(args.extension_artifact_root), args.extension_product)) - if not rows: - fail("manifest would be empty") - - output = repo_path(args.output) - output.parent.mkdir(parents=True, exist_ok=True) - output.write_text("\n".join(rows) + "\n", encoding="utf-8") - print(f"Wrote {len(rows)} Maven artifact publication row(s) to {output.relative_to(ROOT)}") - - -if __name__ == "__main__": - main() diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 2a5e10fa..8065f93a 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -1244,7 +1244,7 @@ def check_kotlin(findings: list[Finding]) -> None: severity="P0", ) for required in [ - "build_maven_artifact_manifest.py", + "build_maven_artifact_manifest.mjs", "publish_liboliphaunt_runtime_maven", "publish_selected_extension_maven", ":oliphaunt-maven-artifacts:publishAndReleaseToMavenCentral", diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index abb5ac90..37134c33 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -782,17 +782,17 @@ def validate_kotlin(kotlin_version: str, liboliphaunt_version: str) -> None: "Kotlin Maven release idempotency probes must not hard-code package coordinates", ) require_text( - "tools/release/build_maven_artifact_manifest.py", - 'product_metadata.registry_package_names("liboliphaunt-native", "maven")', + "tools/release/build_maven_artifact_manifest.mjs", + 'registryPackageNames("liboliphaunt-native", "maven")', "Native runtime Maven artifact manifests must derive package coordinates from release metadata", ) require_text( - "tools/release/build_maven_artifact_manifest.py", - 'artifact_targets.artifact_targets(', + "tools/release/build_maven_artifact_manifest.mjs", + "nativeRuntimeArtifactTargets(", "Native runtime Maven artifact manifests must derive release asset filenames from artifact target metadata", ) reject_text( - "tools/release/build_maven_artifact_manifest.py", + "tools/release/build_maven_artifact_manifest.mjs", "RUNTIME_MAVEN_ARTIFACTS", "Native runtime Maven artifact manifests must not duplicate release asset filenames in a static Maven table", ) diff --git a/tools/release/release.py b/tools/release/release.py index 03a5221c..2089d8ad 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -1524,8 +1524,8 @@ def build_maven_artifact_manifest( ) -> Path: output_path = ROOT / "target" / "release" / "maven-artifacts" / f"{name}.tsv" command = [ - "python3", - "tools/release/build_maven_artifact_manifest.py", + "tools/dev/bun.sh", + "tools/release/build_maven_artifact_manifest.mjs", "--output", str(output_path.relative_to(ROOT)), ] From 4a8b43b56011b85d16301a3d039f42842c3b2f9c Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 22:19:09 +0000 Subject: [PATCH 126/137] chore: port swiftpm renderer to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 24 + .../consumer-dx-release-blueprint.md | 2 +- docs/maintainers/release-setup.md | 2 +- src/sdks/swift/tools/check-sdk.sh | 3 +- .../fixtures/consumer-shape/products.json | 4 +- tools/policy/python-entrypoints.allowlist | 1 - tools/release/build-sdk-ci-artifacts.sh | 3 +- tools/release/check_consumer_shape.py | 6 +- tools/release/check_release_metadata.py | 12 +- .../render_swiftpm_release_package.mjs | 656 ++++++++++++++++++ .../release/render_swiftpm_release_package.py | 270 ------- 11 files changed, 696 insertions(+), 287 deletions(-) create mode 100755 tools/release/render_swiftpm_release_package.mjs delete mode 100755 tools/release/render_swiftpm_release_package.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index a6773a35..e0442070 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -269,6 +269,30 @@ until the current-state gates here are checked with fresh local evidence. `python3 tools/policy/check-release-policy.py`, `python3 tools/release/sync_release_pr.py --check`, `tools/release/release.py check`, and `git diff --cached --check`. +- 2026-06-26: SwiftPM release manifest rendering now runs through + `tools/release/render_swiftpm_release_package.mjs` and the pinned Bun + launcher instead of the retired Python entrypoint. The Bun port preserves + release-shaped Apple XCFramework validation, checksum resolution, and + generated `OliphauntICU` resource-tree extraction without adding hidden npm + archive/plist dependencies. Fresh checks passed: + `node --check tools/release/render_swiftpm_release_package.mjs`, + `tools/dev/bun.sh tools/release/render_swiftpm_release_package.mjs --help`, + release-shaped fixture rendering against + `target/swiftpm-renderer-bun-smoke/assets`, + `bash -n src/sdks/swift/tools/check-sdk.sh + tools/release/build-sdk-ci-artifacts.sh`, + `python3 tools/release/check_release_metadata.py`, + `python3 tools/release/check_consumer_shape.py --products-json + '["oliphaunt-swift"]'`, `tools/dev/bun.sh + tools/policy/check-python-entrypoints.mjs`, `bash + tools/policy/check-tooling-stack.sh`, + `python3 tools/release/check_consumer_shape.py`, + `python3 tools/release/check_artifact_targets.py`, + `python3 tools/policy/check-release-policy.py`, + `python3 tools/release/sync_release_pr.py --check`, + `tools/release/release.py check`, `bash tools/policy/check-sdk-parity.sh`, + and `git diff --cached --check`. SwiftPM package-shape itself was not run + in this Linux batch because `swift` is not installed on the host. - 2026-06-26: Coverage orchestration now runs through `tools/coverage/coverage.mjs` and the pinned Bun launcher while keeping the stable wrapper API (`tools/coverage/run-product`, `check-product`, and diff --git a/docs/maintainers/consumer-dx-release-blueprint.md b/docs/maintainers/consumer-dx-release-blueprint.md index abca9aad..6ab94f0e 100644 --- a/docs/maintainers/consumer-dx-release-blueprint.md +++ b/docs/maintainers/consumer-dx-release-blueprint.md @@ -115,7 +115,7 @@ real local package artifacts installed by npm packages. Extend the generated SwiftPM release manifest in: -- `tools/release/render_swiftpm_release_package.py` +- `tools/release/render_swiftpm_release_package.mjs` Generate extension products and checksum-pinned binary targets. Do not use a plugin to add dependencies. diff --git a/docs/maintainers/release-setup.md b/docs/maintainers/release-setup.md index f854749c..e4037a7b 100644 --- a/docs/maintainers/release-setup.md +++ b/docs/maintainers/release-setup.md @@ -334,7 +334,7 @@ The SwiftPM release manifest is generated from the actual `liboliphaunt` release asset checksum: ```bash -tools/release/render_swiftpm_release_package.py \ +tools/dev/bun.sh tools/release/render_swiftpm_release_package.mjs \ --asset-dir target/liboliphaunt/release-assets \ --output target/oliphaunt-swift/Package.release.swift ``` diff --git a/src/sdks/swift/tools/check-sdk.sh b/src/sdks/swift/tools/check-sdk.sh index 7e3b5ca8..9f5d1386 100755 --- a/src/sdks/swift/tools/check-sdk.sh +++ b/src/sdks/swift/tools/check-sdk.sh @@ -107,7 +107,7 @@ check_swiftpm_release_asset_manifest() { exit 1 fi - run python3 tools/release/render_swiftpm_release_package.py \ + run tools/dev/bun.sh tools/release/render_swiftpm_release_package.mjs \ --asset-dir "$asset_dir" \ --asset-base-url "$asset_base_url" \ --output "$release_manifest" \ @@ -127,7 +127,6 @@ check_swiftpm_release_asset_manifest() { } require swift -require python3 require unzip if [ "$mode" = "coverage" ]; then diff --git a/src/shared/fixtures/consumer-shape/products.json b/src/shared/fixtures/consumer-shape/products.json index 2c427446..84fcdd65 100644 --- a/src/shared/fixtures/consumer-shape/products.json +++ b/src/shared/fixtures/consumer-shape/products.json @@ -770,7 +770,7 @@ "files": [ "Package.swift", "src/sdks/swift/README.md", - "tools/release/render_swiftpm_release_package.py", + "tools/release/render_swiftpm_release_package.mjs", "tools/release/publish_swiftpm_source_tag.mjs" ], "requiredText": { @@ -783,7 +783,7 @@ "## Compatibility", "## Quickstart" ], - "tools/release/render_swiftpm_release_package.py": [ + "tools/release/render_swiftpm_release_package.mjs": [ "binaryTarget(", "liboliphaunt-native-v" ], diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index bad822a4..56533b80 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -24,7 +24,6 @@ tools/release/package_liboliphaunt_wasix_cargo_artifacts.py tools/release/product_metadata.py tools/release/release.py tools/release/release_plan.py -tools/release/render_swiftpm_release_package.py tools/release/sync_release_pr.py tools/release/verify_github_release_attestations.py tools/runtime/with-native-runtime-lock.py diff --git a/tools/release/build-sdk-ci-artifacts.sh b/tools/release/build-sdk-ci-artifacts.sh index 990af12f..9ffd30e0 100755 --- a/tools/release/build-sdk-ci-artifacts.sh +++ b/tools/release/build-sdk-ci-artifacts.sh @@ -120,12 +120,13 @@ case "$product" in ;; oliphaunt-swift) require swift + require bun swift_source_archive="$root/target/liboliphaunt-sdk-check/oliphaunt-swift/package-shape/swift-source-archive/Oliphaunt-source.zip" require_file "$swift_source_archive" cp "$swift_source_archive" "$artifact_root/Oliphaunt-source.zip" [ -n "${OLIPHAUNT_SWIFT_RELEASE_ASSET_DIR:-}" ] || fail "oliphaunt-swift package artifacts require OLIPHAUNT_SWIFT_RELEASE_ASSET_DIR" - python3 tools/release/render_swiftpm_release_package.py \ + tools/dev/bun.sh tools/release/render_swiftpm_release_package.mjs \ --asset-dir "$OLIPHAUNT_SWIFT_RELEASE_ASSET_DIR" \ --output "$artifact_root/Package.swift.release" \ --generated-tree "$work_root/swiftpm-release-tree" diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 8065f93a..71ca0cad 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -1107,7 +1107,7 @@ def check_swift(findings: list[Finding]) -> None: f"Package.swift missing {required}", severity="P0", ) - renderer = read_text("tools/release/render_swiftpm_release_package.py") + renderer = read_text("tools/release/render_swiftpm_release_package.mjs") for required in ["binaryTarget(", "checksum", "base Swift package must not require or publish extension files"]: require( findings, @@ -1115,7 +1115,7 @@ def check_swift(findings: list[Finding]) -> None: "swiftpm-release-manifest", required in renderer, "Swift release manifest renderer must checksum-pin the base binary target and keep extensions separate.", - f"tools/release/render_swiftpm_release_package.py missing {required}", + f"tools/release/render_swiftpm_release_package.mjs missing {required}", severity="P0", ) for forbidden in ["extension_rows", "OliphauntExtension"]: @@ -1125,7 +1125,7 @@ def check_swift(findings: list[Finding]) -> None: "swiftpm-release-manifest", forbidden not in renderer, "Swift base release manifest renderer must not synthesize exact-extension products.", - f"tools/release/render_swiftpm_release_package.py still contains {forbidden}", + f"tools/release/render_swiftpm_release_package.mjs still contains {forbidden}", severity="P0", ) swift_tests = read_text("src/sdks/swift/Tests/OliphauntTests/OliphauntTests.swift") diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 37134c33..d157c479 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -510,18 +510,18 @@ def validate_swift(swift_version: str, liboliphaunt_version: str) -> None: "root SwiftPM package must expose the C bridge target from the monorepo root", ) require_text( - "tools/release/render_swiftpm_release_package.py", + "tools/release/render_swiftpm_release_package.mjs", "binaryTarget(", "SwiftPM release manifest renderer must emit a binary liboliphaunt target", ) require_text( - "tools/release/render_swiftpm_release_package.py", + "tools/release/render_swiftpm_release_package.mjs", "liboliphaunt-native-v", "SwiftPM release manifest renderer must use liboliphaunt GitHub release assets", ) require_text( "src/sdks/swift/tools/check-sdk.sh", - "render_swiftpm_release_package.py", + "render_swiftpm_release_package.mjs", "Swift SDK package check must render the public SwiftPM release manifest from release-shaped assets", ) require_text( @@ -541,7 +541,7 @@ def validate_swift(swift_version: str, liboliphaunt_version: str) -> None: ) require_text( "tools/release/build-sdk-ci-artifacts.sh", - "render_swiftpm_release_package.py", + "render_swiftpm_release_package.mjs", "Swift SDK package artifact builder must render the staged public SwiftPM release manifest", ) require_text( @@ -560,11 +560,11 @@ def validate_swift(swift_version: str, liboliphaunt_version: str) -> None: "Swift SDK package artifact builder must not stage the local validation manifest", ) require_text( - "tools/release/render_swiftpm_release_package.py", + "tools/release/render_swiftpm_release_package.mjs", "base Swift package must not require or publish extension files", "SwiftPM release manifest renderer must keep exact extensions out of the base package", ) - renderer = read_text("tools/release/render_swiftpm_release_package.py") + renderer = read_text("tools/release/render_swiftpm_release_package.mjs") for forbidden in ("extension_rows", "dependency_closure", "OliphauntExtension"): if forbidden in renderer: fail(f"SwiftPM release manifest renderer must not synthesize base-package extension products: {forbidden}") diff --git a/tools/release/render_swiftpm_release_package.mjs b/tools/release/render_swiftpm_release_package.mjs new file mode 100755 index 00000000..7a4e3ada --- /dev/null +++ b/tools/release/render_swiftpm_release_package.mjs @@ -0,0 +1,656 @@ +#!/usr/bin/env bun +import { createHash } from "node:crypto"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { gunzipSync, inflateRawSync } from "node:zlib"; + +import { currentVersion } from "./product-version.mjs"; + +const ROOT = path.resolve(import.meta.dir, "../.."); +const REPOSITORY = "f0rr0/oliphaunt"; +const decoder = new TextDecoder(); + +function fail(message) { + console.error(`render_swiftpm_release_package.mjs: ${message}`); + process.exit(1); +} + +async function fileStat(file) { + return fs.stat(file).catch(() => null); +} + +async function isFile(file) { + const stat = await fileStat(file); + return stat?.isFile() === true; +} + +async function sha256(file) { + return createHash("sha256").update(await fs.readFile(file)).digest("hex"); +} + +function checksumFromManifest(text, asset) { + for (const rawLine of text.split(/\r?\n/u)) { + const line = rawLine.trim(); + if (!line) { + continue; + } + const parts = line.split(/\s+/u); + if (parts.length !== 2) { + continue; + } + const [digest, filename] = parts; + if (filename === `./${asset}` || filename === asset) { + return digest; + } + } + return undefined; +} + +function readUInt16LE(buffer, offset) { + if (offset < 0 || offset + 2 > buffer.length) { + throw new Error("truncated ZIP archive"); + } + return buffer.readUInt16LE(offset); +} + +function readUInt32LE(buffer, offset) { + if (offset < 0 || offset + 4 > buffer.length) { + throw new Error("truncated ZIP archive"); + } + return buffer.readUInt32LE(offset); +} + +function requireZipSignature(buffer, offset, signature, label) { + if (readUInt32LE(buffer, offset) !== signature) { + throw new Error(`invalid ZIP ${label}`); + } +} + +function findEndOfCentralDirectory(buffer) { + const minimumOffset = Math.max(0, buffer.length - 65_557); + for (let offset = buffer.length - 22; offset >= minimumOffset; offset -= 1) { + if (readUInt32LE(buffer, offset) === 0x06054b50) { + return offset; + } + } + throw new Error("ZIP end of central directory was not found"); +} + +function validateZipPath(entryName) { + if ( + entryName.length === 0 || + entryName.includes("\0") || + entryName.startsWith("/") || + entryName.includes("\\") + ) { + throw new Error(`unsafe ZIP entry path: ${entryName}`); + } + const parts = []; + for (const rawPart of entryName.split("/")) { + if (rawPart.length === 0 || rawPart === ".") { + continue; + } + if (rawPart === "..") { + throw new Error(`unsafe ZIP entry path: ${entryName}`); + } + parts.push(rawPart); + } + return `${parts.join("/")}${entryName.endsWith("/") ? "/" : ""}`; +} + +async function readZipArchive(file) { + const buffer = await fs.readFile(file); + const eocd = findEndOfCentralDirectory(buffer); + const totalEntries = readUInt16LE(buffer, eocd + 10); + const centralDirectorySize = readUInt32LE(buffer, eocd + 12); + const centralDirectoryOffset = readUInt32LE(buffer, eocd + 16); + if ( + totalEntries === 0xffff || + centralDirectorySize === 0xffffffff || + centralDirectoryOffset === 0xffffffff + ) { + throw new Error("ZIP64 archives are not supported by this release validator"); + } + if (centralDirectoryOffset + centralDirectorySize > buffer.length) { + throw new Error("ZIP central directory is outside archive bounds"); + } + + const entries = new Map(); + let offset = centralDirectoryOffset; + for (let index = 0; index < totalEntries; index += 1) { + requireZipSignature(buffer, offset, 0x02014b50, "central directory header"); + const method = readUInt16LE(buffer, offset + 10); + const compressedSize = readUInt32LE(buffer, offset + 20); + const uncompressedSize = readUInt32LE(buffer, offset + 24); + const nameLength = readUInt16LE(buffer, offset + 28); + const extraLength = readUInt16LE(buffer, offset + 30); + const commentLength = readUInt16LE(buffer, offset + 32); + const localOffset = readUInt32LE(buffer, offset + 42); + const nameStart = offset + 46; + const nameEnd = nameStart + nameLength; + if (nameEnd > buffer.length) { + throw new Error("ZIP entry name is outside archive bounds"); + } + const rawName = decoder.decode(buffer.subarray(nameStart, nameEnd)); + const entryName = validateZipPath(rawName); + if (entryName) { + entries.set(entryName, { + compressedSize, + localOffset, + method, + uncompressedSize, + }); + } + offset = nameEnd + extraLength + commentLength; + } + if (offset !== centralDirectoryOffset + centralDirectorySize) { + throw new Error("ZIP central directory size does not match entries"); + } + + return { + names: new Set(entries.keys()), + read(entryName) { + const entry = entries.get(entryName); + if (!entry) { + return undefined; + } + requireZipSignature(buffer, entry.localOffset, 0x04034b50, "local file header"); + const localNameLength = readUInt16LE(buffer, entry.localOffset + 26); + const localExtraLength = readUInt16LE(buffer, entry.localOffset + 28); + const dataStart = entry.localOffset + 30 + localNameLength + localExtraLength; + const dataEnd = dataStart + entry.compressedSize; + if (dataEnd > buffer.length) { + throw new Error(`ZIP entry ${entryName} data is outside archive bounds`); + } + const compressed = buffer.subarray(dataStart, dataEnd); + const data = + entry.method === 0 + ? compressed + : entry.method === 8 + ? inflateRawSync(compressed) + : undefined; + if (data === undefined) { + throw new Error(`ZIP entry ${entryName} uses unsupported compression method ${entry.method}`); + } + if (data.length !== entry.uncompressedSize) { + throw new Error(`ZIP entry ${entryName} has invalid uncompressed size`); + } + return data; + }, + }; +} + +function xmlDecode(value) { + return value + .replaceAll(""", '"') + .replaceAll("'", "'") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll("&", "&"); +} + +function tokenizeXml(text) { + return Array.from(text.matchAll(/<[^>]+>|[^<]+/gu), (match) => match[0]); +} + +function tagName(token) { + return token + .replace(/^<\//u, "") + .replace(/^$/u, "") + .trim() + .split(/\s+/u)[0]; +} + +class PlistParser { + constructor(text) { + this.tokens = tokenizeXml(text); + this.index = 0; + } + + parse() { + const token = this.nextToken(); + if (!this.isOpening(token, "plist")) { + throw new Error("plist root element is missing"); + } + const value = this.parseValue(); + const closing = this.nextToken(); + if (!this.isClosing(closing, "plist")) { + throw new Error("plist root element is not closed"); + } + return value; + } + + nextToken() { + while (this.index < this.tokens.length) { + const token = this.tokens[this.index]; + this.index += 1; + if (!token.startsWith("<") && token.trim() === "") { + continue; + } + if ( + token.startsWith(""); + } + + isClosing(token, name) { + return token.startsWith(""); + } + + parseValue() { + const token = this.nextToken(); + if (this.isOpening(token, "dict")) { + return this.parseDict(); + } + if (this.isOpening(token, "array")) { + return this.parseArray(); + } + if (this.isOpening(token, "string")) { + return this.parseTextElement("string"); + } + if (this.isSelfClosing(token, "string")) { + return ""; + } + if (this.isOpening(token, "integer")) { + return Number.parseInt(this.parseTextElement("integer"), 10); + } + if (this.isSelfClosing(token, "true")) { + return true; + } + if (this.isSelfClosing(token, "false")) { + return false; + } + throw new Error(`unsupported plist value ${token}`); + } + + parseDict() { + const result = {}; + while (true) { + const token = this.peekToken(); + if (this.isClosing(token, "dict")) { + this.nextToken(); + return result; + } + const keyOpen = this.nextToken(); + if (!this.isOpening(keyOpen, "key")) { + throw new Error(`expected plist dict key, got ${keyOpen}`); + } + const key = this.parseTextElement("key"); + result[key] = this.parseValue(); + } + } + + parseArray() { + const result = []; + while (true) { + const token = this.peekToken(); + if (this.isClosing(token, "array")) { + this.nextToken(); + return result; + } + result.push(this.parseValue()); + } + } + + parseTextElement(name) { + let text = ""; + while (true) { + const token = this.nextToken(); + if (this.isClosing(token, name)) { + return xmlDecode(text); + } + if (token.startsWith("<")) { + throw new Error(`unexpected tag in plist ${name}: ${token}`); + } + text += token; + } + } +} + +function parsePlist(buffer, source) { + const prefix = buffer.subarray(0, 6).toString("utf8"); + if (prefix === "bplist") { + fail(`SwiftPM Apple XCFramework Info.plist must be XML for release validation: ${source}`); + } + try { + return new PlistParser(buffer.toString("utf8")).parse(); + } catch (error) { + fail(`SwiftPM Apple XCFramework Info.plist is invalid in ${source}: ${error.message}`); + } +} + +async function validateAppleXcframeworkAsset(file) { + let archive; + try { + archive = await readZipArchive(file); + } catch (error) { + fail(`SwiftPM Apple XCFramework asset is not a readable zip file: ${file}: ${error.message}`); + } + const infoData = archive.read("liboliphaunt.xcframework/Info.plist"); + if (infoData === undefined) { + fail(`SwiftPM Apple XCFramework asset is missing liboliphaunt.xcframework/Info.plist: ${file}`); + } + const info = parsePlist(infoData, file); + if (info === null || Array.isArray(info) || typeof info !== "object") { + fail(`SwiftPM Apple XCFramework Info.plist must be a plist dictionary in ${file}`); + } + const libraries = info.AvailableLibraries; + if (!Array.isArray(libraries) || libraries.length === 0) { + fail(`SwiftPM Apple XCFramework Info.plist has no AvailableLibraries in ${file}`); + } + + const platforms = new Set(); + for (const library of libraries) { + if (library === null || Array.isArray(library) || typeof library !== "object") { + continue; + } + const platform = library.SupportedPlatform; + const variant = library.SupportedPlatformVariant ?? ""; + const libraryPath = library.LibraryPath; + const identifier = library.LibraryIdentifier; + if ( + typeof platform !== "string" || + typeof libraryPath !== "string" || + typeof identifier !== "string" + ) { + continue; + } + platforms.add(`${platform}\0${typeof variant === "string" ? variant : ""}`); + const candidate = `liboliphaunt.xcframework/${identifier}/${libraryPath}`; + if (!archive.names.has(candidate) && !Array.from(archive.names).some((name) => name.startsWith(`${candidate}/`))) { + fail(`SwiftPM Apple XCFramework is missing declared library ${candidate}`); + } + } + + const required = [ + ["macos", ""], + ["ios", ""], + ["ios", "simulator"], + ]; + const missing = required.filter(([platform, variant]) => !platforms.has(`${platform}\0${variant}`)); + if (missing.length > 0) { + const rendered = missing + .map(([platform, variant]) => `${platform}${variant ? `-${variant}` : ""}`) + .sort() + .join(", "); + fail(`SwiftPM Apple XCFramework asset ${file} is missing required slice(s): ${rendered}`); + } +} + +function parseTarString(buffer, start, length) { + const end = buffer.indexOf(0, start); + return buffer + .subarray(start, end >= start && end < start + length ? end : start + length) + .toString("utf8") + .trim(); +} + +function parseTarOctal(buffer, start, length) { + const text = parseTarString(buffer, start, length).replaceAll("\0", "").trim(); + return text ? Number.parseInt(text, 8) : 0; +} + +function safeIcuRelativePath(memberName) { + const trimmed = memberName.replace(/^\.\//u, "").replace(/\/+$/u, ""); + if (trimmed === "share/icu" || !trimmed.startsWith("share/icu/")) { + return undefined; + } + const relative = trimmed.slice("share/icu/".length); + const parts = relative.split("/"); + if ( + relative.length === 0 || + path.posix.isAbsolute(relative) || + parts.some((part) => part.length === 0 || part === "." || part === "..") + ) { + fail(`SwiftPM ICU data asset contains unsafe path: ${memberName}`); + } + return relative; +} + +async function prepareIcuResourceTree(assetDir, version, generatedTree) { + if (generatedTree === undefined) { + return; + } + const archivePath = path.join(assetDir, `liboliphaunt-${version}-icu-data.tar.gz`); + if (!(await isFile(archivePath))) { + fail(`SwiftPM ICU resource product requires local ICU data asset: ${archivePath}`); + } + const target = path.join(generatedTree, "generated/swiftpm/OliphauntICU"); + await fs.rm(target, { recursive: true, force: true }); + await fs.mkdir(path.join(target, "share/icu"), { recursive: true }); + + let copied = 0; + let buffer; + try { + buffer = gunzipSync(await fs.readFile(archivePath)); + } catch (error) { + fail(`SwiftPM ICU data asset is not a readable tar archive: ${archivePath}: ${error.message}`); + } + + for (let offset = 0; offset + 512 <= buffer.length; ) { + const header = buffer.subarray(offset, offset + 512); + if (header.every((byte) => byte === 0)) { + break; + } + const name = parseTarString(header, 0, 100); + const prefix = parseTarString(header, 345, 155); + const fullName = prefix ? `${prefix}/${name}` : name; + const size = parseTarOctal(header, 124, 12); + const type = header.subarray(156, 157).toString("utf8"); + const dataStart = offset + 512; + const dataEnd = dataStart + size; + if (dataEnd > buffer.length) { + fail(`SwiftPM ICU data asset member is truncated: ${fullName}`); + } + + const relative = safeIcuRelativePath(fullName); + if (relative !== undefined) { + const destination = path.join(target, "share/icu", ...relative.split("/")); + if (type === "5") { + await fs.mkdir(destination, { recursive: true }); + } else if (type === "" || type === "0" || type === "\0") { + await fs.mkdir(path.dirname(destination), { recursive: true }); + await fs.writeFile(destination, buffer.subarray(dataStart, dataEnd)); + copied += 1; + } else { + fail(`SwiftPM ICU data asset member must be a regular file: ${fullName}`); + } + } + offset += 512 + Math.ceil(size / 512) * 512; + } + + const icuEntries = await fs.readdir(path.join(target, "share/icu")).catch(() => []); + if (copied === 0 || !icuEntries.some((name) => name.startsWith("icudt"))) { + fail(`SwiftPM ICU resource product did not extract ICU icudt data from ${archivePath}`); + } + await fs.writeFile( + path.join(target, "OliphauntICU.swift"), + "public enum OliphauntICUResources {\n public static let bundled = true\n}\n", + "utf8", + ); +} + +async function fetchText(url) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 20_000); + try { + const response = await fetch(url, { signal: controller.signal }); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + return await response.text(); + } finally { + clearTimeout(timeout); + } +} + +async function resolveChecksum(assetDir, assetBaseUrl, asset, version) { + const localAsset = path.join(assetDir, asset); + const localAssetStat = await fileStat(localAsset); + if (localAssetStat?.isFile()) { + if (localAssetStat.size <= 0) { + fail(`SwiftPM Apple XCFramework asset is empty: ${localAsset}`); + } + await validateAppleXcframeworkAsset(localAsset); + return sha256(localAsset); + } + + const localManifest = path.join(assetDir, `liboliphaunt-${version}-release-assets.sha256`); + if (await isFile(localManifest)) { + const checksum = checksumFromManifest(await fs.readFile(localManifest, "utf8"), asset); + if (checksum) { + return checksum; + } + } + + const manifestUrl = `${assetBaseUrl.replace(/\/+$/u, "")}/liboliphaunt-${version}-release-assets.sha256`; + let text; + try { + text = await fetchText(manifestUrl); + } catch (error) { + fail( + `SwiftPM asset ${asset} is not present in ${assetDir}, and checksum ` + + `manifest could not be read from ${manifestUrl}: ${error.message}`, + ); + } + const checksum = checksumFromManifest(text, asset); + if (!checksum) { + fail(`checksum manifest ${manifestUrl} does not contain ${asset}`); + } + return checksum; +} + +function renderManifest(assetBaseUrl, liboliphauntVersion, checksum) { + const asset = `liboliphaunt-${liboliphauntVersion}-apple-spm-xcframework.zip`; + const url = `${assetBaseUrl.replace(/\/+$/u, "")}/${asset}`; + return `// swift-tools-version: 6.0 + +import PackageDescription + +// Generated by tools/release/render_swiftpm_release_package.mjs. +// This is the public SwiftPM release manifest. The source package under +// src/sdks/swift remains the local development package. +// Exact PostgreSQL extensions are released as separate opt-in extension +// artifacts. The base Swift package must not require or publish extension files. +let package = Package( + name: "Oliphaunt", + platforms: [ + .iOS(.v17), + .macOS(.v14) + ], + products: [ + .library(name: "Oliphaunt", targets: ["Oliphaunt"]), + .library(name: "OliphauntICU", targets: ["OliphauntICU"]) + ], + targets: [ + .binaryTarget( + name: "liboliphaunt", + url: "${url}", + checksum: "${checksum}" + ), + .target( + name: "COliphaunt", + dependencies: ["liboliphaunt"], + path: "src/sdks/swift/Sources/COliphaunt", + publicHeadersPath: "include" + ), + .target( + name: "Oliphaunt", + dependencies: ["COliphaunt"], + path: "src/sdks/swift/Sources/Oliphaunt" + ), + .target( + name: "OliphauntICU", + path: "generated/swiftpm/OliphauntICU", + resources: [.copy("share")] + ) + ] +) +`; +} + +function parseArgs(argv) { + const usage = + "usage: tools/release/render_swiftpm_release_package.mjs [--asset-dir DIR] [--asset-base-url URL] [--output FILE] [--generated-tree DIR]"; + if (argv.length === 1 && (argv[0] === "--help" || argv[0] === "-h")) { + console.log(usage); + process.exit(0); + } + const args = {}; + for (let index = 0; index < argv.length; index += 1) { + let arg = argv[index]; + if (!arg.startsWith("--")) { + fail(usage); + } + let value; + const equals = arg.indexOf("="); + if (equals >= 0) { + value = arg.slice(equals + 1); + arg = arg.slice(0, equals); + } else { + value = argv[index + 1]; + if (value === undefined || value.startsWith("--")) { + fail(`${arg} requires a value`); + } + index += 1; + } + if (!["--asset-dir", "--asset-base-url", "--output", "--generated-tree"].includes(arg)) { + fail(`unknown argument ${arg}`); + } + args[arg.slice(2)] = value; + } + return { + assetBaseUrl: args["asset-base-url"], + assetDir: args["asset-dir"] ?? "target/liboliphaunt/release-assets", + generatedTree: args["generated-tree"], + output: args.output, + }; +} + +async function main(argv) { + const args = parseArgs(argv); + const liboliphauntVersion = await currentVersion("liboliphaunt-native"); + const assetDir = path.resolve(ROOT, args.assetDir); + const asset = `liboliphaunt-${liboliphauntVersion}-apple-spm-xcframework.zip`; + const assetBaseUrl = + args.assetBaseUrl ?? + `https://github.com/${REPOSITORY}/releases/download/liboliphaunt-native-v${liboliphauntVersion}`; + const checksum = await resolveChecksum(assetDir, assetBaseUrl, asset, liboliphauntVersion); + const generatedTree = args.generatedTree ? path.resolve(ROOT, args.generatedTree) : undefined; + if (generatedTree !== undefined) { + await fs.mkdir(generatedTree, { recursive: true }); + } + await prepareIcuResourceTree(assetDir, liboliphauntVersion, generatedTree); + const manifest = renderManifest(assetBaseUrl, liboliphauntVersion, checksum); + if (args.output) { + const output = path.resolve(ROOT, args.output); + await fs.mkdir(path.dirname(output), { recursive: true }); + await fs.writeFile(output, manifest, "utf8"); + } else { + process.stdout.write(manifest); + } +} + +await main(Bun.argv.slice(2)); diff --git a/tools/release/render_swiftpm_release_package.py b/tools/release/render_swiftpm_release_package.py deleted file mode 100755 index e6ccabfc..00000000 --- a/tools/release/render_swiftpm_release_package.py +++ /dev/null @@ -1,270 +0,0 @@ -#!/usr/bin/env python3 -"""Render the public SwiftPM manifest for an Oliphaunt Apple SDK release.""" - -from __future__ import annotations - -import argparse -import hashlib -import plistlib -import shutil -import sys -import tarfile -import urllib.error -import urllib.request -import zipfile -from pathlib import Path -from typing import NoReturn - -import product_metadata - - -ROOT = Path(__file__).resolve().parents[2] -REPOSITORY = "f0rr0/oliphaunt" - - -def fail(message: str) -> NoReturn: - print(f"render_swiftpm_release_package.py: {message}", file=sys.stderr) - raise SystemExit(1) - - -def sha256(path: Path) -> str: - digest = hashlib.sha256() - with path.open("rb") as file: - for chunk in iter(lambda: file.read(1024 * 1024), b""): - digest.update(chunk) - return digest.hexdigest() - - -def checksum_from_manifest(text: str, asset: str) -> str | None: - for raw_line in text.splitlines(): - line = raw_line.strip() - if not line: - continue - parts = line.split() - if len(parts) != 2: - continue - digest, filename = parts - if filename == f"./{asset}" or filename == asset: - return digest - return None - - -def validate_apple_xcframework_asset(path: Path) -> None: - try: - with zipfile.ZipFile(path) as archive: - try: - info_data = archive.read("liboliphaunt.xcframework/Info.plist") - except KeyError: - fail(f"SwiftPM Apple XCFramework asset is missing liboliphaunt.xcframework/Info.plist: {path}") - try: - info = plistlib.loads(info_data) - except Exception as error: - fail(f"SwiftPM Apple XCFramework Info.plist is invalid in {path}: {error}") - if not isinstance(info, dict): - fail(f"SwiftPM Apple XCFramework Info.plist must be a plist dictionary in {path}") - libraries = info.get("AvailableLibraries") - if not isinstance(libraries, list) or not libraries: - fail(f"SwiftPM Apple XCFramework Info.plist has no AvailableLibraries in {path}") - archive_names = set(archive.namelist()) - platforms: set[tuple[str, str]] = set() - for library in libraries: - if not isinstance(library, dict): - continue - platform = library.get("SupportedPlatform") - variant = library.get("SupportedPlatformVariant", "") - library_path = library.get("LibraryPath") - identifier = library.get("LibraryIdentifier") - if not isinstance(platform, str) or not isinstance(library_path, str) or not isinstance(identifier, str): - continue - platforms.add((platform, variant if isinstance(variant, str) else "")) - candidate = f"liboliphaunt.xcframework/{identifier}/{library_path}" - if candidate not in archive_names and not any(name.startswith(f"{candidate}/") for name in archive_names): - fail(f"SwiftPM Apple XCFramework is missing declared library {candidate}") - except zipfile.BadZipFile as error: - fail(f"SwiftPM Apple XCFramework asset is not a readable zip file: {path}: {error}") - - required = {("macos", ""), ("ios", ""), ("ios", "simulator")} - missing = required - platforms - if missing: - rendered = ", ".join(f"{platform}{('-' + variant) if variant else ''}" for platform, variant in sorted(missing)) - fail(f"SwiftPM Apple XCFramework asset {path} is missing required slice(s): {rendered}") - - -def prepare_icu_resource_tree(asset_dir: Path, version: str, generated_tree: Path | None) -> None: - if generated_tree is None: - return - archive_path = asset_dir / f"liboliphaunt-{version}-icu-data.tar.gz" - if not archive_path.is_file(): - fail(f"SwiftPM ICU resource product requires local ICU data asset: {archive_path}") - target = generated_tree / "generated/swiftpm/OliphauntICU" - shutil.rmtree(target, ignore_errors=True) - (target / "share/icu").mkdir(parents=True, exist_ok=True) - try: - with tarfile.open(archive_path, "r:*") as archive: - copied = 0 - for member in archive.getmembers(): - name = member.name.removeprefix("./").rstrip("/") - if name == "share/icu" or not name.startswith("share/icu/"): - continue - relative = Path(name).relative_to("share/icu") - if relative.is_absolute() or ".." in relative.parts: - fail(f"SwiftPM ICU data asset contains unsafe path: {member.name}") - destination = target / "share/icu" / relative - if member.isdir(): - destination.mkdir(parents=True, exist_ok=True) - continue - if not member.isfile(): - fail(f"SwiftPM ICU data asset member must be a regular file: {member.name}") - extracted = archive.extractfile(member) - if extracted is None: - fail(f"SwiftPM ICU data asset member could not be read: {member.name}") - destination.parent.mkdir(parents=True, exist_ok=True) - with extracted: - destination.write_bytes(extracted.read()) - copied += 1 - except tarfile.TarError as error: - fail(f"SwiftPM ICU data asset is not a readable tar archive: {archive_path}: {error}") - if copied == 0 or not any(path.name.startswith("icudt") for path in (target / "share/icu").iterdir()): - fail(f"SwiftPM ICU resource product did not extract ICU icudt data from {archive_path}") - (target / "OliphauntICU.swift").write_text( - "public enum OliphauntICUResources {\n" - " public static let bundled = true\n" - "}\n", - encoding="utf-8", - ) - - -def resolve_checksum(asset_dir: Path, asset_base_url: str, asset: str, version: str) -> str: - local_asset = asset_dir / asset - if local_asset.is_file(): - if local_asset.stat().st_size <= 0: - fail(f"SwiftPM Apple XCFramework asset is empty: {local_asset}") - validate_apple_xcframework_asset(local_asset) - return sha256(local_asset) - - local_manifest = asset_dir / f"liboliphaunt-{version}-release-assets.sha256" - if local_manifest.is_file(): - checksum = checksum_from_manifest(local_manifest.read_text(encoding="utf-8"), asset) - if checksum: - return checksum - - manifest_url = f"{asset_base_url.rstrip('/')}/liboliphaunt-{version}-release-assets.sha256" - try: - with urllib.request.urlopen(manifest_url, timeout=20) as response: - text = response.read().decode("utf-8") - except (OSError, UnicodeDecodeError, urllib.error.URLError) as error: - fail( - f"SwiftPM asset {asset} is not present in {asset_dir}, and checksum " - f"manifest could not be read from {manifest_url}: {error}" - ) - checksum = checksum_from_manifest(text, asset) - if not checksum: - fail(f"checksum manifest {manifest_url} does not contain {asset}") - return checksum - - -def render_manifest( - asset_dir: Path, - asset_base_url: str, - liboliphaunt_version: str, - checksum: str, - generated_tree: Path | None, -) -> str: - asset = f"liboliphaunt-{liboliphaunt_version}-apple-spm-xcframework.zip" - url = f"{asset_base_url.rstrip('/')}/{asset}" - if generated_tree is not None: - generated_tree.mkdir(parents=True, exist_ok=True) - return f"""// swift-tools-version: 6.0 - -import PackageDescription - -// Generated by tools/release/render_swiftpm_release_package.py. -// This is the public SwiftPM release manifest. The source package under -// src/sdks/swift remains the local development package. -// Exact PostgreSQL extensions are released as separate opt-in extension -// artifacts. The base Swift package must not require or publish extension files. -let package = Package( - name: "Oliphaunt", - platforms: [ - .iOS(.v17), - .macOS(.v14) - ], - products: [ - .library(name: "Oliphaunt", targets: ["Oliphaunt"]), - .library(name: "OliphauntICU", targets: ["OliphauntICU"]) - ], - targets: [ - .binaryTarget( - name: "liboliphaunt", - url: "{url}", - checksum: "{checksum}" - ), - .target( - name: "COliphaunt", - dependencies: ["liboliphaunt"], - path: "src/sdks/swift/Sources/COliphaunt", - publicHeadersPath: "include" - ), - .target( - name: "Oliphaunt", - dependencies: ["COliphaunt"], - path: "src/sdks/swift/Sources/Oliphaunt" - ), - .target( - name: "OliphauntICU", - path: "generated/swiftpm/OliphauntICU", - resources: [.copy("share")] - ) - ] -) -""" - - -def parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--asset-dir", - default="target/liboliphaunt/release-assets", - help="directory containing liboliphaunt release assets", - ) - parser.add_argument( - "--asset-base-url", - help="base URL for liboliphaunt release assets; defaults to the GitHub release URL", - ) - parser.add_argument( - "--output", - help="write the rendered manifest here; stdout is used when omitted", - ) - parser.add_argument( - "--generated-tree", - help=( - "create the generated SwiftPM release tree root; exact extension " - "artifacts are released as separate opt-in products" - ), - ) - return parser.parse_args(argv) - - -def main(argv: list[str]) -> int: - args = parse_args(argv) - liboliphaunt_version = product_metadata.read_current_version("liboliphaunt-native") - asset_dir = (ROOT / args.asset_dir).resolve() - asset = f"liboliphaunt-{liboliphaunt_version}-apple-spm-xcframework.zip" - base_url = args.asset_base_url or ( - f"https://github.com/{REPOSITORY}/releases/download/liboliphaunt-native-v{liboliphaunt_version}" - ) - checksum = resolve_checksum(asset_dir, base_url, asset, liboliphaunt_version) - generated_tree = (ROOT / args.generated_tree).resolve() if args.generated_tree else None - prepare_icu_resource_tree(asset_dir, liboliphaunt_version, generated_tree) - manifest = render_manifest(asset_dir, base_url, liboliphaunt_version, checksum, generated_tree) - if args.output: - output = ROOT / args.output - output.parent.mkdir(parents=True, exist_ok=True) - output.write_text(manifest, encoding="utf-8") - else: - print(manifest, end="") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) From a2372a1ce15bc90e523d4fcaa458ec7839c76d12 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 22:24:07 +0000 Subject: [PATCH 127/137] docs: record split tool crate contract --- docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index e0442070..1cd33772 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -984,3 +984,12 @@ until the current-state gates here are checked with fresh local evidence. files, and native modules before opening or launching. Deno keeps its package-managed extension limitation, but explicit prepared runtimes are now proven instead of merely accepted by path. +- On 2026-06-26, the split client-tool crate contract was rechecked against the + implementation: native root/runtime artifacts keep `postgres`, `initdb`, and + `pg_ctl`, native `oliphaunt-tools-*` artifacts keep only `pg_dump` and + `psql`, WASIX root/runtime artifacts keep `postgres` plus `initdb`, and + `oliphaunt-wasix-tools` plus tools-AOT artifacts keep `pg_dump` and `psql` + with no WASIX `pg_ctl`. The focused shape checks passed: + `check_consumer_shape.py` for liboliphaunt native/WASIX/Rust, + `check_artifact_targets.py`, `examples/tools/check-examples.sh`, and + `cargo test -p oliphaunt-build --locked`. From d0fa2718bb59bc7766288261436f1e11777aad0d Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 22:36:59 +0000 Subject: [PATCH 128/137] chore: port release attestation verifier to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 10 + tools/policy/check-release-policy.py | 4 +- tools/policy/python-entrypoints.allowlist | 1 - tools/release/check_artifact_targets.py | 2 +- tools/release/release.py | 2 +- .../verify_github_release_attestations.mjs | 669 ++++++++++++++++++ .../verify_github_release_attestations.py | 110 --- 7 files changed, 683 insertions(+), 115 deletions(-) create mode 100755 tools/release/verify_github_release_attestations.mjs delete mode 100755 tools/release/verify_github_release_attestations.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 1cd33772..fff745a9 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -993,3 +993,13 @@ until the current-state gates here are checked with fresh local evidence. `check_consumer_shape.py` for liboliphaunt native/WASIX/Rust, `check_artifact_targets.py`, `examples/tools/check-examples.sh`, and `cargo test -p oliphaunt-build --locked`. +- On 2026-06-26, the GitHub release attestation verifier moved from Python to + Bun. The new `verify_github_release_attestations.mjs` preserves the + asset-backed product set, exact-extension release manifest handling, pinned + signer workflow/source-ref/runner trust checks, and selected release asset + presence validation before calling `gh attestation verify`. Base product + expected-asset parity was checked against the previous Python asset checker, + and the no-product verify path passed through the pinned Bun launcher. A + subagent audit identified the next reasonable Python migration candidates as + the native runtime lock helper, registry publication check cluster, and native + runtime payload optimizer. diff --git a/tools/policy/check-release-policy.py b/tools/policy/check-release-policy.py index 7b483e8d..f8f11a84 100644 --- a/tools/policy/check-release-policy.py +++ b/tools/policy/check-release-policy.py @@ -962,7 +962,7 @@ def check_release_workflow_policy() -> None: ) assert_contains( "tools/release/release.py", - "tools/release/verify_github_release_attestations.py", + "tools/release/verify_github_release_attestations.mjs", "release.py verify-release must verify GitHub artifact attestations", ) for snippet in ( @@ -973,7 +973,7 @@ def check_release_workflow_policy() -> None: "--deny-self-hosted-runners", ): assert_contains( - "tools/release/verify_github_release_attestations.py", + "tools/release/verify_github_release_attestations.mjs", snippet, "Release attestation verification must pin signer workflow, source ref, and runner trust", ) diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index 56533b80..d3090ecf 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -25,5 +25,4 @@ tools/release/product_metadata.py tools/release/release.py tools/release/release_plan.py tools/release/sync_release_pr.py -tools/release/verify_github_release_attestations.py tools/runtime/with-native-runtime-lock.py diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py index a3c3d72d..eb443b41 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -768,7 +768,7 @@ def validate_ci_release_artifacts() -> None: "GitHub release verification must derive exact-extension asset expectations from staged extension package manifests", ) require_text( - "tools/release/verify_github_release_attestations.py", + "tools/release/verify_github_release_attestations.mjs", "exact-extension-artifact", "Release attestation verification must include exact-extension artifact products", ) diff --git a/tools/release/release.py b/tools/release/release.py index 2089d8ad..6f4cd260 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -1777,7 +1777,7 @@ def consumer_shape_scope_args(args: list[str]) -> list[str]: def command_verify_release(args: list[str]) -> None: run(["tools/release/check_release_versions.py", *args, "--check-registries"]) command_consumer_shape(["--require-ready", *consumer_shape_scope_args(args)]) - run(["tools/release/verify_github_release_attestations.py", *args]) + run(["tools/dev/bun.sh", "tools/release/verify_github_release_attestations.mjs", *args]) def publish_liboliphaunt_github_assets(head_ref: str) -> None: diff --git a/tools/release/verify_github_release_attestations.mjs b/tools/release/verify_github_release_attestations.mjs new file mode 100755 index 00000000..1a977617 --- /dev/null +++ b/tools/release/verify_github_release_attestations.mjs @@ -0,0 +1,669 @@ +#!/usr/bin/env bun +// Verify GitHub artifact attestations for asset-backed product releases. + +import { createHash } from "node:crypto"; +import { spawnSync } from "node:child_process"; +import fs from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; + +import { runMoon } from "../policy/moon.mjs"; +import { expectedAssets as expectedDesktopAssets } from "./release-artifact-targets.mjs"; +import { currentVersion } from "./product-version.mjs"; + +const ROOT = path.resolve(import.meta.dir, "../.."); +const PREFIX = "verify_github_release_attestations.mjs"; +const GITHUB_API = process.env.GITHUB_API ?? "https://api.github.com"; + +const BASE_ASSET_BACKED_PRODUCTS = new Set([ + "liboliphaunt-native", + "liboliphaunt-wasix", + "oliphaunt-broker", + "oliphaunt-node-direct", +]); + +const DESKTOP_TARGETS = new Set([ + "linux-arm64-gnu", + "linux-x64-gnu", + "macos-arm64", + "windows-x64-msvc", +]); + +const PUBLIC_EXTENSION_RELEASE_MANIFEST_KEYS = new Set([ + "schema", + "product", + "version", + "sqlName", + "extensionClass", + "versioning", + "sourceIdentity", + "compatibility", + "dependencies", + "nativeModuleStem", + "sharedPreloadLibraries", + "mobileReleaseReady", + "desktopReleaseReady", + "assets", +]); + +const PUBLIC_EXTENSION_RELEASE_ASSET_KEYS = new Set([ + "name", + "family", + "target", + "kind", + "sha256", + "bytes", +]); + +function fail(message) { + console.error(`${PREFIX}: ${message}`); + process.exit(1); +} + +function rel(file) { + return path.relative(ROOT, file).split(path.sep).join("/"); +} + +async function readJson(file) { + try { + const value = JSON.parse(await fs.readFile(file, "utf8")); + if (value === null || Array.isArray(value) || typeof value !== "object") { + fail(`${rel(file)} must contain a JSON object`); + } + return value; + } catch (error) { + fail(`failed to read ${rel(file)}: ${error.message}`); + } +} + +async function readToml(file) { + try { + const value = Bun.TOML.parse(await fs.readFile(file, "utf8")); + if (value === null || Array.isArray(value) || typeof value !== "object") { + fail(`${rel(file)} must contain a TOML table`); + } + return value; + } catch (error) { + fail(`failed to read ${rel(file)}: ${error.message}`); + } +} + +let releaseConfigCache; +async function releaseConfig() { + releaseConfigCache ??= readJson(path.join(ROOT, "release-please-config.json")); + return releaseConfigCache; +} + +let packagePathsCache; +async function packagePathsByProduct() { + if (packagePathsCache !== undefined) { + return packagePathsCache; + } + const config = await releaseConfig(); + const packages = config.packages; + if (packages === null || Array.isArray(packages) || typeof packages !== "object") { + fail("release-please-config.json must define packages"); + } + const paths = new Map(); + for (const [packagePath, packageConfig] of Object.entries(packages)) { + const component = packageConfig?.component; + if (typeof component !== "string" || component.length === 0) { + fail(`${packagePath}.component must be a non-empty string`); + } + if (paths.has(component)) { + fail(`duplicate release-please component ${component}`); + } + paths.set(component, packagePath); + } + packagePathsCache = paths; + return paths; +} + +async function packagePath(product) { + const paths = await packagePathsByProduct(); + const value = paths.get(product); + if (typeof value !== "string" || value.length === 0) { + fail(`unknown release product ${JSON.stringify(product)}`); + } + return value; +} + +async function productConfig(product) { + const productPath = await packagePath(product); + const metadata = await readToml(path.join(ROOT, productPath, "release.toml")); + if (metadata.id !== product) { + fail(`${productPath}/release.toml must declare id = ${JSON.stringify(product)}`); + } + return metadata; +} + +async function exactExtensionProducts() { + const paths = await packagePathsByProduct(); + const products = []; + for (const product of paths.keys()) { + const config = await productConfig(product); + if (config.kind === "exact-extension-artifact") { + products.push(product); + } + } + return products.sort(compareText); +} + +async function assetBackedProducts() { + return new Set([...BASE_ASSET_BACKED_PRODUCTS, ...(await exactExtensionProducts())]); +} + +function compareText(left, right) { + return left < right ? -1 : left > right ? 1 : 0; +} + +async function tagPrefix(product) { + const config = await releaseConfig(); + if (config["include-v-in-tag"] !== true) { + fail("release-please must include v in product tags"); + } + if (config["tag-separator"] !== "-") { + fail("release-please tag-separator must be '-'"); + } + return `${product}-v`; +} + +async function productTag(product, version) { + return `${await tagPrefix(product)}${version}`; +} + +function repository() { + return process.env.GITHUB_REPOSITORY || "f0rr0/oliphaunt"; +} + +let moonReleaseProductsCache; +function moonReleaseProducts() { + if (moonReleaseProductsCache !== undefined) { + return moonReleaseProductsCache; + } + const value = JSON.parse(runMoon(["query", "projects"])); + if (!Array.isArray(value.projects)) { + fail("moon query projects did not return a projects array"); + } + const products = new Map(); + for (const project of value.projects) { + const id = project?.id; + const tags = project?.config?.tags; + const release = project?.config?.project?.metadata?.release; + if (!Array.isArray(tags) || !tags.includes("release-product")) { + continue; + } + if (typeof id !== "string" || release === null || typeof release !== "object") { + fail("Moon release metadata returned an invalid product row"); + } + if (release.component !== id) { + fail(`Moon release product ${id} release.component must match project id`); + } + products.set(id, release); + } + moonReleaseProductsCache = products; + return products; +} + +function publishedTargets(product, preset) { + const release = moonReleaseProducts().get(product); + if (!release) { + fail(`Moon release metadata does not include ${product}`); + } + const artifactTargets = release.artifactTargets; + if ( + artifactTargets === null || + typeof artifactTargets !== "object" || + artifactTargets.preset !== preset + ) { + fail(`Moon release metadata for ${product} must use artifactTargets preset ${preset}`); + } + const targets = artifactTargets.publishedTargets; + if (!Array.isArray(targets) || !targets.every((target) => typeof target === "string" && target)) { + fail(`Moon release metadata for ${product} must declare publishedTargets`); + } + return [...targets].sort(compareText); +} + +function archiveSuffix(target) { + return target === "windows-x64-msvc" ? "zip" : "tar.gz"; +} + +function liboliphauntNativeAssets(version) { + const targets = publishedTargets("liboliphaunt-native", "liboliphaunt-native"); + const assets = targets.map((target) => `liboliphaunt-${version}-${target}.${archiveSuffix(target)}`); + for (const target of targets.filter((target) => DESKTOP_TARGETS.has(target))) { + assets.push(`oliphaunt-tools-${version}-${target}.${archiveSuffix(target)}`); + } + assets.push( + `liboliphaunt-${version}-apple-spm-xcframework.zip`, + `liboliphaunt-${version}-runtime-resources.tar.gz`, + `liboliphaunt-${version}-icu-data.tar.gz`, + `liboliphaunt-${version}-package-size.tsv`, + `liboliphaunt-${version}-release-assets.sha256`, + ); + return [...new Set(assets)].sort(compareText); +} + +function liboliphauntWasixAssets(version) { + const targets = publishedTargets("liboliphaunt-wasix", "liboliphaunt-wasix"); + if (!targets.includes("portable")) { + fail("Moon release metadata for liboliphaunt-wasix must publish portable"); + } + const assets = [ + `liboliphaunt-wasix-${version}-runtime-portable.tar.zst`, + `liboliphaunt-wasix-${version}-icu-data.tar.zst`, + `liboliphaunt-wasix-${version}-release-assets.sha256`, + ]; + for (const target of targets.filter((target) => target !== "portable")) { + assets.push(`liboliphaunt-wasix-${version}-runtime-aot-${target}.tar.zst`); + } + return assets.sort(compareText); +} + +async function expectedExtensionAssets(product, version) { + const releaseAssetRoot = path.join(ROOT, "target/extension-artifacts", product, "release-assets"); + const manifestPath = path.join(releaseAssetRoot, `${product}-${version}-manifest.json`); + const manifest = await readJson(manifestPath); + validateExtensionManifest(product, version, manifest, manifestPath); + const names = manifest.assets.map((asset) => asset.name); + names.push( + `${product}-${version}-manifest.json`, + `${product}-${version}-manifest.properties`, + `${product}-${version}-release-assets.sha256`, + ); + return [...new Set(names)].sort(compareText); +} + +async function expectedAssets(product, version) { + const config = await productConfig(product); + if (config.kind === "exact-extension-artifact") { + return expectedExtensionAssets(product, version); + } + if (product === "liboliphaunt-native") { + return liboliphauntNativeAssets(version); + } + if (product === "liboliphaunt-wasix") { + return liboliphauntWasixAssets(version); + } + if (product === "oliphaunt-broker") { + return expectedDesktopAssets(product, "broker-helper", version, PREFIX); + } + if (product === "oliphaunt-node-direct") { + return expectedDesktopAssets(product, "node-direct-addon", version, PREFIX); + } + fail(`asset expectation is not defined for ${product}`); +} + +function authHeaders(accept) { + const headers = { + Accept: accept, + "User-Agent": "oliphaunt-release-check", + "X-GitHub-Api-Version": "2022-11-28", + }; + const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN; + if (token) { + headers.Authorization = `Bearer ${token}`; + } + return headers; +} + +async function githubJson(url) { + let response; + try { + response = await fetch(url, { + headers: authHeaders("application/vnd.github+json"), + }); + } catch (error) { + fail(`failed to query GitHub release URL ${url}: ${error.message}`); + } + if (response.status === 404) { + fail(`GitHub release not found for URL ${url}`); + } + if (!response.ok) { + fail(`GitHub API returned HTTP ${response.status} for ${url}`); + } + return response.json(); +} + +async function releaseAssets(repo, tag) { + const repoPath = encodeURIComponent(repo).replaceAll("%2F", "/"); + const tagPath = encodeURIComponent(tag); + const url = `${GITHUB_API.replace(/\/$/u, "")}/repos/${repoPath}/releases/tags/${tagPath}`; + const data = await githubJson(url); + if (data === null || Array.isArray(data) || typeof data !== "object") { + fail(`GitHub release response for ${tag} was not an object`); + } + if (!Array.isArray(data.assets)) { + fail(`GitHub release response for ${tag} did not include assets`); + } + const assets = new Map(); + for (const asset of data.assets) { + if (asset === null || typeof asset !== "object" || typeof asset.name !== "string") { + continue; + } + if (assets.has(asset.name)) { + fail(`GitHub release ${tag} declares duplicate asset ${asset.name}`); + } + assets.set(asset.name, asset); + } + return assets; +} + +async function requestBytes(url, name) { + if (typeof url !== "string" || url.length === 0) { + fail(`GitHub release asset ${name} did not include an API download URL`); + } + let response; + try { + response = await fetch(url, { + headers: authHeaders("application/octet-stream"), + }); + } catch (error) { + fail(`failed to download GitHub asset ${name}: ${error.message}`); + } + if (!response.ok) { + fail(`GitHub asset download returned HTTP ${response.status} for ${name}`); + } + return new Uint8Array(await response.arrayBuffer()); +} + +function sha256Bytes(data) { + return createHash("sha256").update(data).digest("hex"); +} + +function validateKeySet(object, expected, context) { + const actual = new Set(Object.keys(object)); + const missing = [...expected].filter((key) => !actual.has(key)); + const unexpected = [...actual].filter((key) => !expected.has(key)); + if (missing.length > 0 || unexpected.length > 0) { + fail(`${context} keys must be ${JSON.stringify([...expected].sort())}, got ${JSON.stringify([...actual].sort())}`); + } +} + +function validateSha256(value, context) { + if (typeof value !== "string" || !/^[0-9a-f]{64}$/u.test(value)) { + fail(`${context} has invalid sha256 ${JSON.stringify(value)}`); + } +} + +function validateExtensionManifest(product, version, manifest, context) { + if (manifest.schema !== "oliphaunt-extension-release-manifest-v1") { + fail(`${context} schema must be oliphaunt-extension-release-manifest-v1`); + } + if (manifest.product !== product || manifest.version !== version) { + fail(`${context} declares product/version ${manifest.product}@${manifest.version}, expected ${product}@${version}`); + } + validateKeySet(manifest, PUBLIC_EXTENSION_RELEASE_MANIFEST_KEYS, context); + if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) { + fail(`${context} must declare a non-empty assets array`); + } + const seen = new Set(); + for (const [index, asset] of manifest.assets.entries()) { + const assetContext = `${context} assets[${index}]`; + if (asset === null || Array.isArray(asset) || typeof asset !== "object") { + fail(`${assetContext} must be an object`); + } + validateKeySet(asset, PUBLIC_EXTENSION_RELEASE_ASSET_KEYS, assetContext); + for (const key of ["name", "family", "target", "kind", "sha256"]) { + if (typeof asset[key] !== "string" || asset[key].length === 0) { + fail(`${assetContext}.${key} must be a non-empty string`); + } + } + validateSha256(asset.sha256, `${assetContext}.${asset.name}`); + if (!Number.isInteger(asset.bytes) || asset.bytes <= 0) { + fail(`${assetContext}.${asset.name} must declare positive bytes`); + } + if (seen.has(asset.name)) { + fail(`${context} declares duplicate asset ${asset.name}`); + } + seen.add(asset.name); + } +} + +function parseChecksumManifest(data, context) { + const checksums = new Map(); + const text = new TextDecoder().decode(data); + for (const [index, rawLine] of text.split(/\r?\n/u).entries()) { + const line = rawLine.trim(); + if (!line) { + continue; + } + const parts = line.split(/\s+/u); + if (parts.length !== 2) { + fail(`${context}:${index + 1} must contain ' ./'`); + } + const [sha, name] = parts; + validateSha256(sha, `${context}:${index + 1}`); + if (!name.startsWith("./") || name.slice(2).includes("/")) { + fail(`${context}:${index + 1} must reference a direct asset path like ./name`); + } + const assetName = name.slice(2); + if (checksums.has(assetName)) { + fail(`${context} declares duplicate checksum entry for ${assetName}`); + } + checksums.set(assetName, sha); + } + return checksums; +} + +function stableStringify(value) { + if (Array.isArray(value)) { + return `[${value.map(stableStringify).join(",")}]`; + } + if (value !== null && typeof value === "object") { + return `{${Object.keys(value) + .sort(compareText) + .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`) + .join(",")}}`; + } + return JSON.stringify(value); +} + +async function verifyExtensionReleaseAssets(product, version, expectedNames, actualAssets) { + const actualNames = new Set(actualAssets.keys()); + const unexpected = [...actualNames].filter((name) => !expectedNames.has(name)).sort(compareText); + if (unexpected.length > 0) { + fail(`${product} GitHub release ${await productTag(product, version)} has unexpected exact-extension asset(s): ${unexpected.join(", ")}`); + } + + const manifestName = `${product}-${version}-manifest.json`; + const propertiesName = `${product}-${version}-manifest.properties`; + const checksumName = `${product}-${version}-release-assets.sha256`; + const localManifestPath = path.join(ROOT, "target/extension-artifacts", product, "release-assets", manifestName); + const localManifest = await readJson(localManifestPath); + const downloaded = new Map(); + + const manifestBytes = await requestBytes(actualAssets.get(manifestName).url, manifestName); + downloaded.set(manifestName, manifestBytes); + const remoteManifest = JSON.parse(new TextDecoder().decode(manifestBytes)); + if (stableStringify(remoteManifest) !== stableStringify(localManifest)) { + fail(`${product} GitHub release ${await productTag(product, version)} public manifest differs from staged manifest`); + } + validateExtensionManifest(product, version, remoteManifest, `${product} ${version} public extension manifest`); + + const checksumBytes = await requestBytes(actualAssets.get(checksumName).url, checksumName); + downloaded.set(checksumName, checksumBytes); + const checksums = parseChecksumManifest(checksumBytes, checksumName); + const checksumCoveredNames = new Set(remoteManifest.assets.map((asset) => asset.name)); + checksumCoveredNames.add(manifestName); + checksumCoveredNames.add(propertiesName); + if ( + stableStringify([...checksums.keys()].sort(compareText)) !== + stableStringify([...checksumCoveredNames].sort(compareText)) + ) { + fail( + `${product} GitHub release ${await productTag(product, version)} checksum manifest must cover release assets exactly`, + ); + } + + for (const name of [...checksumCoveredNames].sort(compareText)) { + if (!actualAssets.has(name)) { + fail(`${product} GitHub release ${await productTag(product, version)} is missing checksum-covered asset ${name}`); + } + let data = downloaded.get(name); + if (data === undefined) { + data = await requestBytes(actualAssets.get(name).url, name); + downloaded.set(name, data); + } + if (sha256Bytes(data) !== checksums.get(name)) { + fail(`${product} GitHub release ${await productTag(product, version)} asset ${name} checksum mismatch`); + } + const remoteSize = actualAssets.get(name).size; + if (Number.isInteger(remoteSize) && remoteSize !== data.byteLength) { + fail(`${product} GitHub release ${await productTag(product, version)} asset ${name} size mismatch`); + } + } + + for (const asset of remoteManifest.assets) { + const data = downloaded.get(asset.name); + if (data.byteLength !== asset.bytes || sha256Bytes(data) !== asset.sha256) { + fail(`${product} GitHub release ${await productTag(product, version)} asset ${asset.name} public manifest mismatch`); + } + } +} + +async function verifyReleaseAssets(product, version, assets) { + const repo = repository(); + const tag = await productTag(product, version); + const actualAssets = await releaseAssets(repo, tag); + const expectedNames = new Set(assets); + const missing = [...expectedNames].filter((name) => !actualAssets.has(name)).sort(compareText); + if (missing.length > 0) { + fail(`${product} GitHub release ${tag} is missing required asset(s): ${missing.join(", ")}`); + } + const config = await productConfig(product); + if (config.kind === "exact-extension-artifact") { + await verifyExtensionReleaseAssets(product, version, expectedNames, actualAssets); + } + console.log(`${product} GitHub release assets verified for ${tag}: ${assets.join(", ")}`); +} + +function run(args, options = {}) { + console.log(`\n==> ${args.join(" ")}`); + const result = spawnSync(args[0], args.slice(1), { + cwd: ROOT, + stdio: "inherit", + ...options, + }); + if (result.error) { + fail(`${args[0]} failed to start: ${result.error.message}`); + } + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + +function parseArgs(argv) { + const args = { product: [], productsJson: undefined }; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === "--product") { + const product = argv[++index]; + if (!product) { + fail("--product requires a value"); + } + args.product.push(product); + } else if (value.startsWith("--product=")) { + args.product.push(value.slice("--product=".length)); + } else if (value === "--products-json") { + args.productsJson = argv[++index]; + if (args.productsJson === undefined) { + fail("--products-json requires a value"); + } + } else if (value.startsWith("--products-json=")) { + args.productsJson = value.slice("--products-json=".length); + } else if (value === "--head-ref") { + index += 1; + } else if (value.startsWith("--head-ref=")) { + continue; + } else if (value === "--help" || value === "-h") { + console.log("usage: tools/release/verify_github_release_attestations.mjs [--product ID...] [--products-json JSON] [--head-ref REF]"); + process.exit(0); + } else { + fail(`unknown argument ${value}`); + } + } + return args; +} + +async function parseProducts(value) { + const backed = await assetBackedProducts(); + if (!value) { + return [...backed].sort(compareText); + } + let parsed; + try { + parsed = JSON.parse(value); + } catch (error) { + fail(`--products-json must be valid JSON: ${error.message}`); + } + if (!Array.isArray(parsed) || !parsed.every((item) => typeof item === "string")) { + fail("--products-json must be a JSON string array"); + } + return parsed.filter((product) => backed.has(product)); +} + +function requireGh() { + const result = spawnSync("gh", ["--version"], { stdio: "ignore" }); + if (result.error || result.status !== 0) { + fail("gh CLI is required to verify GitHub release attestations"); + } +} + +async function verifyProduct(product, destination) { + const version = await currentVersion(product); + const tag = await productTag(product, version); + const repo = repository(); + const signerWorkflow = `${repo}/.github/workflows/release.yml`; + const assets = await expectedAssets(product, version); + await verifyReleaseAssets(product, version, assets); + const productDir = path.join(destination, product); + await fs.mkdir(productDir, { recursive: true }); + for (const asset of assets) { + run(["gh", "release", "download", tag, "--repo", repo, "--pattern", asset, "--dir", productDir]); + run([ + "gh", + "attestation", + "verify", + path.join(productDir, asset), + "--repo", + repo, + "--signer-workflow", + signerWorkflow, + "--source-ref", + "refs/heads/main", + "--deny-self-hosted-runners", + ]); + } + console.log(`${product} GitHub release attestations verified for ${tag}`); +} + +export { assetBackedProducts, expectedAssets, productTag }; + +async function main(argv) { + const args = parseArgs(argv); + requireGh(); + const products = args.product.length > 0 ? args.product : await parseProducts(args.productsJson); + const backed = await assetBackedProducts(); + const unknown = products.filter((product) => !backed.has(product)).sort(compareText); + if (unknown.length > 0) { + fail(`attestation verification is only defined for asset-backed products: ${unknown.join(", ")}`); + } + if (products.length === 0) { + console.log("no asset-backed products selected; GitHub attestation verification skipped"); + return; + } + const destination = await fs.mkdtemp(path.join(tmpdir(), "oliphaunt-release-attestations.")); + try { + for (const product of products) { + await verifyProduct(product, destination); + } + } finally { + await fs.rm(destination, { recursive: true, force: true }); + } +} + +if (import.meta.main) { + await main(Bun.argv.slice(2)); +} diff --git a/tools/release/verify_github_release_attestations.py b/tools/release/verify_github_release_attestations.py deleted file mode 100755 index ae9a3582..00000000 --- a/tools/release/verify_github_release_attestations.py +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env python3 -"""Verify GitHub artifact attestations for asset-backed product releases.""" - -from __future__ import annotations - -import argparse -import json -import shutil -import subprocess -import sys -import tempfile -from pathlib import Path -from typing import NoReturn - -import check_github_release_assets -import product_metadata - - -BASE_ASSET_BACKED_PRODUCTS = { - "liboliphaunt-native", - "liboliphaunt-wasix", - "oliphaunt-broker", - "oliphaunt-node-direct", -} - - -def asset_backed_products() -> set[str]: - products = set(BASE_ASSET_BACKED_PRODUCTS) - for product in product_metadata.product_ids(): - if product_metadata.product_config(product).get("kind") == "exact-extension-artifact": - products.add(product) - return products - - -def fail(message: str) -> NoReturn: - print(f"verify_github_release_attestations.py: {message}", file=sys.stderr) - raise SystemExit(1) - - -def parse_products(value: str | None) -> list[str]: - if not value: - return sorted(asset_backed_products()) - parsed = json.loads(value) - if not isinstance(parsed, list) or not all(isinstance(item, str) for item in parsed): - fail("--products-json must be a JSON string array") - return [product for product in parsed if product in asset_backed_products()] - - -def run(args: list[str], *, cwd: Path | None = None) -> None: - print("\n==> " + " ".join(args), flush=True) - subprocess.run(args, cwd=cwd, check=True) - - -def verify_product(product: str, destination: Path) -> None: - version = product_metadata.read_current_version(product) - tag = check_github_release_assets.product_tag(product, version) - repo = check_github_release_assets.repository() - signer_workflow = f"{repo}/.github/workflows/release.yml" - assets = check_github_release_assets.expected_assets(product, version) - check_github_release_assets.verify(product, version, assets) - product_dir = destination / product - product_dir.mkdir(parents=True, exist_ok=True) - for asset in assets: - run(["gh", "release", "download", tag, "--repo", repo, "--pattern", asset, "--dir", str(product_dir)]) - run( - [ - "gh", - "attestation", - "verify", - str(product_dir / asset), - "--repo", - repo, - "--signer-workflow", - signer_workflow, - "--source-ref", - "refs/heads/main", - "--deny-self-hosted-runners", - ] - ) - print(f"{product} GitHub release attestations verified for {tag}") - - -def parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--product", action="append", default=[], help="product id to verify") - parser.add_argument("--products-json", help="JSON product id array from the release plan") - parser.add_argument("--head-ref", help="accepted for release.py passthrough; not used") - return parser.parse_args(argv) - - -def main(argv: list[str]) -> int: - args = parse_args(argv) - if shutil.which("gh") is None: - fail("gh CLI is required to verify GitHub release attestations") - products = args.product or parse_products(args.products_json) - unknown = sorted(set(products) - asset_backed_products()) - if unknown: - fail("attestation verification is only defined for asset-backed products: " + ", ".join(unknown)) - if not products: - print("no asset-backed products selected; GitHub attestation verification skipped") - return 0 - with tempfile.TemporaryDirectory(prefix="oliphaunt-release-attestations.") as tmp: - destination = Path(tmp) - for product in products: - verify_product(product, destination) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) From 489580d9afd4bb7cf388c3a881abfc909fae93e3 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 22:45:28 +0000 Subject: [PATCH 129/137] chore: port native runtime lock to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 8 + .../liboliphaunt/native/tools/check-track.sh | 2 +- src/sdks/rust/tools/check-sdk.sh | 2 +- tools/perf/check-native-perf-harness.sh | 8 +- tools/policy/python-entrypoints.allowlist | 1 - tools/runtime/with-native-runtime-lock.mjs | 240 ++++++++++++++++++ tools/runtime/with-native-runtime-lock.py | 161 ------------ 7 files changed, 254 insertions(+), 168 deletions(-) create mode 100644 tools/runtime/with-native-runtime-lock.mjs delete mode 100755 tools/runtime/with-native-runtime-lock.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index fff745a9..b2e1bfcf 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -1003,3 +1003,11 @@ until the current-state gates here are checked with fresh local evidence. subagent audit identified the next reasonable Python migration candidates as the native runtime lock helper, registry publication check cluster, and native runtime payload optimizer. +- On 2026-06-26, the shared native runtime test lock moved from Python to Bun. + `with-native-runtime-lock.mjs` keeps the same command-line shape, + `OLIPHAUNT_NATIVE_RUNTIME_LOCK_FILE`, and + `OLIPHAUNT_NATIVE_RUNTIME_LOCK_TIMEOUT_SECONDS` controls while using an + atomic lock directory plus owner metadata for cross-process serialization and + stale-owner recovery. Direct smokes covered successful command execution, + metadata materialization, contention timeout exit `124`, stale lock cleanup, + invalid timeout handling, and usage errors. diff --git a/src/runtimes/liboliphaunt/native/tools/check-track.sh b/src/runtimes/liboliphaunt/native/tools/check-track.sh index 5cf56f86..d95f1083 100755 --- a/src/runtimes/liboliphaunt/native/tools/check-track.sh +++ b/src/runtimes/liboliphaunt/native/tools/check-track.sh @@ -24,7 +24,7 @@ run() { } native_runtime_lock() { - run tools/runtime/with-native-runtime-lock.py "$@" + run tools/dev/bun.sh tools/runtime/with-native-runtime-lock.mjs "$@" } require() { diff --git a/src/sdks/rust/tools/check-sdk.sh b/src/sdks/rust/tools/check-sdk.sh index bf786dd9..cd7ae46f 100755 --- a/src/sdks/rust/tools/check-sdk.sh +++ b/src/sdks/rust/tools/check-sdk.sh @@ -31,7 +31,7 @@ run() { } native_runtime_lock() { - run tools/runtime/with-native-runtime-lock.py "$@" + run tools/dev/bun.sh tools/runtime/with-native-runtime-lock.mjs "$@" } run_artifact_relay_build_script_tests() { diff --git a/tools/perf/check-native-perf-harness.sh b/tools/perf/check-native-perf-harness.sh index dc4ef225..d0647edc 100755 --- a/tools/perf/check-native-perf-harness.sh +++ b/tools/perf/check-native-perf-harness.sh @@ -1004,10 +1004,10 @@ require_text '--print-required-extension-artifacts' tools/runtime/preflight.sh \ "shared runtime preflight must use the native build script's complete extension artifact inventory" require_text 'oliphaunt_runtime_native_host_extensions_ready()' tools/runtime/preflight.sh \ "shared runtime preflight must treat native extension artifacts as part of runtime readiness" -require_text 'fcntl.flock' tools/runtime/with-native-runtime-lock.py \ - "shared native runtime probes must use an OS-level lock instead of ad hoc task-ordering" -require_text 'msvcrt.locking' tools/runtime/with-native-runtime-lock.py \ - "shared native runtime probes must use an OS-level lock on Windows runners" +require_text 'await fs.mkdir(lockDir)' tools/runtime/with-native-runtime-lock.mjs \ + "shared native runtime probes must use an atomic cross-process lock instead of ad hoc task-ordering" +require_text 'removeStaleLock' tools/runtime/with-native-runtime-lock.mjs \ + "shared native runtime probes must recover stale lock owners after interrupted runs" require_text 'native_runtime_lock cargo test -p oliphaunt --locked \' src/runtimes/liboliphaunt/native/tools/check-track.sh \ "liboliphaunt native Rust probes must be serialized across parallel Moon release lanes" require_text 'native_runtime_lock node src/runtimes/liboliphaunt/native/tools/run-host-c-smoke.mjs' src/runtimes/liboliphaunt/native/tools/check-track.sh \ diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index d3090ecf..50f272da 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -25,4 +25,3 @@ tools/release/product_metadata.py tools/release/release.py tools/release/release_plan.py tools/release/sync_release_pr.py -tools/runtime/with-native-runtime-lock.py diff --git a/tools/runtime/with-native-runtime-lock.mjs b/tools/runtime/with-native-runtime-lock.mjs new file mode 100644 index 00000000..2819541b --- /dev/null +++ b/tools/runtime/with-native-runtime-lock.mjs @@ -0,0 +1,240 @@ +#!/usr/bin/env bun +// Run a command while holding the shared native runtime test lock. + +import { spawn, spawnSync } from "node:child_process"; +import { writeFileSync } from "node:fs"; +import fs from "node:fs/promises"; +import path from "node:path"; + +const DEFAULT_TIMEOUT_SECONDS = 30 * 60; +const NOTICE_INTERVAL_MS = 30 * 1000; +const POLL_INTERVAL_MS = 250; +const OWNER_WRITE_GRACE_MS = 5 * 1000; +const SIGNAL_EXIT_CODES = { + SIGHUP: 129, + SIGINT: 130, + SIGTERM: 143, +}; + +function fail(message, code = 1) { + console.error(message); + process.exit(code); +} + +function repoRoot() { + const result = spawnSync("git", ["rev-parse", "--show-toplevel"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); + if (result.status !== 0 || result.error) { + return process.cwd(); + } + return result.stdout.trim() || process.cwd(); +} + +function lockPath() { + if (process.env.OLIPHAUNT_NATIVE_RUNTIME_LOCK_FILE) { + return path.resolve(process.env.OLIPHAUNT_NATIVE_RUNTIME_LOCK_FILE); + } + return path.join(repoRoot(), "target/oliphaunt-runtime-locks/native-runtime-tests.lock"); +} + +function timeoutSeconds() { + const configured = process.env.OLIPHAUNT_NATIVE_RUNTIME_LOCK_TIMEOUT_SECONDS; + if (!configured) { + return DEFAULT_TIMEOUT_SECONDS; + } + const timeout = Number(configured); + if (!Number.isFinite(timeout)) { + fail("OLIPHAUNT_NATIVE_RUNTIME_LOCK_TIMEOUT_SECONDS must be a number", 2); + } + if (timeout <= 0) { + fail("OLIPHAUNT_NATIVE_RUNTIME_LOCK_TIMEOUT_SECONDS must be greater than zero", 2); + } + return timeout; +} + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function metadata(command, ownerPid = process.pid) { + const lines = [ + `pid=${ownerPid}`, + `wrapper_pid=${process.pid}`, + `cwd=${process.cwd()}`, + `started_at_unix=${Math.floor(Date.now() / 1000)}`, + `command=${command.join(" ")}`, + ]; + if (ownerPid !== process.pid) { + lines.push(`owner=child`); + } + lines.push(""); + return lines.join("\n"); +} + +async function readOwner(lockDir) { + try { + const text = await fs.readFile(path.join(lockDir, "owner"), "utf8"); + const parsed = new Map(); + for (const rawLine of text.split(/\r?\n/u)) { + const index = rawLine.indexOf("="); + if (index > 0) { + parsed.set(rawLine.slice(0, index), rawLine.slice(index + 1)); + } + } + return { text, pid: Number(parsed.get("pid")) }; + } catch { + return null; + } +} + +function processAlive(pid) { + if (!Number.isInteger(pid) || pid <= 0) { + return false; + } + try { + process.kill(pid, 0); + return true; + } catch (error) { + return error?.code === "EPERM"; + } +} + +async function removeStaleLock(lockDir, lockFile) { + const owner = await readOwner(lockDir); + if (owner?.pid && processAlive(owner.pid)) { + return false; + } + if (owner === null) { + const stat = await fs.stat(lockDir).catch(() => null); + if (stat && Date.now() - stat.mtimeMs < OWNER_WRITE_GRACE_MS) { + return false; + } + } + await fs.rm(lockDir, { recursive: true, force: true }); + const label = owner?.text?.trim() ? ` stale owner: ${owner.text.trim().replace(/\n/g, "; ")}` : ""; + console.error(`removed stale native runtime test lock: ${lockFile}${label}`); + return true; +} + +async function acquireLock(lockFile, command, timeout) { + const lockDir = `${lockFile}.lockdir`; + await fs.mkdir(path.dirname(lockFile), { recursive: true }); + + const deadline = Date.now() + timeout * 1000; + let lastNotice = 0; + const lockMetadata = metadata(command); + + for (;;) { + try { + await fs.mkdir(lockDir); + await fs.writeFile(path.join(lockDir, "owner"), lockMetadata, "utf8"); + await fs.writeFile(lockFile, lockMetadata, "utf8"); + return { lockDir, lockFile }; + } catch (error) { + if (error?.code !== "EEXIST") { + throw error; + } + await removeStaleLock(lockDir, lockFile); + const now = Date.now(); + if (now >= deadline) { + throw new Error(`timed out waiting for native runtime test lock after ${timeout.toFixed(0)}s: ${lockFile}`); + } + if (now - lastNotice >= NOTICE_INTERVAL_MS) { + console.error(`waiting for native runtime test lock: ${lockFile}`); + lastNotice = now; + } + await sleep(POLL_INTERVAL_MS); + } + } +} + +async function releaseLock(lock) { + await fs.rm(lock.lockDir, { recursive: true, force: true }); +} + +function writeLockMetadata(lock, command, ownerPid) { + const text = metadata(command, ownerPid); + writeFileSync(path.join(lock.lockDir, "owner"), text, "utf8"); + writeFileSync(lock.lockFile, text, "utf8"); +} + +function signalExitCode(signal) { + return SIGNAL_EXIT_CODES[signal] ?? 1; +} + +async function runCommand(command, lock) { + return await new Promise((resolve) => { + const child = spawn(command[0], command.slice(1), { + cwd: process.cwd(), + env: process.env, + stdio: "inherit", + }); + let releasing = false; + const cleanupAndExit = async (signal) => { + if (releasing) { + return; + } + releasing = true; + child.kill(signal); + await releaseLock(lock); + resolve(signalExitCode(signal)); + }; + for (const signal of ["SIGHUP", "SIGINT", "SIGTERM"]) { + process.once(signal, () => { + cleanupAndExit(signal).catch((error) => { + console.error(`failed to release native runtime test lock: ${error.message}`); + resolve(signalExitCode(signal)); + }); + }); + } + child.on("error", async (error) => { + if (releasing) { + return; + } + releasing = true; + console.error(`failed to start command ${command[0]}: ${error.message}`); + await releaseLock(lock); + resolve(127); + }); + child.on("close", async (code, signal) => { + if (releasing) { + return; + } + releasing = true; + await releaseLock(lock); + resolve(signal ? signalExitCode(signal) : (code ?? 1)); + }); + if (child.pid) { + try { + writeLockMetadata(lock, command, child.pid); + } catch (error) { + console.error(`failed to update native runtime test lock metadata: ${error.message}`); + } + } + }); +} + +async function main(argv) { + if (argv.length < 1) { + console.error("usage: tools/runtime/with-native-runtime-lock.mjs [args...]"); + return 2; + } + const lockFile = lockPath(); + let lock; + try { + lock = await acquireLock(lockFile, argv, timeoutSeconds()); + } catch (error) { + if (error?.message?.startsWith("timed out waiting for native runtime test lock")) { + console.error(error.message); + return 124; + } + throw error; + } + return runCommand(argv, lock); +} + +if (import.meta.main) { + process.exit(await main(Bun.argv.slice(2))); +} diff --git a/tools/runtime/with-native-runtime-lock.py b/tools/runtime/with-native-runtime-lock.py deleted file mode 100755 index a561b79a..00000000 --- a/tools/runtime/with-native-runtime-lock.py +++ /dev/null @@ -1,161 +0,0 @@ -#!/usr/bin/env python3 -"""Run a command while holding the shared native runtime test lock.""" - -from __future__ import annotations - -import errno -import os -from pathlib import Path -import subprocess -import sys -import time - -if os.name == "nt": - import msvcrt -else: - import fcntl - - -DEFAULT_TIMEOUT_SECONDS = 30 * 60 - - -def repo_root() -> Path: - try: - output = subprocess.check_output( - ["git", "rev-parse", "--show-toplevel"], - stderr=subprocess.DEVNULL, - text=True, - ) - except (OSError, subprocess.CalledProcessError): - return Path.cwd() - return Path(output.strip()) - - -def lock_path() -> Path: - configured = os.environ.get("OLIPHAUNT_NATIVE_RUNTIME_LOCK_FILE") - if configured: - return Path(configured) - return repo_root() / "target" / "oliphaunt-runtime-locks" / "native-runtime-tests.lock" - - -def timeout_seconds() -> float: - configured = os.environ.get("OLIPHAUNT_NATIVE_RUNTIME_LOCK_TIMEOUT_SECONDS") - if not configured: - return float(DEFAULT_TIMEOUT_SECONDS) - try: - timeout = float(configured) - except ValueError: - raise SystemExit( - "OLIPHAUNT_NATIVE_RUNTIME_LOCK_TIMEOUT_SECONDS must be a number" - ) from None - if timeout <= 0: - raise SystemExit( - "OLIPHAUNT_NATIVE_RUNTIME_LOCK_TIMEOUT_SECONDS must be greater than zero" - ) - return timeout - - -def open_lock_file(lock_file: Path): - lock_file.parent.mkdir(parents=True, exist_ok=True) - handle = lock_file.open("a+b") - if os.name == "nt": - handle.seek(0, os.SEEK_END) - if handle.tell() == 0: - handle.write(b"\0") - handle.flush() - handle.seek(0) - return handle - - -def try_lock(handle) -> None: - if os.name == "nt": - handle.seek(0) - msvcrt.locking(handle.fileno(), msvcrt.LK_NBLCK, 1) - else: - fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) - - -def unlock(handle) -> None: - if os.name == "nt": - handle.seek(0) - msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1) - else: - fcntl.flock(handle.fileno(), fcntl.LOCK_UN) - - -def is_lock_contention(error: OSError) -> bool: - if os.name == "nt": - return error.errno in { - errno.EACCES, - getattr(errno, "EDEADLK", errno.EACCES), - errno.EAGAIN, - } - return error.errno in {errno.EACCES, errno.EAGAIN} - - -def acquire_lock(lock_file: Path, timeout: float): - handle = open_lock_file(lock_file) - deadline = time.monotonic() + timeout - last_notice = 0.0 - - while True: - try: - try_lock(handle) - break - except OSError as error: - if not is_lock_contention(error): - handle.close() - raise - now = time.monotonic() - if now >= deadline: - handle.close() - raise TimeoutError( - f"timed out waiting for native runtime test lock after {timeout:.0f}s: {lock_file}" - ) from error - if now - last_notice >= 30: - print( - f"waiting for native runtime test lock: {lock_file}", - file=sys.stderr, - flush=True, - ) - last_notice = now - time.sleep(0.25) - - handle.seek(0) - handle.truncate() - metadata = ( - f"pid={os.getpid()}\n" - f"cwd={Path.cwd()}\n" - f"started_at_unix={int(time.time())}\n" - f"command={' '.join(sys.argv[1:])}\n" - ) - handle.write(metadata.encode("utf-8")) - handle.flush() - return handle - - -def main() -> int: - if len(sys.argv) < 2: - print( - "usage: tools/runtime/with-native-runtime-lock.py [args...]", - file=sys.stderr, - ) - return 2 - - path = lock_path() - try: - handle = acquire_lock(path, timeout_seconds()) - except TimeoutError as error: - print(error, file=sys.stderr) - return 124 - - try: - completed = subprocess.run(sys.argv[1:], check=False) - finally: - unlock(handle) - handle.close() - return completed.returncode - - -if __name__ == "__main__": - raise SystemExit(main()) From 219b722cfc9da30fd1ba50f1a6e6dcc7999875ee Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 23:06:25 +0000 Subject: [PATCH 130/137] chore: port registry publication checks to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 7 + docs/maintainers/release-setup.md | 2 +- tools/policy/python-entrypoints.allowlist | 2 - tools/release/check_cratesio_publication.py | 179 ---- tools/release/check_registry_publication.mjs | 930 ++++++++++++++++++ tools/release/check_registry_publication.py | 660 ------------- tools/release/check_release_versions.py | 75 +- tools/release/release.py | 97 +- 8 files changed, 1070 insertions(+), 882 deletions(-) delete mode 100755 tools/release/check_cratesio_publication.py create mode 100644 tools/release/check_registry_publication.mjs delete mode 100755 tools/release/check_registry_publication.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index b2e1bfcf..366fa161 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -1011,3 +1011,10 @@ until the current-state gates here are checked with fresh local evidence. stale-owner recovery. Direct smokes covered successful command execution, metadata materialization, contention timeout exit `124`, stale lock cleanup, invalid timeout handling, and usage errors. +- On 2026-06-26, the public registry publication checker moved from Python to + Bun. `check_registry_publication.mjs` now owns crates.io, npm, JSR, and Maven + package/version/identity queries, preserves the existing release CLI modes and + registry retry environment controls, and provides JSON helper subcommands for + the still-Python release orchestrators. Representative Python/Bun parity + checks passed for `oliphaunt-js` npm/JSR and `oliphaunt-rust` crates.io + report modes before the retired Python entrypoints were removed. diff --git a/docs/maintainers/release-setup.md b/docs/maintainers/release-setup.md index e4037a7b..f89e89a5 100644 --- a/docs/maintainers/release-setup.md +++ b/docs/maintainers/release-setup.md @@ -423,7 +423,7 @@ dependency tags, registry packages, and GitHub release assets already exist. First-time package identities are not a dry-run prerequisite. Some registries create the package identity during the first publish, while others require maintainer setup before a package settings page or trusted publisher can be -configured. Treat `check_registry_publication.py --require-identities` as an +configured. Treat `tools/dev/bun.sh tools/release/check_registry_publication.mjs --require-identities` as an optional setup diagnostic, not the release gate. The release gate checks that planned versions are not already published, runs package-native dry-runs where the registry supports them, and verifies publication after the real publish. diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index 50f272da..ff24fb1f 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -9,10 +9,8 @@ tools/release/artifact_targets.py tools/release/build-extension-ci-artifacts.py tools/release/check_artifact_targets.py tools/release/check_consumer_shape.py -tools/release/check_cratesio_publication.py tools/release/check_github_release_assets.py tools/release/check_liboliphaunt_release_assets.py -tools/release/check_registry_publication.py tools/release/check_release_metadata.py tools/release/check_release_versions.py tools/release/check_staged_artifacts.py diff --git a/tools/release/check_cratesio_publication.py b/tools/release/check_cratesio_publication.py deleted file mode 100755 index a1aa285d..00000000 --- a/tools/release/check_cratesio_publication.py +++ /dev/null @@ -1,179 +0,0 @@ -#!/usr/bin/env python3 -"""Check whether selected Cargo product crates are published on crates.io.""" - -from __future__ import annotations - -import argparse -import os -import sys -import time -import tomllib -import urllib.error -import urllib.parse -import urllib.request -from pathlib import Path -from typing import NoReturn - -import product_metadata - - -ROOT = Path(__file__).resolve().parents[2] -CRATES_IO_API = os.environ.get("CRATES_IO_API", "https://crates.io/api/v1") -REQUEST_ATTEMPTS = int(os.environ.get("OLIPHAUNT_REGISTRY_QUERY_ATTEMPTS", "3")) -REQUEST_RETRY_DELAY_SECONDS = float( - os.environ.get("OLIPHAUNT_REGISTRY_QUERY_RETRY_DELAY", "1.0") -) - - -def fail(message: str) -> NoReturn: - print(f"check_cratesio_publication.py: {message}", file=sys.stderr) - raise SystemExit(1) - - -def request_attempts() -> int: - return max(1, REQUEST_ATTEMPTS) - - -def sleep_before_retry(attempt: int) -> None: - if attempt + 1 < request_attempts() and REQUEST_RETRY_DELAY_SECONDS > 0: - time.sleep(REQUEST_RETRY_DELAY_SECONDS) - - -def retryable_http_error(error: urllib.error.HTTPError) -> bool: - return error.code == 429 or error.code >= 500 - - -def cargo_package_name(manifest_path: str) -> str: - path = ROOT / manifest_path - manifest = tomllib.loads(path.read_text(encoding="utf-8")) - package = manifest.get("package") - if not isinstance(package, dict): - fail(f"{manifest_path} does not define [package]") - name = package.get("name") - if not isinstance(name, str) or not name: - fail(f"{manifest_path} does not define package.name") - return name - - -def product_crates(product: str) -> list[str]: - config = product_metadata.product_config(product) - publish_targets = product_metadata.string_list(config, "publish_targets", product) - if "crates-io" not in publish_targets: - fail(f"{product} does not publish to crates.io") - crates = [ - raw.split(":", 1)[1] - for raw in product_metadata.string_list(config, "registry_packages", product) - if raw.startswith("crates:") - ] - if not crates: - for version_file in product_metadata.version_files(product): - if Path(version_file).name == "Cargo.toml": - crates.append(cargo_package_name(version_file)) - if not crates: - fail(f"{product} does not declare Cargo registry packages") - if len(crates) != len(set(crates)): - fail(f"{product} declares duplicate Cargo registry packages: {crates}") - return sorted(crates) - - -def query_crates(product: str) -> tuple[str, list[str], list[str], list[str]]: - version = product_metadata.read_current_version(product) - crates = product_crates(product) - missing: list[str] = [] - published: list[str] = [] - for crate in crates: - if crate_version_exists(crate, version): - published.append(crate) - else: - missing.append(crate) - return version, crates, missing, published - - -def assert_product_publication(product: str, *, require_published: bool) -> None: - version, crates, missing, published = query_crates(product) - if require_published and missing: - fail( - f"{product} tag exists but crates.io is missing version {version} for: " - + ", ".join(missing) - ) - if not require_published and published: - fail( - f"{product} version {version} is already published on crates.io for: " - + ", ".join(published) - ) - state = "published" if require_published else "unpublished" - print(f"{product} crates.io {state} check passed for {version}: {', '.join(crates)}") - - -def crate_version_exists(crate: str, version: str) -> bool: - crate_path = urllib.parse.quote(crate, safe="") - version_path = urllib.parse.quote(version, safe="") - url = f"{CRATES_IO_API.rstrip('/')}/crates/{crate_path}/{version_path}" - return cratesio_url_exists(url, f"{crate} {version}") - - -def crate_exists(crate: str) -> bool: - crate_path = urllib.parse.quote(crate, safe="") - url = f"{CRATES_IO_API.rstrip('/')}/crates/{crate_path}" - return cratesio_url_exists(url, crate) - - -def cratesio_url_exists(url: str, label: str) -> bool: - last_error: Exception | None = None - for attempt in range(request_attempts()): - request = urllib.request.Request( - url, - headers={ - "Accept": "application/json", - "User-Agent": "oliphaunt-release-check (https://github.com/f0rr0/oliphaunt)", - }, - ) - try: - with urllib.request.urlopen(request, timeout=20) as response: - return 200 <= response.status < 300 - except urllib.error.HTTPError as error: - if error.code == 404: - return False - if not retryable_http_error(error): - fail(f"crates.io returned HTTP {error.code} for {label}") - last_error = error - sleep_before_retry(attempt) - except urllib.error.URLError as error: - last_error = error - sleep_before_retry(attempt) - assert last_error is not None - if isinstance(last_error, urllib.error.HTTPError): - fail(f"crates.io returned HTTP {last_error.code} for {label}") - fail(f"failed to query crates.io for {label}: {last_error}") - - -def parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--product", required=True, help="release product id") - parser.add_argument( - "--require-published", - action="store_true", - help="fail if any Cargo crate for the product is missing from crates.io", - ) - parser.add_argument( - "--require-unpublished", - action="store_true", - help="fail if any Cargo crate for the product already exists on crates.io", - ) - return parser.parse_args(argv) - - -def main(argv: list[str]) -> int: - args = parse_args(argv) - if args.require_published == args.require_unpublished: - fail("pass exactly one of --require-published or --require-unpublished") - - assert_product_publication( - args.product, - require_published=args.require_published, - ) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) diff --git a/tools/release/check_registry_publication.mjs b/tools/release/check_registry_publication.mjs new file mode 100644 index 00000000..88d2555c --- /dev/null +++ b/tools/release/check_registry_publication.mjs @@ -0,0 +1,930 @@ +#!/usr/bin/env bun +import { readFile } from "node:fs/promises"; +import fs from "node:fs"; +import path from "node:path"; +import { currentVersion } from "./product-version.mjs"; + +const ROOT = path.resolve(import.meta.dir, "../.."); +const CRATES_IO_API = process.env.CRATES_IO_API || "https://crates.io/api/v1"; +const NPM_REGISTRY = process.env.NPM_REGISTRY || "https://registry.npmjs.org"; +const JSR_REGISTRY = process.env.JSR_REGISTRY || "https://jsr.io"; +const MAVEN_CENTRAL_BASE = process.env.MAVEN_CENTRAL_BASE || "https://repo1.maven.org/maven2"; +const REQUEST_ATTEMPTS = Math.max(1, Number.parseInt(process.env.OLIPHAUNT_REGISTRY_QUERY_ATTEMPTS || "3", 10) || 3); +const REQUEST_RETRY_DELAY_SECONDS = Math.max(0, Number.parseFloat(process.env.OLIPHAUNT_REGISTRY_QUERY_RETRY_DELAY || "1.0") || 0); +const REGISTRY_TARGETS = new Set(["crates-io", "npm", "jsr", "maven-central"]); +const REGISTRY_KINDS = new Set(["crates", "npm", "jsr", "maven"]); +const USER_AGENT = "oliphaunt-release-check (https://github.com/f0rr0/oliphaunt)"; + +const caches = { + releaseConfig: undefined, + packageByProduct: undefined, + productConfig: new Map(), +}; + +class RegistryHttpError extends Error { + constructor(status, label) { + super(`HTTP ${status} for ${label}`); + this.status = status; + } +} + +function fail(message) { + console.error(`check_registry_publication.mjs: ${message}`); + process.exit(1); +} + +function rel(file) { + const relative = path.relative(ROOT, file); + return relative.startsWith("..") || path.isAbsolute(relative) ? file : relative.split(path.sep).join("/"); +} + +async function readJson(file) { + let text; + try { + text = await readFile(file, "utf8"); + } catch { + fail(`missing ${rel(file)}`); + } + const value = JSON.parse(text); + if (value === null || Array.isArray(value) || typeof value !== "object") { + fail(`${rel(file)} must contain a JSON object`); + } + return value; +} + +async function readToml(file) { + let text; + try { + text = await readFile(file, "utf8"); + } catch { + fail(`missing ${rel(file)}`); + } + const value = Bun.TOML.parse(text); + if (value === null || Array.isArray(value) || typeof value !== "object") { + fail(`${rel(file)} must contain a TOML table`); + } + return value; +} + +async function releaseConfig() { + if (caches.releaseConfig === undefined) { + caches.releaseConfig = await readJson(path.join(ROOT, "release-please-config.json")); + } + return caches.releaseConfig; +} + +function assertRelative(value, context) { + if (typeof value !== "string" || value.length === 0) { + fail(`${context} must be a non-empty string`); + } + const parts = value.split(/[\\/]/u); + if (path.isAbsolute(value) || /^[A-Za-z]:[\\/]/u.test(value) || parts.includes("..")) { + fail(`${context} must stay inside the repository: ${JSON.stringify(value)}`); + } + return value; +} + +async function packageByProduct() { + if (caches.packageByProduct !== undefined) { + return caches.packageByProduct; + } + const config = await releaseConfig(); + const packages = config.packages; + if (packages === null || Array.isArray(packages) || typeof packages !== "object") { + fail("release-please-config.json must define packages"); + } + const byProduct = new Map(); + for (const [rawPackagePath, packageConfig] of Object.entries(packages)) { + if (packageConfig === null || Array.isArray(packageConfig) || typeof packageConfig !== "object") { + fail(`${rawPackagePath} release-please config must be an object`); + } + const component = packageConfig.component; + if (typeof component !== "string" || component.length === 0) { + fail(`${rawPackagePath}.component must be a non-empty string`); + } + if (byProduct.has(component)) { + fail(`duplicate release-please component ${component}`); + } + const packagePath = assertRelative(rawPackagePath, `${component}.packagePath`); + byProduct.set(component, { packagePath, packageConfig }); + } + caches.packageByProduct = byProduct; + return byProduct; +} + +async function packageRecord(product) { + const record = (await packageByProduct()).get(product); + if (record === undefined) { + fail(`unknown release product ${JSON.stringify(product)}`); + } + return record; +} + +async function productIds() { + return [...(await packageByProduct()).keys()]; +} + +async function packagePath(product) { + return (await packageRecord(product)).packagePath; +} + +function packageRelativePath(packagePathValue, relative, context) { + return path.join(assertRelative(packagePathValue, `${context}.packagePath`), assertRelative(relative, context)).split(path.sep).join("/"); +} + +async function releaseMetadata(product) { + if (caches.productConfig.has(product)) { + return caches.productConfig.get(product); + } + const packagePathValue = await packagePath(product); + const metadata = await readToml(path.join(ROOT, packagePathValue, "release.toml")); + if (metadata.id !== product) { + fail(`${packagePathValue}/release.toml must declare id = ${JSON.stringify(product)}`); + } + caches.productConfig.set(product, metadata); + return metadata; +} + +async function productConfig(product) { + return releaseMetadata(product); +} + +function stringList(config, key, product) { + const value = config[key] ?? []; + if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) { + fail(`${product}.${key} must be a string list`); + } + return value; +} + +async function canonicalVersionFile(product) { + const { packagePath: packagePathValue, packageConfig } = await packageRecord(product); + const versionFile = packageConfig["version-file"]; + if (typeof versionFile === "string" && versionFile.length > 0) { + return packageRelativePath(packagePathValue, versionFile, `${product}.version-file`); + } + const releaseType = packageConfig["release-type"]; + if (releaseType === "rust") { + return packageRelativePath(packagePathValue, "Cargo.toml", `${product}.rust`); + } + if (releaseType === "node" || releaseType === "expo") { + return packageRelativePath(packagePathValue, "package.json", `${product}.node`); + } + fail(`${product} release-please config must declare version-file for release type ${JSON.stringify(releaseType)}`); +} + +async function extraVersionFiles(product) { + const { packagePath: packagePathValue, packageConfig } = await packageRecord(product); + const extraFiles = packageConfig["extra-files"] ?? []; + if (!Array.isArray(extraFiles)) { + fail(`${product}.extra-files must be a list`); + } + return extraFiles.map((entry, index) => { + const context = `${product}.extra-files[${index}]`; + if (typeof entry === "string") { + return packageRelativePath(packagePathValue, entry, context); + } + if (entry === null || Array.isArray(entry) || typeof entry !== "object") { + fail(`${context} must be a path string or object`); + } + const entryPath = entry.path; + if (typeof entryPath !== "string" || entryPath.length === 0) { + fail(`${context}.path must be a non-empty string`); + } + return packageRelativePath(packagePathValue, entryPath, `${context}.path`); + }); +} + +async function versionFiles(product) { + const files = [await canonicalVersionFile(product), ...(await extraVersionFiles(product))]; + for (const file of files) { + if (!fs.existsSync(path.join(ROOT, file))) { + fail(`${product} version file does not exist: ${file}`); + } + } + return files; +} + +async function cargoPackageName(manifestPath) { + const manifest = await readToml(path.join(ROOT, manifestPath)); + const name = manifest.package?.name; + if (typeof name !== "string" || name.length === 0) { + fail(`${manifestPath} does not define package.name`); + } + return name; +} + +async function productCrates(product) { + const config = await productConfig(product); + const publishTargets = stringList(config, "publish_targets", product); + if (!publishTargets.includes("crates-io")) { + fail(`${product} does not publish to crates.io`); + } + const crates = stringList(config, "registry_packages", product) + .filter((raw) => raw.startsWith("crates:")) + .map((raw) => raw.slice("crates:".length)); + if (crates.length === 0) { + for (const file of await versionFiles(product)) { + if (path.basename(file) === "Cargo.toml") { + crates.push(await cargoPackageName(file)); + } + } + } + if (crates.length === 0) { + fail(`${product} does not declare Cargo registry packages`); + } + const duplicates = [...new Set(crates.filter((crate, index) => crates.indexOf(crate) !== index))].sort(); + if (duplicates.length > 0) { + fail(`${product} declares duplicate Cargo registry packages: ${duplicates.join(", ")}`); + } + return crates.sort(); +} + +function parseRegistryPackage(raw, product, version) { + const separator = raw.indexOf(":"); + if (separator <= 0 || separator === raw.length - 1) { + fail(`${product}.registry_packages entry ${JSON.stringify(raw)} must use kind:name`); + } + const kind = raw.slice(0, separator); + const name = raw.slice(separator + 1); + if (!REGISTRY_KINDS.has(kind)) { + fail(`${product}.registry_packages entry ${JSON.stringify(raw)} has unsupported kind ${JSON.stringify(kind)}`); + } + return { kind, name, version }; +} + +function packageLabel(pkg) { + return `${pkg.kind}:${pkg.name}@${pkg.version}`; +} + +function identityLabel(pkg) { + return `${pkg.kind}:${pkg.name}`; +} + +async function graphRegistryPackages(product, version) { + const config = await productConfig(product); + return stringList(config, "registry_packages", product).map((raw) => parseRegistryPackage(raw, product, version)); +} + +async function nativePublishedTargets() { + const moon = await readFile(path.join(ROOT, "src/runtimes/liboliphaunt/native/moon.yml"), "utf8"); + const lines = moon.split(/\r?\n/u); + const targets = []; + let inPublished = false; + let baseIndent = -1; + for (const line of lines) { + const indent = line.match(/^\s*/u)?.[0].length ?? 0; + const trimmed = line.trim(); + if (trimmed === "publishedTargets:") { + inPublished = true; + baseIndent = indent; + continue; + } + if (!inPublished) { + continue; + } + if (trimmed.startsWith("- ")) { + const match = trimmed.match(/^-\s+"?([^"]+)"?/u); + if (match) { + targets.push(match[1]); + } + continue; + } + if (trimmed.length > 0 && indent <= baseIndent) { + break; + } + } + if (targets.length === 0) { + fail("src/runtimes/liboliphaunt/native/moon.yml does not declare publishedTargets"); + } + return targets; +} + +async function publishedAndroidMavenTargets(product) { + const packagePathValue = await packagePath(product); + const overridePath = path.join(ROOT, packagePathValue, "targets", "artifacts.toml"); + let rows; + if (fs.existsSync(overridePath)) { + const data = await readToml(overridePath); + if (data.schema !== "oliphaunt-extension-artifact-targets-v1") { + fail(`${rel(overridePath)} must use schema = "oliphaunt-extension-artifact-targets-v1"`); + } + rows = data.targets; + if (!Array.isArray(rows) || rows.length === 0) { + fail(`${rel(overridePath)} must define [[targets]] rows`); + } + } else { + rows = (await nativePublishedTargets()).map((target) => ({ + target, + family: "native", + kind: target.startsWith("android-") || target === "ios-xcframework" ? "native-static-registry" : "native-dynamic", + status: "supported", + published: true, + })); + } + return rows + .filter((row) => row && row.family === "native" && row.kind === "native-static-registry" && row.published === true && typeof row.target === "string" && row.target.startsWith("android-")) + .map((row) => row.target) + .sort(); +} + +async function derivedExactExtensionMavenPackages(product, version) { + const config = await productConfig(product); + if (config.kind !== "exact-extension-artifact") { + return []; + } + return (await publishedAndroidMavenTargets(product)).map((target) => ({ + kind: "maven", + name: `dev.oliphaunt.extensions:${product}-${target}`, + version, + })); +} + +async function productRegistryPackages(product, { versionOverride = undefined, registryKind = undefined } = {}) { + const config = await productConfig(product); + const version = versionOverride || (await currentVersion(product)); + const publishTargets = new Set(stringList(config, "publish_targets", product)); + const graphPackages = await graphRegistryPackages(product, version); + const allowedGraphKinds = new Set(); + if (publishTargets.has("crates-io")) { + allowedGraphKinds.add("crates"); + } + const expectedKinds = new Map([ + ["npm", "npm"], + ["jsr", "jsr"], + ["maven-central", "maven"], + ]); + for (const [target, kind] of expectedKinds.entries()) { + if (publishTargets.has(target)) { + allowedGraphKinds.add(kind); + } + } + const stalePackages = graphPackages + .filter((pkg) => !allowedGraphKinds.has(pkg.kind)) + .map((pkg) => `${pkg.kind}:${pkg.name}`) + .sort(); + if (stalePackages.length > 0) { + fail(`${product}.registry_packages contains entries without a matching registry publish target: ${stalePackages.join(", ")}`); + } + const packages = [...graphPackages]; + if (publishTargets.has("crates-io")) { + const derivedCrates = (await productCrates(product)).map((name) => ({ kind: "crates", name, version })); + const graphCrates = packages.filter((pkg) => pkg.kind === "crates"); + if (graphCrates.length > 0) { + const derivedNames = derivedCrates.map((pkg) => pkg.name).sort(); + const graphNames = graphCrates.map((pkg) => pkg.name).sort(); + if (JSON.stringify(graphNames) !== JSON.stringify(derivedNames)) { + fail(`${product}.registry_packages crates entries ${JSON.stringify(graphNames)} do not match Cargo manifests ${JSON.stringify(derivedNames)}`); + } + } else { + packages.push(...derivedCrates); + } + } + const derivedMaven = await derivedExactExtensionMavenPackages(product, version); + if (derivedMaven.length > 0) { + const graphMaven = packages.filter((pkg) => pkg.kind === "maven"); + const derivedNames = derivedMaven.map((pkg) => pkg.name).sort(); + const graphNames = graphMaven.map((pkg) => pkg.name).sort(); + if (JSON.stringify(graphNames) !== JSON.stringify(derivedNames)) { + fail(`${product}.registry_packages maven entries ${JSON.stringify(graphNames)} do not match exact-extension Android artifact targets ${JSON.stringify(derivedNames)}`); + } + } + const missingKinds = []; + for (const [target, kind] of expectedKinds.entries()) { + if (publishTargets.has(target) && !packages.some((pkg) => pkg.kind === kind)) { + missingKinds.push(kind); + } + } + if (missingKinds.length > 0) { + const selectedTargets = [...publishTargets].filter((target) => REGISTRY_TARGETS.has(target)).sort(); + fail(`${product} publishes to ${JSON.stringify(selectedTargets)} but is missing registry_packages entries for: ${missingKinds.join(", ")}`); + } + let filtered = packages; + if (registryKind !== undefined) { + if (!REGISTRY_KINDS.has(registryKind)) { + fail(`unsupported registry kind ${JSON.stringify(registryKind)}`); + } + filtered = packages.filter((pkg) => pkg.kind === registryKind); + if (filtered.length === 0) { + fail(`${product} has no ${registryKind} registry packages to check`); + } + } + return filtered; +} + +function retryableStatus(status) { + return status === 429 || status >= 500; +} + +function sleep(seconds) { + if (seconds <= 0) { + return Promise.resolve(); + } + return new Promise((resolve) => setTimeout(resolve, seconds * 1000)); +} + +async function requestJson(url, label) { + let lastError; + for (let attempt = 0; attempt < REQUEST_ATTEMPTS; attempt += 1) { + try { + const response = await fetch(url, { + headers: { + Accept: "application/json", + "User-Agent": USER_AGENT, + }, + signal: AbortSignal.timeout(20_000), + }); + if (response.ok) { + return await response.json(); + } + const error = new RegistryHttpError(response.status, label); + if (!retryableStatus(response.status)) { + throw error; + } + lastError = error; + } catch (error) { + lastError = error; + if (error instanceof RegistryHttpError && !retryableStatus(error.status)) { + throw error; + } + } + if (attempt + 1 < REQUEST_ATTEMPTS) { + await sleep(REQUEST_RETRY_DELAY_SECONDS); + } + } + throw lastError ?? new Error(`failed to query ${label}`); +} + +async function urlExistsViaGet(url) { + return urlExists(url, { method: "GET", allowMethodFallback: false }); +} + +async function urlExists(url, { method = "HEAD", allowMethodFallback = true } = {}) { + let lastError; + for (let attempt = 0; attempt < REQUEST_ATTEMPTS; attempt += 1) { + try { + const response = await fetch(url, { + method, + headers: { + Accept: "application/json", + "User-Agent": USER_AGENT, + }, + signal: AbortSignal.timeout(20_000), + }); + if (response.ok) { + return true; + } + if (response.status === 404) { + return false; + } + if (response.status === 405 && method === "HEAD" && allowMethodFallback) { + return urlExistsViaGet(url); + } + const error = new RegistryHttpError(response.status, url); + if (!retryableStatus(response.status)) { + fail(`registry returned HTTP ${response.status} for ${url}`); + } + lastError = error; + } catch (error) { + lastError = error; + if (error instanceof RegistryHttpError && !retryableStatus(error.status)) { + fail(`registry returned HTTP ${error.status} for ${url}`); + } + } + if (attempt + 1 < REQUEST_ATTEMPTS) { + await sleep(REQUEST_RETRY_DELAY_SECONDS); + } + } + if (lastError instanceof RegistryHttpError) { + fail(`registry returned HTTP ${lastError.status} for ${url}`); + } + fail(`failed to query registry URL ${url}: ${lastError}`); +} + +async function cratesioUrlExists(url, label) { + try { + return await urlExists(url, { method: "GET", allowMethodFallback: false }); + } catch (error) { + if (error instanceof RegistryHttpError && error.status === 404) { + return false; + } + throw error; + } +} + +async function crateVersionExists(crate, version) { + const cratePath = encodeURIComponent(crate); + const versionPath = encodeURIComponent(version); + const url = `${CRATES_IO_API.replace(/\/+$/u, "")}/crates/${cratePath}/${versionPath}`; + return cratesioUrlExists(url, `${crate} ${version}`); +} + +async function crateExists(crate) { + const cratePath = encodeURIComponent(crate); + const url = `${CRATES_IO_API.replace(/\/+$/u, "")}/crates/${cratePath}`; + return cratesioUrlExists(url, crate); +} + +async function npmPackageMetadata(packageName) { + const packagePath = encodeURIComponent(packageName); + const url = `${NPM_REGISTRY.replace(/\/+$/u, "")}/${packagePath}`; + try { + const data = await requestJson(url, packageName); + return data && !Array.isArray(data) && typeof data === "object" ? data : undefined; + } catch (error) { + if (error instanceof RegistryHttpError && error.status === 404) { + return undefined; + } + if (error instanceof RegistryHttpError) { + fail(`npm registry returned HTTP ${error.status} for ${packageName}`); + } + fail(`failed to query npm registry for ${packageName}: ${error}`); + } +} + +async function npmVersionExists(packageName, version) { + const data = await npmPackageMetadata(packageName); + if (data === undefined) { + return false; + } + const versions = data.versions; + return versions !== null && !Array.isArray(versions) && typeof versions === "object" && version in versions; +} + +async function npmPackageExists(packageName) { + return (await npmPackageMetadata(packageName)) !== undefined; +} + +function mavenCoordinatePaths(coordinate, version = undefined) { + const parts = coordinate.split(":"); + if (parts.length !== 2 || parts.some((part) => part.length === 0)) { + fail(`invalid Maven coordinate ${JSON.stringify(coordinate)}; expected group:artifact`); + } + const [group, artifact] = parts; + const groupPath = group.split(".").map((part) => encodeURIComponent(part)).join("/"); + const artifactPath = encodeURIComponent(artifact); + if (version === undefined) { + return `${MAVEN_CENTRAL_BASE.replace(/\/+$/u, "")}/${groupPath}/${artifactPath}/maven-metadata.xml`; + } + const versionPath = encodeURIComponent(version); + return `${MAVEN_CENTRAL_BASE.replace(/\/+$/u, "")}/${groupPath}/${artifactPath}/${versionPath}/${artifactPath}-${versionPath}.pom`; +} + +async function mavenVersionExists(coordinate, version) { + return urlExists(mavenCoordinatePaths(coordinate, version)); +} + +async function mavenCoordinateExists(coordinate) { + return urlExists(mavenCoordinatePaths(coordinate)); +} + +function jsrMetaUrl(packageName) { + if (!packageName.startsWith("@") || !packageName.includes("/")) { + fail(`invalid JSR package ${JSON.stringify(packageName)}; expected @scope/name`); + } + const [scope, name] = packageName.slice(1).split("/", 2); + return `${JSR_REGISTRY.replace(/\/+$/u, "")}/@${encodeURIComponent(scope)}/${encodeURIComponent(name)}/meta.json`; +} + +async function jsrPackageMetadata(packageName) { + try { + const data = await requestJson(jsrMetaUrl(packageName), packageName); + return data && !Array.isArray(data) && typeof data === "object" ? data : undefined; + } catch (error) { + if (error instanceof RegistryHttpError && error.status === 404) { + return undefined; + } + if (error instanceof RegistryHttpError) { + fail(`JSR registry returned HTTP ${error.status} for ${packageName}`); + } + fail(`failed to query JSR registry for ${packageName}: ${error}`); + } +} + +async function jsrVersionExists(packageName, version) { + const data = await jsrPackageMetadata(packageName); + if (data === undefined) { + return false; + } + const versions = data.versions; + return versions !== null && !Array.isArray(versions) && typeof versions === "object" && version in versions; +} + +async function jsrPackageExists(packageName) { + return (await jsrPackageMetadata(packageName)) !== undefined; +} + +async function packageExists(pkg) { + if (pkg.kind === "crates") { + return crateVersionExists(pkg.name, pkg.version); + } + if (pkg.kind === "npm") { + return npmVersionExists(pkg.name, pkg.version); + } + if (pkg.kind === "jsr") { + return jsrVersionExists(pkg.name, pkg.version); + } + if (pkg.kind === "maven") { + return mavenVersionExists(pkg.name, pkg.version); + } + fail(`unsupported registry package kind ${JSON.stringify(pkg.kind)}`); +} + +async function packageIdentityExists(pkg) { + if (pkg.kind === "crates") { + return crateExists(pkg.name); + } + if (pkg.kind === "npm") { + return npmPackageExists(pkg.name); + } + if (pkg.kind === "jsr") { + return jsrPackageExists(pkg.name); + } + if (pkg.kind === "maven") { + return mavenCoordinateExists(pkg.name); + } + fail(`unsupported registry package kind ${JSON.stringify(pkg.kind)}`); +} + +async function queryProductPublication(product, { versionOverride = undefined, registryKind = undefined, retries = 0, retryDelay = 0 } = {}) { + const packages = await productRegistryPackages(product, { versionOverride, registryKind }); + const attempts = Math.max(1, retries + 1); + let lastMissing = []; + let lastPublished = []; + for (let attempt = 0; attempt < attempts; attempt += 1) { + const missing = []; + const published = []; + for (const pkg of packages) { + if (await packageExists(pkg)) { + published.push(pkg); + } else { + missing.push(pkg); + } + } + lastMissing = missing; + lastPublished = published; + if (missing.length === 0 || attempt === attempts - 1) { + break; + } + await sleep(retryDelay); + } + return { packages, missing: lastMissing, published: lastPublished }; +} + +async function productIdentityStatus(product, { registryKind = undefined } = {}) { + const packages = await productRegistryPackages(product, { registryKind }); + const present = []; + const missing = []; + for (const pkg of packages) { + if (await packageIdentityExists(pkg)) { + present.push(pkg); + } else { + missing.push(pkg); + } + } + return { packages, present, missing }; +} + +function parseFlags(argv) { + const flags = new Map(); + const positionals = []; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (!arg.startsWith("--")) { + positionals.push(arg); + continue; + } + const eq = arg.indexOf("="); + if (eq !== -1) { + flags.set(arg.slice(2, eq), arg.slice(eq + 1)); + continue; + } + const name = arg.slice(2); + if (["require-published", "require-unpublished", "report", "require-identities", "report-identities", "json"].includes(name)) { + flags.set(name, true); + continue; + } + if (index + 1 >= argv.length) { + fail(`${arg} requires a value`); + } + flags.set(name, argv[index + 1]); + index += 1; + } + return { flags, positionals }; +} + +function flagString(flags, name, { required = false } = {}) { + const value = flags.get(name); + if (value === undefined) { + if (required) { + fail(`--${name} is required`); + } + return undefined; + } + if (value === true) { + fail(`--${name} requires a value`); + } + return value; +} + +function flagNumber(flags, name, defaultValue) { + const raw = flagString(flags, name); + if (raw === undefined) { + return defaultValue; + } + const value = Number(raw); + if (!Number.isFinite(value)) { + fail(`--${name} must be numeric`); + } + return value; +} + +async function parseProducts(flags) { + const rawProducts = flagString(flags, "products-json"); + const product = flagString(flags, "product"); + if (Boolean(rawProducts) === Boolean(product)) { + fail("pass exactly one of --product or --products-json"); + } + if (product !== undefined) { + return [product]; + } + let value; + try { + value = JSON.parse(rawProducts); + } catch (error) { + fail(`--products-json must be valid JSON: ${error.message}`); + } + if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) { + fail("--products-json must be a JSON string list"); + } + const known = new Set(await productIds()); + const unknown = value.filter((item) => !known.has(item)).sort(); + if (unknown.length > 0) { + fail(`unknown release products: ${unknown.join(", ")}`); + } + return value; +} + +function serializeQueryResult(result) { + return { + packages: result.packages.map((pkg) => ({ ...pkg, label: packageLabel(pkg) })), + missing: result.missing.map((pkg) => ({ ...pkg, label: packageLabel(pkg) })), + published: result.published.map((pkg) => ({ ...pkg, label: packageLabel(pkg) })), + }; +} + +function printJson(value) { + console.log(JSON.stringify(value, null, 2)); +} + +async function runProductCrates(flags) { + const product = flagString(flags, "product", { required: true }); + const version = flagString(flags, "version") ?? (await currentVersion(product)); + printJson({ product, version, crates: await productCrates(product) }); +} + +async function runCrateVersionExists(flags) { + const crate = flagString(flags, "crate", { required: true }); + const version = flagString(flags, "version", { required: true }); + printJson({ crate, version, exists: await crateVersionExists(crate, version) }); +} + +async function runCrateExists(flags) { + const crate = flagString(flags, "crate", { required: true }); + printJson({ crate, exists: await crateExists(crate) }); +} + +async function runQueryProductPublication(flags) { + const product = flagString(flags, "product", { required: true }); + const registryKind = flagString(flags, "registry-kind"); + const versionOverride = flagString(flags, "version"); + const retries = flagNumber(flags, "retries", 0); + const retryDelay = flagNumber(flags, "retry-delay", 0); + if (retries < 0 || retryDelay < 0) { + fail("--retries and --retry-delay must be non-negative"); + } + printJson(serializeQueryResult(await queryProductPublication(product, { + versionOverride, + registryKind, + retries, + retryDelay, + }))); +} + +async function runProductRegistryPackages(flags) { + const product = flagString(flags, "product", { required: true }); + const registryKind = flagString(flags, "registry-kind"); + const versionOverride = flagString(flags, "version"); + printJson({ + packages: (await productRegistryPackages(product, { versionOverride, registryKind })).map((pkg) => ({ + ...pkg, + label: packageLabel(pkg), + })), + }); +} + +async function runPublicationCli(flags) { + const versionOverride = flagString(flags, "version"); + const registryKind = flagString(flags, "registry-kind"); + const retries = flagNumber(flags, "retries", 0); + const retryDelay = flagNumber(flags, "retry-delay", 0); + if (versionOverride !== undefined && flagString(flags, "product") === undefined) { + fail("--version can only be used with --product"); + } + if (retries < 0 || retryDelay < 0) { + fail("--retries and --retry-delay must be non-negative"); + } + const modes = ["require-published", "require-unpublished", "report", "require-identities", "report-identities"].filter((mode) => flags.has(mode)); + if (modes.length !== 1) { + fail("pass exactly one publication mode"); + } + const products = await parseProducts(flags); + const mode = modes[0]; + if (mode === "require-identities") { + const missingMessages = []; + for (const product of products) { + const status = await productIdentityStatus(product, { registryKind }); + if (status.packages.length === 0) { + console.log(`${product} has no external registry package identities to check`); + } else if (status.missing.length > 0) { + missingMessages.push(`${product}: ${status.missing.map(identityLabel).join(", ")}`); + } else { + console.log(`${product} registry identity check passed: ${status.packages.map(identityLabel).join(", ")}`); + } + } + if (missingMessages.length > 0) { + fail(`registry package identities are missing:\n - ${missingMessages.join("\n - ")}`); + } + return; + } + for (const product of products) { + if (mode === "report-identities") { + const status = await productIdentityStatus(product, { registryKind }); + if (status.packages.length === 0) { + console.log(`${product} has no external registry package identities to check`); + } + if (status.present.length > 0) { + console.log(`${product} registry identities present: ${status.present.map(identityLabel).join(", ")}`); + } + if (status.missing.length > 0) { + console.log(`${product} registry identities missing: ${status.missing.map(identityLabel).join(", ")}`); + } + continue; + } + const result = await queryProductPublication(product, { + versionOverride, + registryKind, + retries, + retryDelay, + }); + if (result.packages.length === 0) { + console.log(`${product} has no external registry packages to check`); + continue; + } + if (mode === "report") { + if (result.published.length > 0) { + console.log(`${product} registry versions already present: ${result.published.map(packageLabel).join(", ")}`); + } + if (result.missing.length > 0) { + console.log(`${product} registry versions not yet present: ${result.missing.map(packageLabel).join(", ")}`); + } + continue; + } + if (mode === "require-published" && result.missing.length > 0) { + fail(`${product} registry publication is missing: ${result.missing.map(packageLabel).join(", ")}`); + } + if (mode === "require-unpublished" && result.published.length > 0) { + fail(`${product} version is already published in public registries: ${result.published.map(packageLabel).join(", ")}`); + } + const state = mode === "require-published" ? "published" : "unpublished"; + console.log(`${product} registry ${state} check passed: ${result.packages.map(packageLabel).join(", ")}`); + } +} + +async function main(argv) { + const subcommands = new Map([ + ["product-crates", runProductCrates], + ["crate-version-exists", runCrateVersionExists], + ["crate-exists", runCrateExists], + ["query-product-publication", runQueryProductPublication], + ["product-registry-packages", runProductRegistryPackages], + ]); + const first = argv[0]; + if (subcommands.has(first)) { + const { flags, positionals } = parseFlags(argv.slice(1)); + if (positionals.length > 0) { + fail(`unexpected positional arguments: ${positionals.join(", ")}`); + } + await subcommands.get(first)(flags); + return; + } + const { flags, positionals } = parseFlags(argv); + if (positionals.length > 0) { + fail(`unexpected positional arguments: ${positionals.join(", ")}`); + } + await runPublicationCli(flags); +} + +if (import.meta.main) { + await main(Bun.argv.slice(2)); +} diff --git a/tools/release/check_registry_publication.py b/tools/release/check_registry_publication.py deleted file mode 100755 index a2089677..00000000 --- a/tools/release/check_registry_publication.py +++ /dev/null @@ -1,660 +0,0 @@ -#!/usr/bin/env python3 -"""Check selected product versions across public package registries.""" - -from __future__ import annotations - -import argparse -import json -import os -import sys -import time -import urllib.error -import urllib.parse -import urllib.request -from dataclasses import dataclass -from typing import NoReturn - -import check_cratesio_publication -import extension_artifact_targets -import product_metadata - - -NPM_REGISTRY = os.environ.get("NPM_REGISTRY", "https://registry.npmjs.org") -JSR_REGISTRY = os.environ.get("JSR_REGISTRY", "https://jsr.io") -MAVEN_CENTRAL_BASE = os.environ.get( - "MAVEN_CENTRAL_BASE", - "https://repo1.maven.org/maven2", -) -REQUEST_ATTEMPTS = int(os.environ.get("OLIPHAUNT_REGISTRY_QUERY_ATTEMPTS", "3")) -REQUEST_RETRY_DELAY_SECONDS = float( - os.environ.get("OLIPHAUNT_REGISTRY_QUERY_RETRY_DELAY", "1.0") -) -REGISTRY_TARGETS = { - "crates-io", - "npm", - "jsr", - "maven-central", -} - - -@dataclass(frozen=True) -class RegistryPackage: - kind: str - name: str - version: str - - @property - def label(self) -> str: - return f"{self.kind}:{self.name}@{self.version}" - - -def fail(message: str) -> NoReturn: - print(f"check_registry_publication.py: {message}", file=sys.stderr) - raise SystemExit(1) - - -def request_attempts() -> int: - return max(1, REQUEST_ATTEMPTS) - - -def sleep_before_retry(attempt: int) -> None: - if attempt + 1 < request_attempts() and REQUEST_RETRY_DELAY_SECONDS > 0: - time.sleep(REQUEST_RETRY_DELAY_SECONDS) - - -def retryable_http_error(error: urllib.error.HTTPError) -> bool: - return error.code == 429 or error.code >= 500 - - -def request_json(url: str) -> object: - last_error: Exception | None = None - for attempt in range(request_attempts()): - request = urllib.request.Request( - url, - headers={ - "Accept": "application/json", - "User-Agent": "oliphaunt-release-check (https://github.com/f0rr0/oliphaunt)", - }, - ) - try: - with urllib.request.urlopen(request, timeout=20) as response: - return json.load(response) - except urllib.error.HTTPError as error: - if not retryable_http_error(error): - raise - last_error = error - sleep_before_retry(attempt) - except urllib.error.URLError as error: - last_error = error - sleep_before_retry(attempt) - assert last_error is not None - raise last_error - - -def url_exists(url: str) -> bool: - last_error: Exception | None = None - for attempt in range(request_attempts()): - request = urllib.request.Request( - url, - method="HEAD", - headers={ - "Accept": "application/json", - "User-Agent": "oliphaunt-release-check (https://github.com/f0rr0/oliphaunt)", - }, - ) - try: - with urllib.request.urlopen(request, timeout=20) as response: - return 200 <= response.status < 300 - except urllib.error.HTTPError as error: - if error.code == 404: - return False - if error.code == 405: - return url_exists_via_get(url) - if not retryable_http_error(error): - fail(f"registry returned HTTP {error.code} for {url}") - last_error = error - sleep_before_retry(attempt) - except urllib.error.URLError as error: - last_error = error - sleep_before_retry(attempt) - assert last_error is not None - if isinstance(last_error, urllib.error.HTTPError): - fail(f"registry returned HTTP {last_error.code} for {url}") - fail(f"failed to query registry URL {url}: {last_error}") - - -def url_exists_via_get(url: str) -> bool: - last_error: Exception | None = None - for attempt in range(request_attempts()): - request = urllib.request.Request( - url, - headers={ - "Accept": "application/json", - "User-Agent": "oliphaunt-release-check (https://github.com/f0rr0/oliphaunt)", - }, - ) - try: - with urllib.request.urlopen(request, timeout=20) as response: - return 200 <= response.status < 300 - except urllib.error.HTTPError as error: - if error.code == 404: - return False - if not retryable_http_error(error): - fail(f"registry returned HTTP {error.code} for {url}") - last_error = error - sleep_before_retry(attempt) - except urllib.error.URLError as error: - last_error = error - sleep_before_retry(attempt) - assert last_error is not None - if isinstance(last_error, urllib.error.HTTPError): - fail(f"registry returned HTTP {last_error.code} for {url}") - fail(f"failed to query registry URL {url}: {last_error}") - - -def npm_version_exists(package: str, version: str) -> bool: - package_path = urllib.parse.quote(package, safe="") - url = f"{NPM_REGISTRY.rstrip('/')}/{package_path}" - try: - data = request_json(url) - except urllib.error.HTTPError as error: - if error.code == 404: - return False - fail(f"npm registry returned HTTP {error.code} for {package}") - except urllib.error.URLError as error: - fail(f"failed to query npm registry for {package}: {error}") - if not isinstance(data, dict): - fail(f"npm registry returned malformed metadata for {package}") - versions = data.get("versions") - if not isinstance(versions, dict): - return False - return version in versions - - -def npm_package_exists(package: str) -> bool: - package_path = urllib.parse.quote(package, safe="") - url = f"{NPM_REGISTRY.rstrip('/')}/{package_path}" - try: - data = request_json(url) - except urllib.error.HTTPError as error: - if error.code == 404: - return False - fail(f"npm registry returned HTTP {error.code} for {package}") - except urllib.error.URLError as error: - fail(f"failed to query npm registry for {package}: {error}") - return isinstance(data, dict) - - -def maven_version_exists(coordinate: str, version: str) -> bool: - parts = coordinate.split(":") - if len(parts) != 2 or not all(parts): - fail(f"invalid Maven coordinate {coordinate!r}; expected group:artifact") - group, artifact = parts - group_path = "/".join(urllib.parse.quote(part, safe="") for part in group.split(".")) - artifact_path = urllib.parse.quote(artifact, safe="") - version_path = urllib.parse.quote(version, safe="") - url = ( - f"{MAVEN_CENTRAL_BASE.rstrip('/')}/{group_path}/{artifact_path}/" - f"{version_path}/{artifact_path}-{version_path}.pom" - ) - return url_exists(url) - - -def maven_coordinate_exists(coordinate: str) -> bool: - parts = coordinate.split(":") - if len(parts) != 2 or not all(parts): - fail(f"invalid Maven coordinate {coordinate!r}; expected group:artifact") - group, artifact = parts - group_path = "/".join(urllib.parse.quote(part, safe="") for part in group.split(".")) - artifact_path = urllib.parse.quote(artifact, safe="") - metadata_url = ( - f"{MAVEN_CENTRAL_BASE.rstrip('/')}/{group_path}/{artifact_path}/maven-metadata.xml" - ) - return url_exists(metadata_url) - - -def jsr_version_exists(package: str, version: str) -> bool: - if not package.startswith("@") or "/" not in package: - fail(f"invalid JSR package {package!r}; expected @scope/name") - scope, name = package[1:].split("/", 1) - scope_path = urllib.parse.quote(scope, safe="") - name_path = urllib.parse.quote(name, safe="") - url = f"{JSR_REGISTRY.rstrip('/')}/@{scope_path}/{name_path}/meta.json" - try: - data = request_json(url) - except urllib.error.HTTPError as error: - if error.code == 404: - return False - fail(f"JSR registry returned HTTP {error.code} for {package}") - except urllib.error.URLError as error: - fail(f"failed to query JSR registry for {package}: {error}") - if not isinstance(data, dict): - fail(f"JSR registry returned malformed metadata for {package}") - versions = data.get("versions") - if not isinstance(versions, dict): - return False - return version in versions - - -def jsr_package_exists(package: str) -> bool: - if not package.startswith("@") or "/" not in package: - fail(f"invalid JSR package {package!r}; expected @scope/name") - scope, name = package[1:].split("/", 1) - scope_path = urllib.parse.quote(scope, safe="") - name_path = urllib.parse.quote(name, safe="") - url = f"{JSR_REGISTRY.rstrip('/')}/@{scope_path}/{name_path}/meta.json" - try: - data = request_json(url) - except urllib.error.HTTPError as error: - if error.code == 404: - return False - fail(f"JSR registry returned HTTP {error.code} for {package}") - except urllib.error.URLError as error: - fail(f"failed to query JSR registry for {package}: {error}") - return isinstance(data, dict) - - -def package_exists(package: RegistryPackage) -> bool: - if package.kind == "crates": - return check_cratesio_publication.crate_version_exists(package.name, package.version) - if package.kind == "npm": - return npm_version_exists(package.name, package.version) - if package.kind == "jsr": - return jsr_version_exists(package.name, package.version) - if package.kind == "maven": - return maven_version_exists(package.name, package.version) - fail(f"unsupported registry package kind {package.kind!r}") - - -def package_identity_exists(package: RegistryPackage) -> bool: - if package.kind == "crates": - return check_cratesio_publication.crate_exists(package.name) - if package.kind == "npm": - return npm_package_exists(package.name) - if package.kind == "jsr": - return jsr_package_exists(package.name) - if package.kind == "maven": - return maven_coordinate_exists(package.name) - fail(f"unsupported registry package kind {package.kind!r}") - - -def parse_registry_package(raw: str, product: str, version: str) -> RegistryPackage: - kind, separator, name = raw.partition(":") - if separator != ":" or not kind or not name: - fail(f"{product}.registry_packages entry {raw!r} must use kind:name") - if kind not in {"crates", "npm", "jsr", "maven"}: - fail(f"{product}.registry_packages entry {raw!r} has unsupported kind {kind!r}") - return RegistryPackage(kind=kind, name=name, version=version) - - -def graph_registry_packages( - product: str, - graph: dict | None = None, - *, - version_override: str | None = None, -) -> list[RegistryPackage]: - data = graph if graph is not None else product_metadata.load_graph() - config = product_metadata.product_config(product, data) - version = version_override or product_metadata.read_current_version(product) - raw_packages = product_metadata.string_list(config, "registry_packages", product) - return [ - parse_registry_package(raw_package, product, version) - for raw_package in raw_packages - ] - - -def derived_crates_packages(product: str) -> list[RegistryPackage]: - version, crates, _, _ = check_cratesio_publication.query_crates(product) - return [ - RegistryPackage(kind="crates", name=crate, version=version) - for crate in crates - ] - - -def derived_exact_extension_maven_packages(product: str, version: str) -> list[RegistryPackage]: - config = product_metadata.product_config(product) - if config.get("kind") != "exact-extension-artifact": - return [] - return [ - RegistryPackage( - kind="maven", - name=f"dev.oliphaunt.extensions:{product}-{target.target}", - version=version, - ) - for target in extension_artifact_targets.published_android_maven_targets(product) - ] - - -def product_registry_packages( - product: str, - graph: dict | None = None, - *, - version_override: str | None = None, - registry_kind: str | None = None, -) -> list[RegistryPackage]: - data = graph if graph is not None else product_metadata.load_graph() - config = product_metadata.product_config(product, data) - version = version_override or product_metadata.read_current_version(product) - publish_targets = set(product_metadata.string_list(config, "publish_targets", product)) - graph_packages = graph_registry_packages(product, data, version_override=version_override) - allowed_graph_kinds: set[str] = set() - if "crates-io" in publish_targets: - allowed_graph_kinds.add("crates") - expected_kinds = { - "npm": "npm", - "jsr": "jsr", - "maven-central": "maven", - } - allowed_graph_kinds.update(kind for target, kind in expected_kinds.items() if target in publish_targets) - stale_packages = sorted( - f"{package.kind}:{package.name}" - for package in graph_packages - if package.kind not in allowed_graph_kinds - ) - if stale_packages: - fail( - f"{product}.registry_packages contains entries without a matching registry publish target: " - + ", ".join(stale_packages) - ) - packages = list(graph_packages) - if "crates-io" in publish_targets: - derived_crates = derived_crates_packages(product) - if version_override is not None: - derived_crates = [ - RegistryPackage(kind=package.kind, name=package.name, version=version_override) - for package in derived_crates - ] - graph_crates = [package for package in packages if package.kind == "crates"] - if graph_crates: - derived_names = sorted(package.name for package in derived_crates) - graph_names = sorted(package.name for package in graph_crates) - if graph_names != derived_names: - fail( - f"{product}.registry_packages crates entries {graph_names} " - f"do not match Cargo manifests {derived_names}" - ) - else: - packages.extend(derived_crates) - derived_extension_maven = derived_exact_extension_maven_packages(product, version) - if derived_extension_maven: - graph_maven = [package for package in packages if package.kind == "maven"] - derived_names = sorted(package.name for package in derived_extension_maven) - graph_names = sorted(package.name for package in graph_maven) - if graph_names != derived_names: - fail( - f"{product}.registry_packages maven entries {graph_names} " - f"do not match exact-extension Android artifact targets {derived_names}" - ) - missing_kinds = [] - for target, kind in expected_kinds.items(): - if target in publish_targets and not any(package.kind == kind for package in packages): - missing_kinds.append(kind) - if missing_kinds: - fail( - f"{product} publishes to {sorted(publish_targets & REGISTRY_TARGETS)} " - f"but is missing registry_packages entries for: {', '.join(missing_kinds)}" - ) - if registry_kind is not None: - packages = [package for package in packages if package.kind == registry_kind] - if not packages: - fail(f"{product} has no {registry_kind} registry packages to check") - return packages - - -def query_product_publication( - product: str, - *, - version_override: str | None = None, - registry_kind: str | None = None, - retries: int = 0, - retry_delay: float = 0.0, -) -> tuple[list[RegistryPackage], list[RegistryPackage], list[RegistryPackage]]: - packages = product_registry_packages( - product, - version_override=version_override, - registry_kind=registry_kind, - ) - if not packages: - return [], [], [] - - attempts = max(1, retries + 1) - last_missing: list[RegistryPackage] = [] - last_published: list[RegistryPackage] = [] - for attempt in range(attempts): - missing: list[RegistryPackage] = [] - published: list[RegistryPackage] = [] - for package in packages: - if package_exists(package): - published.append(package) - else: - missing.append(package) - last_missing = missing - last_published = published - if not missing or attempt == attempts - 1: - break - if retry_delay > 0: - time.sleep(retry_delay) - return packages, last_missing, last_published - - -def assert_product_publication( - product: str, - *, - require_published: bool, - version_override: str | None = None, - registry_kind: str | None = None, - retries: int = 0, - retry_delay: float = 0.0, -) -> None: - packages, missing, published = query_product_publication( - product, - version_override=version_override, - registry_kind=registry_kind, - retries=retries, - retry_delay=retry_delay, - ) - if not packages: - print(f"{product} has no external registry packages to check") - return - if require_published and missing: - fail( - f"{product} registry publication is missing: " - + ", ".join(package.label for package in missing) - ) - if not require_published and published: - fail( - f"{product} version is already published in public registries: " - + ", ".join(package.label for package in published) - ) - state = "published" if require_published else "unpublished" - print( - f"{product} registry {state} check passed: " - + ", ".join(package.label for package in packages) - ) - - -def report_product_publication( - product: str, - *, - version_override: str | None = None, - registry_kind: str | None = None, -) -> None: - packages, missing, published = query_product_publication( - product, - version_override=version_override, - registry_kind=registry_kind, - ) - if not packages: - print(f"{product} has no external registry packages to check") - return - if published: - print( - f"{product} registry versions already present: " - + ", ".join(package.label for package in published) - ) - if missing: - print( - f"{product} registry versions not yet present: " - + ", ".join(package.label for package in missing) - ) - - -def product_identity_status( - product: str, - *, - registry_kind: str | None = None, -) -> tuple[list[RegistryPackage], list[RegistryPackage], list[RegistryPackage]]: - packages = product_registry_packages(product, registry_kind=registry_kind) - present: list[RegistryPackage] = [] - missing: list[RegistryPackage] = [] - for package in packages: - if package_identity_exists(package): - present.append(package) - else: - missing.append(package) - return packages, present, missing - - -def assert_product_identities( - product: str, - *, - registry_kind: str | None = None, -) -> None: - packages, _, missing = product_identity_status(product, registry_kind=registry_kind) - if not packages: - print(f"{product} has no external registry package identities to check") - return - if missing: - fail( - f"{product} registry package identities are missing: " - + ", ".join(f"{package.kind}:{package.name}" for package in missing) - ) - print( - f"{product} registry identity check passed: " - + ", ".join(f"{package.kind}:{package.name}" for package in packages) - ) - - -def report_product_identities( - product: str, - *, - registry_kind: str | None = None, -) -> None: - packages, present, missing = product_identity_status(product, registry_kind=registry_kind) - if not packages: - print(f"{product} has no external registry package identities to check") - return - if present: - print( - f"{product} registry identities present: " - + ", ".join(f"{package.kind}:{package.name}" for package in present) - ) - if missing: - print( - f"{product} registry identities missing: " - + ", ".join(f"{package.kind}:{package.name}" for package in missing) - ) - - -def parse_products(raw: str | None, product: str | None) -> list[str]: - if bool(raw) == bool(product): - fail("pass exactly one of --product or --products-json") - if product: - return [product] - value = json.loads(raw or "") - if not isinstance(value, list) or not all(isinstance(item, str) for item in value): - fail("--products-json must be a JSON string list") - known = set(product_metadata.product_ids()) - unknown = sorted(set(value) - known) - if unknown: - fail(f"unknown release products: {', '.join(unknown)}") - return value - - -def parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--product", help="single release product id") - parser.add_argument("--products-json", help="JSON list of release product ids") - parser.add_argument( - "--version", - help="override the product version to check; valid only with --product", - ) - parser.add_argument( - "--registry-kind", - choices=["crates", "npm", "jsr", "maven"], - help="restrict checks to one registry package kind for the selected product", - ) - mode = parser.add_mutually_exclusive_group(required=True) - mode.add_argument("--require-published", action="store_true") - mode.add_argument("--require-unpublished", action="store_true") - mode.add_argument("--report", action="store_true") - mode.add_argument("--require-identities", action="store_true") - mode.add_argument("--report-identities", action="store_true") - parser.add_argument( - "--retries", - type=int, - default=0, - help="additional registry query attempts before failing", - ) - parser.add_argument( - "--retry-delay", - type=float, - default=0.0, - help="seconds to sleep between retry attempts", - ) - return parser.parse_args(argv) - - -def main(argv: list[str]) -> int: - args = parse_args(argv) - if args.version and not args.product: - fail("--version can only be used with --product") - products = parse_products(args.products_json, args.product) - if args.retries < 0: - fail("--retries must be non-negative") - if args.retry_delay < 0: - fail("--retry-delay must be non-negative") - if args.require_identities: - missing_messages: list[str] = [] - for product in products: - packages, _, missing = product_identity_status(product, registry_kind=args.registry_kind) - if not packages: - print(f"{product} has no external registry package identities to check") - continue - if missing: - missing_messages.append( - f"{product}: " - + ", ".join(f"{package.kind}:{package.name}" for package in missing) - ) - else: - print( - f"{product} registry identity check passed: " - + ", ".join(f"{package.kind}:{package.name}" for package in packages) - ) - if missing_messages: - fail("registry package identities are missing:\n - " + "\n - ".join(missing_messages)) - return 0 - - for product in products: - if args.report_identities: - report_product_identities(product, registry_kind=args.registry_kind) - elif args.report: - report_product_publication( - product, - version_override=args.version, - registry_kind=args.registry_kind, - ) - else: - assert_product_publication( - product, - require_published=args.require_published, - version_override=args.version, - registry_kind=args.registry_kind, - retries=args.retries, - retry_delay=args.retry_delay, - ) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) diff --git a/tools/release/check_release_versions.py b/tools/release/check_release_versions.py index bf3ac47c..d215d84b 100755 --- a/tools/release/check_release_versions.py +++ b/tools/release/check_release_versions.py @@ -12,12 +12,17 @@ from typing import NoReturn import check_github_release_assets -import check_registry_publication import product_metadata import release_plan ROOT = Path(__file__).resolve().parents[2] +REGISTRY_TARGETS = { + "crates-io", + "npm", + "jsr", + "maven-central", +} def fail(message: str) -> NoReturn: @@ -55,6 +60,58 @@ def git_output(args: list[str]) -> str: return subprocess.check_output(["git", *args], cwd=ROOT, text=True).strip() +def registry_command(args: list[str]) -> list[str]: + return [ + "tools/dev/bun.sh", + "tools/release/check_registry_publication.mjs", + *args, + ] + + +def registry_run(args: list[str]) -> None: + result = subprocess.run(registry_command(args), cwd=ROOT, check=False) + if result.returncode != 0: + raise SystemExit(result.returncode) + + +def registry_json(args: list[str]) -> dict: + output = subprocess.check_output(registry_command(args), cwd=ROOT, text=True) + value = json.loads(output) + if not isinstance(value, dict): + fail("registry publication helper did not return a JSON object") + return value + + +def registry_assert_product_publication( + product: str, + *, + require_published: bool, + version_override: str | None = None, +) -> None: + args = [ + "--product", + product, + "--require-published" if require_published else "--require-unpublished", + ] + if version_override is not None: + args.extend(["--version", version_override]) + registry_run(args) + + +def registry_report_product_publication(product: str) -> None: + registry_run(["--product", product, "--report"]) + + +def registry_query_product_publication(product: str) -> tuple[list[dict], list[dict], list[dict]]: + data = registry_json(["query-product-publication", "--product", product]) + packages = data.get("packages") + missing = data.get("missing") + published = data.get("published") + if not isinstance(packages, list) or not isinstance(missing, list) or not isinstance(published, list): + fail("registry publication helper returned malformed publication status") + return packages, missing, published + + def tag_match_pattern(prefix: str) -> str: return f"{prefix}[0-9]*" if prefix else "[0-9]*" @@ -211,19 +268,19 @@ def validate_registry_publication( targets = config.get("publish_targets", []) if not isinstance(targets, list) or not all(isinstance(item, str) for item in targets): fail(f"{product}.publish_targets must be a string list") - registry_targets = set(targets) & check_registry_publication.REGISTRY_TARGETS + registry_targets = set(targets) & REGISTRY_TARGETS if not registry_targets: continue if current_tag_at_head.get(product, False): if "crates-io" in registry_targets: - check_registry_publication.assert_product_publication( + registry_assert_product_publication( product, require_published=True, ) else: - check_registry_publication.report_product_publication(product) + registry_report_product_publication(product) continue - packages, _, published = check_registry_publication.query_product_publication(product) + packages, _, published = registry_query_product_publication(product) if not packages: print(f"{product} has no external registry packages to check") continue @@ -235,7 +292,7 @@ def validate_registry_publication( current_tag = f"{prefix}{version}" fail( f"{product} version {version} is already published in public registries: " - + ", ".join(package.label for package in published) + + ", ".join(str(package["label"]) for package in published) + f"; the matching product tag {current_tag} is missing or does not " f"point at release commit {head_commit}. If this was an intentional " "first package identity bootstrap, create and push that product tag at " @@ -244,7 +301,7 @@ def validate_registry_publication( ) print( f"{product} registry unpublished check passed: " - + ", ".join(package.label for package in packages) + + ", ".join(str(package["label"]) for package in packages) ) @@ -285,9 +342,9 @@ def validate_released_dependency_artifacts( targets = dependency_config.get("publish_targets", []) if not isinstance(targets, list) or not all(isinstance(item, str) for item in targets): fail(f"{dependency}.publish_targets must be a string list") - registry_targets = set(targets) & check_registry_publication.REGISTRY_TARGETS + registry_targets = set(targets) & REGISTRY_TARGETS if registry_targets: - check_registry_publication.assert_product_publication( + registry_assert_product_publication( dependency, require_published=True, version_override=dependency_version, diff --git a/tools/release/release.py b/tools/release/release.py index 6f4cd260..c8aca5b9 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -18,7 +18,6 @@ from typing import NoReturn import artifact_targets -import check_cratesio_publication import extension_artifact_targets import optimize_native_runtime_payload import package_liboliphaunt_cargo_artifacts @@ -30,6 +29,10 @@ ROOT = Path(__file__).resolve().parents[2] EXTENSION_PRODUCT_PREFIX = "oliphaunt-extension-" NODE_DIRECT_PACKAGE_ROOT = ROOT / "src/runtimes/node-direct/packages" +REGISTRY_PUBLICATION_CHECK = [ + "tools/dev/bun.sh", + "tools/release/check_registry_publication.mjs", +] def fail(message: str) -> NoReturn: @@ -53,6 +56,39 @@ def succeeds(args: list[str], *, cwd: Path = ROOT) -> bool: return result.returncode == 0 +def registry_check_args(*args: str) -> list[str]: + return [*REGISTRY_PUBLICATION_CHECK, *args] + + +def registry_check_json(*args: str) -> dict: + value = json.loads(output(registry_check_args(*args))) + if not isinstance(value, dict): + fail("registry publication helper did not return a JSON object") + return value + + +def cratesio_product_crates(product: str) -> list[str]: + value = registry_check_json("product-crates", "--product", product) + crates = value.get("crates") + if not isinstance(crates, list) or not all(isinstance(crate, str) for crate in crates): + fail(f"registry publication helper returned invalid crates for {product}") + return crates + + +def cratesio_crate_version_exists(crate: str, version: str) -> bool: + value = registry_check_json( + "crate-version-exists", + "--crate", + crate, + "--version", + version, + ) + exists = value.get("exists") + if not isinstance(exists, bool): + fail(f"registry publication helper returned invalid crates.io status for {crate} {version}") + return exists + + def pnpm_pack_for_npm_publish(package_dir: Path) -> Path: """Pack with pnpm so workspace: dependency specs become publishable versions.""" @@ -181,7 +217,7 @@ def verify_staged_cargo_crate_identity( def verify_staged_cargo_product_crates(product: str, version: str, *, allow_dirty: bool) -> None: - crates = check_cratesio_publication.product_crates(product) + crates = cratesio_product_crates(product) for crate in crates: verify_staged_cargo_crate_identity(product, crate, version, allow_dirty=allow_dirty) staged_names = sorted(path.name for path in staged_cargo_crates(product)) @@ -525,12 +561,11 @@ def product_tag_points_at(product: str, head_ref: str) -> bool: def product_registry_is_published(product: str) -> bool: return succeeds( - [ - "tools/release/check_registry_publication.py", + registry_check_args( "--product", product, "--require-published", - ] + ) ) @@ -540,7 +575,7 @@ def published_rerun(product: str, head_ref: str) -> bool: def wait_for_cratesio_package(crate: str, version: str, *, retries: int = 12, retry_delay: float = 10.0) -> None: for attempt in range(retries + 1): - if check_cratesio_publication.crate_version_exists(crate, version): + if cratesio_crate_version_exists(crate, version): return if attempt < retries: print(f"waiting for crates.io to index {crate} {version}...") @@ -561,7 +596,7 @@ def verify_generated_cratesio_packages_published(product: str, crates: list[str] def cargo_publish_package(package: str, version: str, *, allow_dirty: bool = False) -> None: - if check_cratesio_publication.crate_version_exists(package, version): + if cratesio_crate_version_exists(package, version): print(f"{package} {version} is already published on crates.io; skipping cargo publish.") return run( @@ -578,7 +613,7 @@ def cargo_publish_package(package: str, version: str, *, allow_dirty: bool = Fal def cargo_publish_manifest(package: str, version: str, manifest_path: Path, *, allow_dirty: bool = False) -> None: - if check_cratesio_publication.crate_version_exists(package, version): + if cratesio_crate_version_exists(package, version): print(f"{package} {version} is already published on crates.io; skipping cargo publish.") return run( @@ -1133,7 +1168,7 @@ def publish_wasm_crates_io(head_ref: str) -> None: verify_release_tag("oliphaunt-wasix-rust", head_ref) run( [ - "tools/release/check_registry_publication.py", + *REGISTRY_PUBLICATION_CHECK, "--product", "liboliphaunt-wasix", "--registry-kind", @@ -1152,7 +1187,7 @@ def publish_wasm_crates_io(head_ref: str) -> None: cargo_publish_manifest("oliphaunt-wasix", version, release_manifest) run( [ - "tools/release/check_registry_publication.py", + *REGISTRY_PUBLICATION_CHECK, "--product", "oliphaunt-wasix-rust", "--require-published", @@ -1706,7 +1741,7 @@ def command_check_registries(args: list[str]) -> None: fail("check-registries --require-identities requires --products-json") run( [ - "tools/release/check_registry_publication.py", + *REGISTRY_PUBLICATION_CHECK, "--products-json", products_json, "--require-identities", @@ -1839,7 +1874,7 @@ def publish_kotlin_maven(head_ref: str) -> None: ) run( [ - "tools/release/check_registry_publication.py", + *REGISTRY_PUBLICATION_CHECK, "--product", "oliphaunt-kotlin", "--require-published", @@ -1859,7 +1894,7 @@ def publish_liboliphaunt_runtime_maven(head_ref: str) -> None: version = current_product_version("liboliphaunt-native") if succeeds( [ - "tools/release/check_registry_publication.py", + *REGISTRY_PUBLICATION_CHECK, "--product", "liboliphaunt-native", "--registry-kind", @@ -1876,7 +1911,7 @@ def publish_liboliphaunt_runtime_maven(head_ref: str) -> None: ) run( [ - "tools/release/check_registry_publication.py", + *REGISTRY_PUBLICATION_CHECK, "--product", "liboliphaunt-native", "--registry-kind", @@ -1902,7 +1937,7 @@ def publish_react_native_npm(head_ref: str) -> None: ) run( [ - "tools/release/check_registry_publication.py", + *REGISTRY_PUBLICATION_CHECK, "--product", "oliphaunt-react-native", "--require-published", @@ -1926,7 +1961,7 @@ def publish_rust_crates_io(head_ref: str) -> None: native_version = current_product_version("liboliphaunt-native") run( [ - "tools/release/check_registry_publication.py", + *REGISTRY_PUBLICATION_CHECK, "--product", "liboliphaunt-native", "--registry-kind", @@ -1938,7 +1973,7 @@ def publish_rust_crates_io(head_ref: str) -> None: ) run( [ - "tools/release/check_registry_publication.py", + *REGISTRY_PUBLICATION_CHECK, "--product", "oliphaunt-broker", "--registry-kind", @@ -1954,7 +1989,7 @@ def publish_rust_crates_io(head_ref: str) -> None: cargo_publish_manifest("oliphaunt", version, release_manifest) run( [ - "tools/release/check_registry_publication.py", + *REGISTRY_PUBLICATION_CHECK, "--product", "oliphaunt-rust", "--require-published", @@ -2669,7 +2704,7 @@ def broker_cargo_artifact_crates(version: str) -> list[tuple[str, Path, Path]]: published_only=True, ) } - configured_crates = set(check_cratesio_publication.product_crates("oliphaunt-broker")) + configured_crates = set(cratesio_product_crates("oliphaunt-broker")) if configured_crates != expected_crates: fail( "oliphaunt-broker crates.io packages must match broker artifact targets: " @@ -2733,7 +2768,7 @@ def liboliphaunt_cargo_artifact_crates(version: str) -> list[tuple[str, Path | N ) for target in native_targets } - configured_crates = set(check_cratesio_publication.product_crates("liboliphaunt-native")) + configured_crates = set(cratesio_product_crates("liboliphaunt-native")) if configured_crates != expected_aggregators: fail( "liboliphaunt-native crates.io packages must match native Rust runtime/tool artifact targets: " @@ -2807,7 +2842,7 @@ def liboliphaunt_wasix_cargo_artifact_crates(version: str) -> list[tuple[str, Pa expected_base_crates = set( package_liboliphaunt_wasix_cargo_artifacts.public_cargo_package_names() ) - configured_crates = set(check_cratesio_publication.product_crates("liboliphaunt-wasix")) + configured_crates = set(cratesio_product_crates("liboliphaunt-wasix")) if configured_crates != expected_base_crates: fail( "liboliphaunt-wasix crates.io packages must match WASIX runtime/AOT artifact packages: " @@ -2882,7 +2917,7 @@ def publish_liboliphaunt_cargo_artifacts(head_ref: str) -> None: ) run( [ - "tools/release/check_registry_publication.py", + *REGISTRY_PUBLICATION_CHECK, "--product", "liboliphaunt-native", "--registry-kind", @@ -2909,7 +2944,7 @@ def publish_liboliphaunt_wasix_cargo_artifacts(head_ref: str) -> None: ) run( [ - "tools/release/check_registry_publication.py", + *REGISTRY_PUBLICATION_CHECK, "--product", "liboliphaunt-wasix", "--registry-kind", @@ -2930,7 +2965,7 @@ def publish_broker_cargo_artifacts(head_ref: str) -> None: cargo_publish_manifest(crate, version, manifest_path) run( [ - "tools/release/check_registry_publication.py", + *REGISTRY_PUBLICATION_CHECK, "--product", "oliphaunt-broker", "--registry-kind", @@ -2956,7 +2991,7 @@ def publish_node_direct_npm_optional_packages(head_ref: str) -> None: run(["npm", "publish", str(tarball), "--access", "public", "--provenance"]) run( [ - "tools/release/check_registry_publication.py", + *REGISTRY_PUBLICATION_CHECK, "--product", "oliphaunt-node-direct", "--require-published", @@ -2974,7 +3009,7 @@ def publish_liboliphaunt_npm_packages(head_ref: str) -> None: npm_publish_packages(liboliphaunt_npm_tarballs(version), version) run( [ - "tools/release/check_registry_publication.py", + *REGISTRY_PUBLICATION_CHECK, "--product", "liboliphaunt-native", "--registry-kind", @@ -2994,7 +3029,7 @@ def publish_broker_npm_packages(head_ref: str) -> None: npm_publish_packages(broker_npm_tarballs(version), version) run( [ - "tools/release/check_registry_publication.py", + *REGISTRY_PUBLICATION_CHECK, "--product", "oliphaunt-broker", "--registry-kind", @@ -3027,7 +3062,7 @@ def publish_typescript_npm_jsr(head_ref: str) -> None: npm_publish_pnpm_packed_package(ROOT / "src/sdks/js", product="oliphaunt-js") if succeeds( [ - "tools/release/check_registry_publication.py", + *REGISTRY_PUBLICATION_CHECK, "--product", "oliphaunt-js", "--registry-kind", @@ -3041,7 +3076,7 @@ def publish_typescript_npm_jsr(head_ref: str) -> None: run(["pnpm", "exec", "jsr", "publish"], cwd=jsr_source) run( [ - "tools/release/check_registry_publication.py", + *REGISTRY_PUBLICATION_CHECK, "--product", "oliphaunt-js", "--require-published", @@ -3078,7 +3113,7 @@ def publish_selected_extension_release_assets(products: list[str], head_ref: str def extension_maven_artifacts_published(products: list[str]) -> bool: return succeeds( [ - "tools/release/check_registry_publication.py", + *REGISTRY_PUBLICATION_CHECK, "--products-json", json.dumps(products), "--registry-kind", @@ -3091,7 +3126,7 @@ def extension_maven_artifacts_published(products: list[str]) -> bool: def require_extension_maven_artifacts_published(products: list[str]) -> None: run( [ - "tools/release/check_registry_publication.py", + *REGISTRY_PUBLICATION_CHECK, "--products-json", json.dumps(products), "--registry-kind", From fad1fc74838397d9dbe2b9cf32f40cf613be98f1 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 23:14:23 +0000 Subject: [PATCH 131/137] chore: port github release asset check to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 5 + tools/policy/python-entrypoints.allowlist | 1 - tools/release/check_artifact_targets.py | 8 +- tools/release/check_github_release_assets.mjs | 74 +++ tools/release/check_github_release_assets.py | 423 ------------------ tools/release/check_release_versions.py | 24 +- .../verify_github_release_attestations.mjs | 2 +- 7 files changed, 102 insertions(+), 435 deletions(-) create mode 100644 tools/release/check_github_release_assets.mjs delete mode 100755 tools/release/check_github_release_assets.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 366fa161..c514b7a3 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -1018,3 +1018,8 @@ until the current-state gates here are checked with fresh local evidence. the still-Python release orchestrators. Representative Python/Bun parity checks passed for `oliphaunt-js` npm/JSR and `oliphaunt-rust` crates.io report modes before the retired Python entrypoints were removed. +- On 2026-06-26, the product-scoped GitHub release asset checker moved from + Python to Bun. The new `check_github_release_assets.mjs` reuses the shared + expected-asset and exact-extension manifest validation from the attestation + verifier, while `check_release_versions.py` now shells to the Bun checker for + released dependency asset verification. diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index ff24fb1f..af10ea85 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -9,7 +9,6 @@ tools/release/artifact_targets.py tools/release/build-extension-ci-artifacts.py tools/release/check_artifact_targets.py tools/release/check_consumer_shape.py -tools/release/check_github_release_assets.py tools/release/check_liboliphaunt_release_assets.py tools/release/check_release_metadata.py tools/release/check_release_versions.py diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py index eb443b41..63a3243d 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -261,8 +261,8 @@ def validate_github_asset_helpers() -> None: "macOS liboliphaunt target packager must write into the release asset directory", ) require_text( - "tools/release/check_github_release_assets.py", - "artifact_targets.expected_assets", + "tools/release/check_github_release_assets.mjs", + "expectedAssets", "GitHub release asset checks must derive product assets from product-local artifact targets", ) require_text( @@ -763,8 +763,8 @@ def validate_ci_release_artifacts() -> None: "exact-extension package artifacts must publish a machine-readable release manifest", ) require_text( - "tools/release/check_github_release_assets.py", - "expected_extension_assets", + "tools/release/check_github_release_assets.mjs", + "verifyReleaseAssets", "GitHub release verification must derive exact-extension asset expectations from staged extension package manifests", ) require_text( diff --git a/tools/release/check_github_release_assets.mjs b/tools/release/check_github_release_assets.mjs new file mode 100644 index 00000000..77ee9720 --- /dev/null +++ b/tools/release/check_github_release_assets.mjs @@ -0,0 +1,74 @@ +#!/usr/bin/env bun +// Verify product-scoped GitHub release assets without requiring attestations. + +import { currentVersion } from "./product-version.mjs"; +import { + expectedAssets, + verifyReleaseAssets, +} from "./verify_github_release_attestations.mjs"; + +function fail(message) { + console.error(`check_github_release_assets.mjs: ${message}`); + process.exit(1); +} + +function parseArgs(argv) { + const args = { + asset: [], + defaultAssets: false, + product: undefined, + version: undefined, + }; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === "--asset") { + const asset = argv[++index]; + if (!asset) { + fail("--asset requires a value"); + } + args.asset.push(asset); + } else if (value.startsWith("--asset=")) { + args.asset.push(value.slice("--asset=".length)); + } else if (value === "--default-assets") { + args.defaultAssets = true; + } else if (value === "--version") { + args.version = argv[++index]; + if (!args.version) { + fail("--version requires a value"); + } + } else if (value.startsWith("--version=")) { + args.version = value.slice("--version=".length); + } else if (value === "--help" || value === "-h") { + console.log("usage: tools/release/check_github_release_assets.mjs [--version VERSION] [--default-assets] [--asset NAME...]"); + process.exit(0); + } else if (value.startsWith("--")) { + fail(`unknown argument ${value}`); + } else if (args.product === undefined) { + args.product = value; + } else { + fail(`unexpected positional argument ${value}`); + } + } + if (args.product === undefined) { + fail("product is required"); + } + return args; +} + +async function main(argv) { + const args = parseArgs(argv); + const version = args.version ?? await currentVersion(args.product); + const assets = [...args.asset]; + if (args.defaultAssets) { + assets.push(...await expectedAssets(args.product, version)); + } + const uniqueAssets = [...new Set(assets)].sort(); + if (uniqueAssets.length === 0) { + fail("pass --default-assets or at least one --asset"); + } + await verifyReleaseAssets(args.product, version, uniqueAssets); +} + +if (import.meta.main) { + await main(Bun.argv.slice(2)); +} diff --git a/tools/release/check_github_release_assets.py b/tools/release/check_github_release_assets.py deleted file mode 100755 index dd699a72..00000000 --- a/tools/release/check_github_release_assets.py +++ /dev/null @@ -1,423 +0,0 @@ -#!/usr/bin/env python3 -"""Verify product-scoped GitHub release assets.""" - -from __future__ import annotations - -import argparse -import hashlib -import json -import os -from pathlib import Path -import sys -import urllib.error -import urllib.parse -import urllib.request -from typing import NoReturn - -import artifact_targets -import extension_artifact_targets -import product_metadata - - -GITHUB_API = os.environ.get("GITHUB_API", "https://api.github.com") - - -def fail(message: str) -> NoReturn: - print(f"check_github_release_assets.py: {message}", file=sys.stderr) - raise SystemExit(1) - - -def repository() -> str: - repo = os.environ.get("GITHUB_REPOSITORY") - if repo: - return repo - graph = product_metadata.load_graph() - policy = graph.get("policy") - if isinstance(policy, dict) and isinstance(policy.get("repository"), str): - return policy["repository"] - fail("GITHUB_REPOSITORY is not set and release metadata has no policy.repository") - - -def product_tag(product: str, version: str) -> str: - return f"{product_metadata.tag_prefix(product)}{version}" - - -def expected_assets(product: str, version: str) -> list[str]: - config = product_metadata.product_config(product) - if config.get("kind") == "exact-extension-artifact": - return expected_extension_assets(product, version) - return artifact_targets.expected_assets(product, version, surface="github-release") - - -def expected_extension_assets(product: str, version: str) -> list[str]: - release_asset_root = Path("target") / "extension-artifacts" / product / "release-assets" - manifest_path = release_asset_root / f"{product}-{version}-manifest.json" - if not manifest_path.is_file(): - fail( - f"{product} exact-extension release verification requires staged public release manifest " - f"{manifest_path}; download the CI workflow oliphaunt-extension-package-artifacts artifact first" - ) - manifest = json.loads(manifest_path.read_text(encoding="utf-8")) - expected = { - "schema": "oliphaunt-extension-release-manifest-v1", - "product": product, - "version": version, - } - for key, value in expected.items(): - if manifest.get(key) != value: - fail(f"{manifest_path} has {key}={manifest.get(key)!r}, expected {value!r}") - actual_keys = set(manifest) - expected_keys = product_metadata.PUBLIC_EXTENSION_RELEASE_MANIFEST_KEYS - if actual_keys != expected_keys: - fail(f"{manifest_path} public manifest keys must be {sorted(expected_keys)}, got {sorted(actual_keys)}") - assets = manifest.get("assets") - if not isinstance(assets, list): - fail(f"{manifest_path} must contain an assets array") - names: list[str] = [] - for index, asset in enumerate(assets): - if not isinstance(asset, dict): - fail(f"{manifest_path} assets[{index}] must be an object") - name = asset.get("name") - if not isinstance(name, str) or not name: - fail(f"{manifest_path} assets[{index}] must declare name") - actual_asset_keys = set(asset) - expected_asset_keys = product_metadata.PUBLIC_EXTENSION_RELEASE_ASSET_KEYS - if actual_asset_keys != expected_asset_keys: - fail( - f"{manifest_path} assets[{index}] keys must be " - f"{sorted(expected_asset_keys)}, got {sorted(actual_asset_keys)}" - ) - names.append(name) - if not names: - fail(f"{manifest_path} does not declare any release assets") - names.extend( - [ - f"{product}-{version}-manifest.json", - f"{product}-{version}-manifest.properties", - f"{product}-{version}-release-assets.sha256", - ] - ) - return sorted(set(names)) - - -def request_bytes(url: str) -> bytes: - headers = { - "Accept": "application/octet-stream", - "User-Agent": "oliphaunt-release-check", - "X-GitHub-Api-Version": "2022-11-28", - } - token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN") - if token: - headers["Authorization"] = f"Bearer {token}" - request = urllib.request.Request(url, headers=headers) - try: - with urllib.request.urlopen(request, timeout=60) as response: - return response.read() - except urllib.error.HTTPError as error: - fail(f"GitHub asset download returned HTTP {error.code} for {url}") - except urllib.error.URLError as error: - fail(f"failed to download GitHub asset {url}: {error}") - - -def sha256_bytes(data: bytes) -> str: - return hashlib.sha256(data).hexdigest() - - -def parse_checksum_manifest(data: bytes, context: str) -> dict[str, str]: - checksums: dict[str, str] = {} - text = data.decode("utf-8") - for line_number, raw_line in enumerate(text.splitlines(), start=1): - line = raw_line.strip() - if not line: - continue - parts = line.split(None, 1) - if len(parts) != 2: - fail(f"{context}:{line_number} must contain ' ./'") - sha, name = parts - if len(sha) != 64 or any(char not in "0123456789abcdef" for char in sha): - fail(f"{context}:{line_number} has invalid sha256 {sha!r}") - if not name.startswith("./") or "/" in name[2:]: - fail(f"{context}:{line_number} must reference a direct asset path like ./name") - asset_name = name[2:] - if asset_name in checksums: - fail(f"{context} declares duplicate checksum entry for {asset_name}") - checksums[asset_name] = sha - return checksums - - -def github_json(url: str) -> object: - headers = { - "Accept": "application/vnd.github+json", - "User-Agent": "oliphaunt-release-check", - "X-GitHub-Api-Version": "2022-11-28", - } - token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN") - if token: - headers["Authorization"] = f"Bearer {token}" - request = urllib.request.Request(url, headers=headers) - try: - with urllib.request.urlopen(request, timeout=20) as response: - return json.load(response) - except urllib.error.HTTPError as error: - if error.code == 404: - fail(f"GitHub release not found for URL {url}") - fail(f"GitHub API returned HTTP {error.code} for {url}") - except urllib.error.URLError as error: - fail(f"failed to query GitHub release URL {url}: {error}") - - -def release_assets(repo: str, tag: str) -> dict[str, dict]: - repo_path = urllib.parse.quote(repo, safe="/") - tag_path = urllib.parse.quote(tag, safe="") - url = f"{GITHUB_API.rstrip('/')}/repos/{repo_path}/releases/tags/{tag_path}" - data = github_json(url) - if not isinstance(data, dict): - fail(f"GitHub release response for {tag} was not an object") - assets = data.get("assets") - if not isinstance(assets, list): - fail(f"GitHub release response for {tag} did not include assets") - parsed: dict[str, dict] = {} - for asset in assets: - if not isinstance(asset, dict) or not isinstance(asset.get("name"), str): - continue - name = asset["name"] - if name in parsed: - fail(f"GitHub release {tag} declares duplicate asset {name}") - parsed[name] = asset - return parsed - - -def release_asset_names(repo: str, tag: str) -> list[str]: - return sorted(release_assets(repo, tag)) - - -def download_asset(asset: dict, name: str) -> bytes: - url = asset.get("url") - if not isinstance(url, str) or not url: - fail(f"GitHub release asset {name} did not include an API download URL") - return request_bytes(url) - - -def extension_artifact_kind_allowed(family: str, target: str, kind: str) -> bool: - if family == "wasix": - return target == "wasix-portable" and kind == "wasix-runtime" - if family != "native": - return False - if target == "ios-xcframework": - return kind in {"runtime", "ios-xcframework"} - if target.startswith("android-"): - return kind in {"runtime", "android-static-archive"} - return kind == "runtime" - - -def validate_extension_public_manifest(product: str, version: str, manifest: object) -> list[dict]: - if not isinstance(manifest, dict): - fail(f"{product} {version} public extension manifest must be a JSON object") - expected = { - "schema": "oliphaunt-extension-release-manifest-v1", - "product": product, - "version": version, - } - for key, value in expected.items(): - if manifest.get(key) != value: - fail(f"{product} {version} public extension manifest has {key}={manifest.get(key)!r}, expected {value!r}") - actual_keys = set(manifest) - expected_keys = product_metadata.PUBLIC_EXTENSION_RELEASE_MANIFEST_KEYS - if actual_keys != expected_keys: - fail( - f"{product} {version} public extension manifest keys must be " - f"{sorted(expected_keys)}, got {sorted(actual_keys)}" - ) - - rows = manifest.get("assets") - if not isinstance(rows, list) or not rows: - fail(f"{product} {version} public extension manifest must declare assets") - - seen_names: set[str] = set() - staged_targets_by_family: dict[str, set[str]] = {"native": set(), "wasix": set()} - parsed_assets: list[dict] = [] - for index, asset in enumerate(rows): - if not isinstance(asset, dict): - fail(f"{product} {version} public extension manifest assets[{index}] must be an object") - actual_asset_keys = set(asset) - expected_asset_keys = product_metadata.PUBLIC_EXTENSION_RELEASE_ASSET_KEYS - if actual_asset_keys != expected_asset_keys: - fail( - f"{product} {version} public extension manifest assets[{index}] keys must be " - f"{sorted(expected_asset_keys)}, got {sorted(actual_asset_keys)}" - ) - name = asset.get("name") - family = asset.get("family") - target = asset.get("target") - kind = asset.get("kind") - sha = asset.get("sha256") - size = asset.get("bytes") - if not all(isinstance(value, str) and value for value in (name, family, target, kind, sha)): - fail(f"{product} {version} public extension manifest contains an incomplete asset row: {asset!r}") - if not isinstance(size, int) or size <= 0: - fail(f"{product} {version} public extension manifest asset {name} must declare positive bytes") - if len(sha) != 64 or any(char not in "0123456789abcdef" for char in sha): - fail(f"{product} {version} public extension manifest asset {name} has invalid sha256 {sha!r}") - if name in seen_names: - fail(f"{product} {version} public extension manifest declares duplicate asset {name}") - seen_names.add(name) - if not extension_artifact_kind_allowed(family, target, kind): - fail( - f"{product} {version} public extension manifest asset {name} has invalid " - f"family={family!r} target={target!r} kind={kind!r}" - ) - staged_targets_by_family.setdefault(family, set()).add(target) - parsed_assets.append(asset) - - declared_native_targets = { - target.target - for target in extension_artifact_targets.artifact_targets( - product=product, - family="native", - published_only=True, - ) - } - declared_wasix_targets = { - target.target - for target in extension_artifact_targets.artifact_targets( - product=product, - family="wasix", - published_only=True, - ) - } - if staged_targets_by_family["native"] != declared_native_targets: - fail( - f"{product} {version} public extension manifest native targets must match published targets: " - f"{sorted(staged_targets_by_family['native'])} vs {sorted(declared_native_targets)}" - ) - if staged_targets_by_family["wasix"] != declared_wasix_targets: - fail( - f"{product} {version} public extension manifest WASIX targets must match published targets: " - f"{sorted(staged_targets_by_family['wasix'])} vs {sorted(declared_wasix_targets)}" - ) - return parsed_assets - - -def verify_extension_release_assets( - product: str, - version: str, - expected_names: set[str], - actual_assets: dict[str, dict], -) -> None: - actual_names = set(actual_assets) - unexpected = sorted(actual_names - expected_names) - if unexpected: - fail( - f"{product} GitHub release {product_tag(product, version)} has unexpected exact-extension asset(s): " - + ", ".join(unexpected) - ) - - manifest_name = f"{product}-{version}-manifest.json" - properties_name = f"{product}-{version}-manifest.properties" - checksum_name = f"{product}-{version}-release-assets.sha256" - local_manifest_path = Path("target") / "extension-artifacts" / product / "release-assets" / manifest_name - local_manifest = json.loads(local_manifest_path.read_text(encoding="utf-8")) - - downloaded: dict[str, bytes] = {} - manifest_bytes = download_asset(actual_assets[manifest_name], manifest_name) - downloaded[manifest_name] = manifest_bytes - remote_manifest = json.loads(manifest_bytes.decode("utf-8")) - if remote_manifest != local_manifest: - fail(f"{product} GitHub release {product_tag(product, version)} public manifest differs from staged manifest") - public_assets = validate_extension_public_manifest(product, version, remote_manifest) - - checksum_bytes = download_asset(actual_assets[checksum_name], checksum_name) - downloaded[checksum_name] = checksum_bytes - checksums = parse_checksum_manifest(checksum_bytes, checksum_name) - checksum_covered_names = {asset["name"] for asset in public_assets} - checksum_covered_names.add(manifest_name) - checksum_covered_names.add(properties_name) - if set(checksums) != checksum_covered_names: - fail( - f"{product} GitHub release {product_tag(product, version)} checksum manifest must cover " - "release assets exactly: " - f"{sorted(checksums)} vs {sorted(checksum_covered_names)}" - ) - - for name in sorted(checksum_covered_names): - if name not in actual_assets: - fail(f"{product} GitHub release {product_tag(product, version)} is missing checksum-covered asset {name}") - data = downloaded.get(name) - if data is None: - data = download_asset(actual_assets[name], name) - downloaded[name] = data - expected_sha = checksums[name] - actual_sha = sha256_bytes(data) - if actual_sha != expected_sha: - fail(f"{product} GitHub release {product_tag(product, version)} asset {name} checksum mismatch") - remote_size = actual_assets[name].get("size") - if isinstance(remote_size, int) and remote_size != len(data): - fail( - f"{product} GitHub release {product_tag(product, version)} asset {name} size " - f"{remote_size} from GitHub metadata does not match downloaded bytes {len(data)}" - ) - - for asset in public_assets: - name = asset["name"] - data = downloaded[name] - if len(data) != asset["bytes"]: - fail(f"{product} GitHub release {product_tag(product, version)} asset {name} byte size mismatch") - actual_sha = sha256_bytes(data) - if actual_sha != asset["sha256"]: - fail( - f"{product} GitHub release {product_tag(product, version)} asset {name} " - "public manifest checksum mismatch" - ) - - -def verify(product: str, version: str, assets: list[str]) -> None: - repo = repository() - tag = product_tag(product, version) - actual_assets = release_assets(repo, tag) - expected_names = set(assets) - missing = sorted(expected_names - set(actual_assets)) - if missing: - fail( - f"{product} GitHub release {tag} is missing required asset(s): " - + ", ".join(missing) - ) - if product_metadata.product_config(product).get("kind") == "exact-extension-artifact": - verify_extension_release_assets(product, version, expected_names, actual_assets) - print(f"{product} GitHub release assets verified for {tag}: {', '.join(assets)}") - - -def parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("product", help="release product id") - parser.add_argument( - "--version", - help="product version to check; defaults to the current product version", - ) - parser.add_argument( - "--asset", - action="append", - default=[], - help="required asset name; may be passed more than once", - ) - parser.add_argument( - "--default-assets", - action="store_true", - help="check the product's default release asset set", - ) - return parser.parse_args(argv) - - -def main(argv: list[str]) -> int: - args = parse_args(argv) - version = args.version or product_metadata.read_current_version(args.product) - assets = list(args.asset) - if args.default_assets: - assets.extend(expected_assets(args.product, version)) - if not assets: - fail("pass --default-assets or at least one --asset") - verify(args.product, version, sorted(set(assets))) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) diff --git a/tools/release/check_release_versions.py b/tools/release/check_release_versions.py index d215d84b..923fd729 100755 --- a/tools/release/check_release_versions.py +++ b/tools/release/check_release_versions.py @@ -11,7 +11,6 @@ from pathlib import Path from typing import NoReturn -import check_github_release_assets import product_metadata import release_plan @@ -112,6 +111,23 @@ def registry_query_product_publication(product: str) -> tuple[list[dict], list[d return packages, missing, published +def verify_github_release_assets(product: str, version: str) -> None: + result = subprocess.run( + [ + "tools/dev/bun.sh", + "tools/release/check_github_release_assets.mjs", + product, + "--version", + version, + "--default-assets", + ], + cwd=ROOT, + check=False, + ) + if result.returncode != 0: + raise SystemExit(result.returncode) + + def tag_match_pattern(prefix: str) -> str: return f"{prefix}[0-9]*" if prefix else "[0-9]*" @@ -350,11 +366,7 @@ def validate_released_dependency_artifacts( version_override=dependency_version, ) if "github-release-assets" in targets: - check_github_release_assets.verify( - dependency, - dependency_version, - check_github_release_assets.expected_assets(dependency, dependency_version), - ) + verify_github_release_assets(dependency, dependency_version) def validate_release_dependencies(products: list[str], graph: dict) -> None: diff --git a/tools/release/verify_github_release_attestations.mjs b/tools/release/verify_github_release_attestations.mjs index 1a977617..d776f1bf 100755 --- a/tools/release/verify_github_release_attestations.mjs +++ b/tools/release/verify_github_release_attestations.mjs @@ -639,7 +639,7 @@ async function verifyProduct(product, destination) { console.log(`${product} GitHub release attestations verified for ${tag}`); } -export { assetBackedProducts, expectedAssets, productTag }; +export { assetBackedProducts, expectedAssets, productTag, verifyReleaseAssets }; async function main(argv) { const args = parseArgs(argv); From 084ba7280db200cf43947469f1a25d88917633f9 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 23:29:47 +0000 Subject: [PATCH 132/137] chore: port native payload optimizer to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 10 +- tools/policy/check-tooling-stack.sh | 2 +- tools/policy/python-entrypoints.allowlist | 1 - tools/release/check_artifact_targets.py | 2 +- tools/release/check_consumer_shape.py | 42 +- .../check_liboliphaunt_release_assets.py | 23 +- tools/release/check_release_metadata.py | 32 +- .../native-runtime-payload-policy.json | 7 + .../optimize_native_runtime_payload.mjs | 582 ++++++++++++++++++ .../optimize_native_runtime_payload.py | 440 ------------- .../package-liboliphaunt-linux-assets.sh | 4 +- .../package-liboliphaunt-macos-assets.sh | 4 +- .../package-liboliphaunt-windows-assets.ps1 | 4 +- .../package_liboliphaunt_cargo_artifacts.py | 19 +- tools/release/release.py | 65 +- 15 files changed, 754 insertions(+), 483 deletions(-) create mode 100644 tools/release/native-runtime-payload-policy.json create mode 100644 tools/release/optimize_native_runtime_payload.mjs delete mode 100644 tools/release/optimize_native_runtime_payload.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index c514b7a3..9581e966 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -350,7 +350,7 @@ until the current-state gates here are checked with fresh local evidence. counting, empty-directory behavior, and missing-path failure. Fresh checks passed: `bash tools/policy/check-tooling-stack.sh`, `bash src/runtimes/node-direct/tools/check-package.sh check-static`, - `python3 tools/release/optimize_native_runtime_payload.py --help`, + `tools/dev/bun.sh tools/release/optimize_native_runtime_payload.mjs --help`, `python3 tools/release/check_artifact_targets.py`, `python3 tools/policy/check-release-policy.py`, `python3 tools/release/check_release_metadata.py`, @@ -1023,3 +1023,11 @@ until the current-state gates here are checked with fresh local evidence. expected-asset and exact-extension manifest validation from the attestation verifier, while `check_release_versions.py` now shells to the Bun checker for released dependency asset verification. +- On 2026-06-26, native runtime payload optimization moved from Python to Bun. + `optimize_native_runtime_payload.mjs` now owns pruning, stripping, and + validation for root runtime payloads and split `oliphaunt-tools` payloads, + while Python release orchestrators call the Bun CLI and read the shared + `native-runtime-payload-policy.json` tool split policy. Direct synthetic + smokes proved runtime mode keeps only `initdb`, `pg_ctl`, and `postgres`, + tools mode keeps only `pg_dump` and `psql`, and the modified Python callers + still compile. diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index bef703b9..c4bd1d33 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -281,7 +281,7 @@ for native_strip_caller in \ tools/release/package-liboliphaunt-mobile-assets.sh \ src/runtimes/node-direct/tools/build-node-addon.sh \ src/extensions/artifacts/native/tools/extension-artifact-packager.mjs \ - tools/release/optimize_native_runtime_payload.py + tools/release/optimize_native_runtime_payload.mjs do grep -Fq 'strip_native_release_binaries.mjs' "$native_strip_caller" || fail "$native_strip_caller must use the Bun native binary stripper" diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index af10ea85..130f74a7 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -15,7 +15,6 @@ tools/release/check_release_versions.py tools/release/check_staged_artifacts.py tools/release/extension_artifact_targets.py tools/release/local_registry_publish.py -tools/release/optimize_native_runtime_payload.py tools/release/package_liboliphaunt_cargo_artifacts.py tools/release/package_liboliphaunt_wasix_cargo_artifacts.py tools/release/product_metadata.py diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py index 63a3243d..74f60284 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -944,7 +944,7 @@ def validate_ci_release_artifacts() -> None: ) require_text( "tools/release/package_liboliphaunt_cargo_artifacts.py", - "optimize_native_runtime_payload.optimize_payload", + "optimize_native_payload(", "liboliphaunt Cargo artifact packages must prune and validate native runtime payloads before splitting", ) reject_text( diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 71ca0cad..3896f6bb 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -20,7 +20,6 @@ import artifact_targets import product_metadata import extension_artifact_targets -import optimize_native_runtime_payload import package_liboliphaunt_wasix_cargo_artifacts @@ -29,6 +28,27 @@ SCHEMA = "oliphaunt-consumer-shape-v1" SEVERITY_ORDER = {"P0": 0, "P1": 1, "P2": 2} FORBIDDEN_INSTALL_SCRIPTS = {"preinstall", "install", "postinstall", "prepare"} +NATIVE_PAYLOAD_POLICY = json.loads( + (ROOT / "tools/release/native-runtime-payload-policy.json").read_text(encoding="utf-8") +) +NATIVE_RUNTIME_TOOL_STEMS = tuple(NATIVE_PAYLOAD_POLICY["nativeRuntimeToolStems"]) +NATIVE_TOOLS_TOOL_STEMS = tuple(NATIVE_PAYLOAD_POLICY["nativeToolsToolStems"]) + + +def is_windows_native_target(target: str | None) -> bool: + return target is not None and target.startswith("windows-") + + +def required_native_runtime_tools(target: str | None) -> tuple[str, ...]: + if is_windows_native_target(target): + return tuple(f"{stem}.exe" for stem in NATIVE_RUNTIME_TOOL_STEMS) + return NATIVE_RUNTIME_TOOL_STEMS + + +def required_native_tools_package_tools(target: str | None) -> tuple[str, ...]: + if is_windows_native_target(target): + return tuple(f"{stem}.exe" for stem in NATIVE_TOOLS_TOOL_STEMS) + return NATIVE_TOOLS_TOOL_STEMS @dataclass(frozen=True) @@ -300,7 +320,7 @@ def liboliphaunt_native_expected_registry_packages() -> set[str]: def native_npm_tool_split_failures( root: str, *, - tool_set: optimize_native_runtime_payload.NativeToolSet, + tool_set: str, ) -> list[str]: failures: list[str] = [] for package_json_path in sorted((ROOT / root).glob("*/package.json")): @@ -321,9 +341,9 @@ def native_npm_tool_split_failures( failures.append(f"{path}: publishConfig.executableFiles={executable_files!r}") continue if tool_set == "runtime": - expected_tools = optimize_native_runtime_payload.required_runtime_tools(target) + expected_tools = required_native_runtime_tools(target) elif tool_set == "tools": - expected_tools = optimize_native_runtime_payload.required_tools_package_tools(target) + expected_tools = required_native_tools_package_tools(target) else: fail(f"unsupported native npm tool split check: {tool_set}") expected = {f"./runtime/bin/{tool}" for tool in expected_tools} @@ -449,7 +469,7 @@ def check_liboliphaunt(findings: list[Finding]) -> None: severity="P0", ) native_packager = read_text("tools/release/package_liboliphaunt_cargo_artifacts.py") - native_optimizer = read_text("tools/release/optimize_native_runtime_payload.py") + native_optimizer = read_text("tools/release/optimize_native_runtime_payload.mjs") release_cli = read_text("tools/release/release.py") local_registry_publisher = read_text("tools/release/local_registry_publish.py") native_runtime_package_split_failures = native_npm_tool_split_failures( @@ -464,8 +484,8 @@ def check_liboliphaunt(findings: list[Finding]) -> None: findings, product, "liboliphaunt-native-tool-split", - set(optimize_native_runtime_payload.NATIVE_RUNTIME_TOOL_STEMS) == {"initdb", "pg_ctl", "postgres"} - and set(optimize_native_runtime_payload.NATIVE_TOOLS_TOOL_STEMS) == {"pg_dump", "psql"} + set(NATIVE_RUNTIME_TOOL_STEMS) == {"initdb", "pg_ctl", "postgres"} + and set(NATIVE_TOOLS_TOOL_STEMS) == {"pg_dump", "psql"} and "missing oliphaunt-tools native release asset" in native_packager and "extract_archive(tools_archive, tools_root)" in native_packager and "validate_tools_target_pair" in native_packager @@ -484,7 +504,7 @@ def check_liboliphaunt(findings: list[Finding]) -> None: and not native_tools_package_split_failures, "Native root packages and crates must keep postgres/initdb/pg_ctl only, with pg_dump/psql published through oliphaunt-tools packages/crates.", [ - "tools/release/optimize_native_runtime_payload.py", + "tools/release/optimize_native_runtime_payload.mjs", "tools/release/package_liboliphaunt_cargo_artifacts.py", "tools/release/release.py", *native_runtime_package_split_failures, @@ -569,7 +589,7 @@ def check_liboliphaunt(findings: list[Finding]) -> None: packaging_scripts = { "tools/release/package-liboliphaunt-macos-assets.sh": [ "oliphaunt_assert_base_runtime_has_no_optional_extensions", - "optimize_native_runtime_payload.py", + "optimize_native_runtime_payload.mjs", "plpgsql.dylib", "$stage/lib/modules/", "liboliphaunt-${version}-${target_id}.tar.gz", @@ -577,7 +597,7 @@ def check_liboliphaunt(findings: list[Finding]) -> None: ], "tools/release/package-liboliphaunt-linux-assets.sh": [ "oliphaunt_assert_base_runtime_has_no_optional_extensions", - "optimize_native_runtime_payload.py", + "optimize_native_runtime_payload.mjs", "plpgsql.so", "$stage/lib/modules/", "liboliphaunt-${version}-${target_id}.tar.gz", @@ -585,7 +605,7 @@ def check_liboliphaunt(findings: list[Finding]) -> None: ], "tools/release/package-liboliphaunt-windows-assets.ps1": [ "Assert-BaseRuntimeHasNoOptionalExtensions", - "optimize_native_runtime_payload.py", + "optimize_native_runtime_payload.mjs", "plpgsql.dll", "lib/modules", 'Copy-Item -Recurse -Force (Join-Path $Runtime "*") (Join-Path $Stage "runtime")', diff --git a/tools/release/check_liboliphaunt_release_assets.py b/tools/release/check_liboliphaunt_release_assets.py index 835db777..08afb46c 100755 --- a/tools/release/check_liboliphaunt_release_assets.py +++ b/tools/release/check_liboliphaunt_release_assets.py @@ -8,6 +8,7 @@ import hashlib import json import shutil +import subprocess import sys import tarfile import tempfile @@ -16,7 +17,6 @@ from typing import NoReturn import artifact_targets -import optimize_native_runtime_payload import product_metadata @@ -196,17 +196,26 @@ def validate_native_target_artifact( target: str, *, require_runtime: bool, - tool_set: optimize_native_runtime_payload.NativeToolSet, + tool_set: str, ) -> None: with tempfile.TemporaryDirectory(prefix=f"oliphaunt-native-{target}-") as temp: extracted = Path(temp) / "payload" extract_archive(path, extracted) - optimize_native_runtime_payload.validate_payload( - extracted, + command = [ + "tools/dev/bun.sh", + "tools/release/optimize_native_runtime_payload.mjs", + str(extracted), + "--target", target, - require_runtime=require_runtime, - tool_set=tool_set, - ) + "--tool-set", + tool_set, + "--check", + ] + if not require_runtime: + command.append("--allow-missing-runtime") + result = subprocess.run(command, cwd=ROOT, check=False) + if result.returncode != 0: + raise SystemExit(result.returncode) def validate_native_target_artifacts(asset_dir: Path, version: str) -> None: diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index d157c479..ce944be3 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -12,13 +12,33 @@ import artifact_targets import extension_artifact_targets -import optimize_native_runtime_payload import package_liboliphaunt_wasix_cargo_artifacts import product_metadata import release ROOT = Path(__file__).resolve().parents[2] +NATIVE_PAYLOAD_POLICY = json.loads( + (ROOT / "tools/release/native-runtime-payload-policy.json").read_text(encoding="utf-8") +) +NATIVE_RUNTIME_TOOL_STEMS = tuple(NATIVE_PAYLOAD_POLICY["nativeRuntimeToolStems"]) +NATIVE_TOOLS_TOOL_STEMS = tuple(NATIVE_PAYLOAD_POLICY["nativeToolsToolStems"]) + + +def is_windows_native_target(target: str | None) -> bool: + return target is not None and target.startswith("windows-") + + +def required_native_runtime_tools(target: str | None) -> tuple[str, ...]: + if is_windows_native_target(target): + return tuple(f"{stem}.exe" for stem in NATIVE_RUNTIME_TOOL_STEMS) + return NATIVE_RUNTIME_TOOL_STEMS + + +def required_native_tools_package_tools(target: str | None) -> tuple[str, ...]: + if is_windows_native_target(target): + return tuple(f"{stem}.exe" for stem in NATIVE_TOOLS_TOOL_STEMS) + return NATIVE_TOOLS_TOOL_STEMS def fail(message: str) -> NoReturn: @@ -150,7 +170,7 @@ def validate_platform_npm_packages( files = ["bin", "runtime", "README.md"] if target.target == "windows-x64-msvc" else ["lib", "runtime", "README.md"] executable_files = [ f"./runtime/bin/{tool}" - for tool in sorted(optimize_native_runtime_payload.required_runtime_tools(target.target)) + for tool in sorted(required_native_runtime_tools(target.target)) ] elif product == "liboliphaunt-native" and kind == "native-tools": if metadata.get("product") != "oliphaunt-tools": @@ -162,7 +182,7 @@ def validate_platform_npm_packages( files = ["runtime", "README.md"] executable_files = [ f"./runtime/bin/{tool}" - for tool in sorted(optimize_native_runtime_payload.required_tools_package_tools(target.target)) + for tool in sorted(required_native_tools_package_tools(target.target)) ] elif product == "oliphaunt-broker": if target.executable_relative_path is None: @@ -1479,9 +1499,11 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None ): fail("oliphaunt-wasix-dump must require the tools feature at Cargo install/build time") native_packager_source = read_text("tools/release/package_liboliphaunt_cargo_artifacts.py") + native_optimizer_source = read_text("tools/release/optimize_native_runtime_payload.mjs") if ( - optimize_native_runtime_payload.NATIVE_RUNTIME_TOOL_STEMS != ("initdb", "pg_ctl", "postgres") - or optimize_native_runtime_payload.NATIVE_TOOLS_TOOL_STEMS != ("pg_dump", "psql") + NATIVE_RUNTIME_TOOL_STEMS != ("initdb", "pg_ctl", "postgres") + or NATIVE_TOOLS_TOOL_STEMS != ("pg_dump", "psql") + or "native-runtime-payload-policy.json" not in native_optimizer_source or "missing oliphaunt-tools native release asset" not in native_packager_source or "extract_archive(tools_archive, tools_root)" not in native_packager_source or "validate_tools_target_pair" not in native_packager_source diff --git a/tools/release/native-runtime-payload-policy.json b/tools/release/native-runtime-payload-policy.json new file mode 100644 index 00000000..3aa653b7 --- /dev/null +++ b/tools/release/native-runtime-payload-policy.json @@ -0,0 +1,7 @@ +{ + "nativeRuntimeToolStems": ["initdb", "pg_ctl", "postgres"], + "nativeToolsToolStems": ["pg_dump", "psql"], + "devRuntimeDirs": ["include", "lib/pkgconfig", "lib/postgresql/pgxs"], + "devRuntimeSuffixes": [".a", ".la", ".pdb"], + "windowsDevRuntimeSuffixes": [".lib"] +} diff --git a/tools/release/optimize_native_runtime_payload.mjs b/tools/release/optimize_native_runtime_payload.mjs new file mode 100644 index 00000000..33d3185a --- /dev/null +++ b/tools/release/optimize_native_runtime_payload.mjs @@ -0,0 +1,582 @@ +#!/usr/bin/env bun +import { + accessSync, + closeSync, + constants, + existsSync, + lstatSync, + openSync, + readFileSync, + readdirSync, + readSync, + rmSync, + rmdirSync, +} from "node:fs"; +import { dirname, join, relative, resolve, sep } from "node:path"; +import { spawnSync } from "node:child_process"; +import { platform } from "node:os"; +import { fileURLToPath } from "node:url"; + +const TOOL = "optimize_native_runtime_payload.mjs"; +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const POLICY_PATH = join(ROOT, "tools/release/native-runtime-payload-policy.json"); +const POLICY = JSON.parse(readFileSync(POLICY_PATH, "utf8")); + +export const NATIVE_RUNTIME_TOOL_STEMS = Object.freeze([...POLICY.nativeRuntimeToolStems]); +export const NATIVE_TOOLS_TOOL_STEMS = Object.freeze([...POLICY.nativeToolsToolStems]); +export const NATIVE_PACKAGED_TOOL_STEMS = Object.freeze([ + ...NATIVE_RUNTIME_TOOL_STEMS, + ...NATIVE_TOOLS_TOOL_STEMS, +]); + +const DEV_RUNTIME_DIRS = Object.freeze([...POLICY.devRuntimeDirs]); +const DEV_RUNTIME_SUFFIXES = Object.freeze([...POLICY.devRuntimeSuffixes]); +const WINDOWS_DEV_RUNTIME_SUFFIXES = Object.freeze([...POLICY.windowsDevRuntimeSuffixes]); +const MACHO_MAGICS = new Set([ + "feedface", + "cefaedfe", + "feedfacf", + "cffaedfe", + "cafebabe", + "bebafeca", +]); +const ELF_DEBUG_SECTION = /\]\s+\.(debug_[^\s]+|symtab|strtab)\s/g; + +function fail(message) { + console.error(`${TOOL}: ${message}`); + process.exit(1); +} + +function rel(path) { + const resolved = resolve(String(path)); + const relativePath = relative(ROOT, resolved); + if (!relativePath || relativePath.startsWith("..") || relativePath === resolved) { + return resolved.split(sep).join("/"); + } + return relativePath.split(sep).join("/"); +} + +function exists(path) { + return existsSync(path); +} + +function isDirectory(path) { + try { + return lstatSync(path).isDirectory(); + } catch { + return false; + } +} + +function isFile(path) { + try { + return lstatSync(path).isFile(); + } catch { + return false; + } +} + +function readPrefix(path, size = 8) { + const buffer = Buffer.alloc(size); + let fd; + try { + fd = openSync(path, "r"); + const bytesRead = readSync(fd, buffer, 0, size, 0); + return buffer.subarray(0, bytesRead); + } catch (error) { + fail(`failed to read ${path}: ${error.message}`); + } finally { + if (fd !== undefined) { + closeSync(fd); + } + } +} + +function classifyNativeFile(path) { + const prefix = readPrefix(path); + if (prefix.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46]))) { + return { path, kind: "elf", archive: false }; + } + if (MACHO_MAGICS.has(prefix.subarray(0, 4).toString("hex"))) { + return { path, kind: "macho", archive: false }; + } + if (prefix.subarray(0, 2).toString("ascii") === "MZ") { + return { path, kind: "pe", archive: false }; + } + if (prefix.subarray(0, 8).toString("ascii") === "!\n") { + return { path, kind: "archive", archive: true }; + } + return null; +} + +export function isWindowsTarget(target, runtimeDir = null) { + if (target && target.startsWith("windows-")) { + return true; + } + if (!runtimeDir) { + return false; + } + const binDir = join(runtimeDir, "bin"); + return NATIVE_PACKAGED_TOOL_STEMS.some((stem) => isFile(join(binDir, `${stem}.exe`))); +} + +export function requiredRuntimeTools(target, runtimeDir = null) { + if (isWindowsTarget(target, runtimeDir)) { + return NATIVE_RUNTIME_TOOL_STEMS.map((stem) => `${stem}.exe`); + } + return [...NATIVE_RUNTIME_TOOL_STEMS]; +} + +export function requiredToolsPackageTools(target, runtimeDir = null) { + if (isWindowsTarget(target, runtimeDir)) { + return NATIVE_TOOLS_TOOL_STEMS.map((stem) => `${stem}.exe`); + } + return [...NATIVE_TOOLS_TOOL_STEMS]; +} + +export function packagedRuntimeTools(target, runtimeDir = null) { + if (isWindowsTarget(target, runtimeDir)) { + return NATIVE_PACKAGED_TOOL_STEMS.map((stem) => `${stem}.exe`); + } + return [...NATIVE_PACKAGED_TOOL_STEMS]; +} + +export function runtimeToolsForSet(target, runtimeDir = null, toolSet = "packaged") { + if (toolSet === "runtime") { + return requiredRuntimeTools(target, runtimeDir); + } + if (toolSet === "tools") { + return requiredToolsPackageTools(target, runtimeDir); + } + return packagedRuntimeTools(target, runtimeDir); +} + +export function requiredRuntimeMemberPaths(target, prefix) { + return requiredRuntimeTools(target).map((tool) => `${prefix.replace(/\/+$/, "")}/${tool}`); +} + +export function requiredToolsMemberPaths(target, prefix) { + return requiredToolsPackageTools(target).map((tool) => `${prefix.replace(/\/+$/, "")}/${tool}`); +} + +function runtimeDirFor(root) { + for (const candidate of [ + join(root, "runtime"), + join(root, "oliphaunt", "runtime", "files"), + ]) { + if (isDirectory(candidate)) { + return candidate; + } + } + if (isDirectory(join(root, "bin")) && (isDirectory(join(root, "share")) || isDirectory(join(root, "lib")))) { + return root; + } + return null; +} + +function removePath(path) { + rmSync(path, { recursive: true, force: true }); +} + +function walk(root, { includeDirs = false } = {}) { + if (!isDirectory(root)) { + return []; + } + const results = []; + const visit = (current) => { + for (const name of readdirSync(current).sort()) { + const path = join(current, name); + let stat; + try { + stat = lstatSync(path); + } catch { + continue; + } + if (stat.isDirectory()) { + if (includeDirs) { + results.push(path); + } + visit(path); + } else if (stat.isFile()) { + results.push(path); + } + } + }; + visit(root); + return results.sort(); +} + +function pruneEmptyDirs(root) { + for (const path of walk(root, { includeDirs: true }).filter(isDirectory).sort().reverse()) { + try { + rmdirSync(path); + } catch { + // Directory is not empty or disappeared while pruning. + } + } +} + +function posixRelative(from, to) { + return relative(from, to).split(sep).join("/"); +} + +function isDevRuntimeFile(relativePath, { windows }) { + const name = relativePath.split("/").pop().toLowerCase(); + if (DEV_RUNTIME_SUFFIXES.some((suffix) => name.endsWith(suffix))) { + return true; + } + return windows && WINDOWS_DEV_RUNTIME_SUFFIXES.some((suffix) => name.endsWith(suffix)); +} + +export function pruneRuntimePayload(root, target = null, { toolSet = "packaged" } = {}) { + const runtimeDir = runtimeDirFor(root); + if (!runtimeDir) { + return; + } + + const windows = isWindowsTarget(target, runtimeDir); + const requiredTools = new Set(runtimeToolsForSet(target, runtimeDir, toolSet)); + const binDir = join(runtimeDir, "bin"); + if (isDirectory(binDir)) { + for (const name of readdirSync(binDir).sort()) { + const path = join(binDir, name); + if (windows) { + if (name.toLowerCase().endsWith(".exe") && !requiredTools.has(name)) { + removePath(path); + } + } else if (!requiredTools.has(name)) { + removePath(path); + } + } + } + + if (toolSet === "tools" && isDirectory(runtimeDir)) { + for (const name of readdirSync(runtimeDir).sort()) { + if (name !== "bin") { + removePath(join(runtimeDir, name)); + } + } + } + + for (const relativePath of DEV_RUNTIME_DIRS) { + removePath(join(runtimeDir, ...relativePath.split("/"))); + } + + for (const path of walk(runtimeDir, { includeDirs: true }).sort().reverse()) { + if (isDirectory(path) && path.endsWith(".dSYM")) { + removePath(path); + continue; + } + if (!isFile(path)) { + continue; + } + const relativePath = posixRelative(runtimeDir, path); + if (isDevRuntimeFile(relativePath, { windows })) { + removePath(path); + } + } + + pruneEmptyDirs(runtimeDir); +} + +function which(command) { + const pathEnv = process.env.PATH ?? ""; + const extensions = platform() === "win32" ? ["", ".exe", ".cmd", ".bat"] : [""]; + for (const dir of pathEnv.split(platform() === "win32" ? ";" : ":")) { + if (!dir) { + continue; + } + for (const extension of extensions) { + const candidate = join(dir, `${command}${extension}`); + if (isFile(candidate)) { + return candidate; + } + } + } + return null; +} + +function stripSupportedForTarget(target) { + if (!target) { + return true; + } + if (target.startsWith("linux-") || target.startsWith("android-")) { + return platform() === "linux"; + } + if (target.startsWith("macos-") || target.startsWith("ios-")) { + return platform() === "darwin"; + } + if (target.startsWith("windows-")) { + return Boolean( + process.env.OLIPHAUNT_PE_STRIP || + process.env.OLIPHAUNT_STRIP || + which("llvm-strip") || + platform() === "win32", + ); + } + return true; +} + +function stripPayload(root) { + const result = spawnSync(process.execPath, ["tools/release/strip_native_release_binaries.mjs", root], { + cwd: ROOT, + stdio: "inherit", + env: process.env, + }); + if (result.status !== 0) { + fail(`failed to strip native payload under ${rel(root)}`); + } +} + +function fileOutput(path) { + const fileTool = which("file"); + if (!fileTool) { + return null; + } + const result = spawnSync(fileTool, [path], { + cwd: ROOT, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.status !== 0) { + return null; + } + return result.stdout; +} + +function elfDebugErrors(path) { + const readelf = which("readelf"); + if (readelf) { + const result = spawnSync(readelf, ["-S", path], { + cwd: ROOT, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.status !== 0) { + return [`${rel(path)} could not be inspected with readelf: ${result.stderr.trim()}`]; + } + const sections = new Set(); + for (const match of result.stdout.matchAll(ELF_DEBUG_SECTION)) { + sections.add(match[1]); + } + return [...sections].sort().map((section) => `${rel(path)} contains unstripped ELF section .${section}`); + } + + const output = fileOutput(path); + if (output && (output.includes("not stripped") || output.includes("with debug_info"))) { + return [`${rel(path)} appears to contain unstripped ELF debug/symbol data`]; + } + return []; +} + +function validateNativeFiles(root) { + const errors = []; + for (const path of walk(root)) { + const native = classifyNativeFile(path); + if (!native) { + continue; + } + if (native.kind === "elf" && !native.archive) { + errors.push(...elfDebugErrors(path)); + } + } + return errors; +} + +function validateRuntimeTree(root, target, requireRuntime, { toolSet = "packaged" } = {}) { + const errors = []; + const runtimeDir = runtimeDirFor(root); + if (!runtimeDir) { + if (requireRuntime) { + errors.push(`${rel(root)} is missing a runtime tree`); + } + return errors; + } + + const windows = isWindowsTarget(target, runtimeDir); + const requiredTools = new Set(runtimeToolsForSet(target, runtimeDir, toolSet)); + const binDir = join(runtimeDir, "bin"); + if (requireRuntime && !isDirectory(binDir)) { + errors.push(`${rel(runtimeDir)} is missing bin`); + } + if (isDirectory(binDir)) { + for (const tool of [...requiredTools].sort()) { + const path = join(binDir, tool); + if (!isFile(path)) { + errors.push(`${rel(runtimeDir)} is missing required runtime tool bin/${tool}`); + continue; + } + if (!windows) { + try { + accessSync(path, constants.X_OK); + } catch { + errors.push(`${rel(path)} must be executable`); + } + } + } + for (const name of readdirSync(binDir).sort()) { + const path = join(binDir, name); + if (windows) { + if (name.toLowerCase().endsWith(".exe") && !requiredTools.has(name)) { + errors.push(`${rel(path)} is an extra Windows runtime executable`); + } + } else if (!requiredTools.has(name)) { + errors.push(`${rel(path)} is an extra runtime tool`); + } + } + } + + if (toolSet === "tools" && isDirectory(runtimeDir)) { + const allowed = new Set([...requiredTools].map((tool) => `bin/${tool}`)); + for (const path of walk(runtimeDir)) { + const relativePath = posixRelative(runtimeDir, path); + if (!allowed.has(relativePath)) { + errors.push(`${rel(path)} is not part of the native tools payload`); + } + } + } + + for (const relativePath of DEV_RUNTIME_DIRS) { + const path = join(runtimeDir, ...relativePath.split("/")); + if (exists(path)) { + errors.push(`${rel(path)} is a development-only runtime path`); + } + } + + for (const path of walk(runtimeDir, { includeDirs: true })) { + if (isDirectory(path) && path.endsWith(".dSYM")) { + errors.push(`${rel(path)} is a development-only debug symbol bundle`); + continue; + } + if (!isFile(path)) { + continue; + } + const relativePath = posixRelative(runtimeDir, path); + if (isDevRuntimeFile(relativePath, { windows })) { + errors.push(`${rel(path)} is a development-only runtime file`); + } + } + + return errors; +} + +export function validatePayload(root, target = null, { requireRuntime = true, toolSet = "packaged" } = {}) { + const errors = [ + ...validateRuntimeTree(root, target, requireRuntime, { toolSet }), + ...validateNativeFiles(root), + ]; + if (errors.length > 0) { + for (const error of errors) { + console.error(error); + } + fail(`${rel(root)} is not an optimized native runtime payload`); + } +} + +export function optimizePayload( + root, + target = null, + { strip = "auto", requireRuntime = true, toolSet = "packaged" } = {}, +) { + pruneRuntimePayload(root, target, { toolSet }); + const shouldStrip = strip === true || (strip === "auto" && stripSupportedForTarget(target)); + if (shouldStrip) { + stripPayload(root); + } + validatePayload(root, target, { requireRuntime, toolSet }); +} + +function usage() { + return `Usage: tools/release/optimize_native_runtime_payload.mjs [options] + +Prune, strip, and validate liboliphaunt native runtime payloads. + +Options: + --target Release target id. + --check Validate without mutating the payload. + --no-strip Prune but skip native binary stripping before validation. + --allow-missing-runtime Validate native files when the archive is library-only. + --tool-set packaged, runtime, or tools. Default: packaged. + --help Show this help. +`; +} + +function parseArgs(argv) { + const args = { + root: null, + target: null, + check: false, + noStrip: false, + allowMissingRuntime: false, + toolSet: "packaged", + }; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === "--help" || arg === "-h") { + console.log(usage()); + process.exit(0); + } + if (arg === "--target") { + args.target = argv[++index]; + if (!args.target) { + fail("--target requires a value"); + } + continue; + } + if (arg === "--check") { + args.check = true; + continue; + } + if (arg === "--no-strip") { + args.noStrip = true; + continue; + } + if (arg === "--allow-missing-runtime") { + args.allowMissingRuntime = true; + continue; + } + if (arg === "--tool-set") { + args.toolSet = argv[++index]; + if (!["packaged", "runtime", "tools"].includes(args.toolSet)) { + fail("--tool-set must be one of: packaged, runtime, tools"); + } + continue; + } + if (arg.startsWith("-")) { + fail(`unknown option: ${arg}`); + } + if (args.root) { + fail(`unexpected positional argument: ${arg}`); + } + args.root = arg; + } + if (!args.root) { + console.error(usage()); + process.exit(2); + } + return args; +} + +export function main(argv = process.argv.slice(2)) { + const args = parseArgs(argv); + const root = resolve(args.root); + if (!exists(root)) { + fail(`payload root does not exist: ${root}`); + } + if (args.check) { + validatePayload(root, args.target, { + requireRuntime: !args.allowMissingRuntime, + toolSet: args.toolSet, + }); + return; + } + optimizePayload(root, args.target, { + strip: args.noStrip ? false : "auto", + requireRuntime: !args.allowMissingRuntime, + toolSet: args.toolSet, + }); +} + +if (import.meta.main) { + main(); +} diff --git a/tools/release/optimize_native_runtime_payload.py b/tools/release/optimize_native_runtime_payload.py deleted file mode 100644 index e3a16a40..00000000 --- a/tools/release/optimize_native_runtime_payload.py +++ /dev/null @@ -1,440 +0,0 @@ -#!/usr/bin/env python3 -"""Prune, strip, and validate liboliphaunt native runtime payloads.""" - -from __future__ import annotations - -import argparse -import os -import re -import shutil -import subprocess -import sys -from dataclasses import dataclass -from pathlib import Path, PurePosixPath -from typing import Literal, NoReturn - - -ROOT = Path(__file__).resolve().parents[2] -NATIVE_RUNTIME_TOOL_STEMS = ("initdb", "pg_ctl", "postgres") -NATIVE_TOOLS_TOOL_STEMS = ("pg_dump", "psql") -NATIVE_PACKAGED_TOOL_STEMS = (*NATIVE_RUNTIME_TOOL_STEMS, *NATIVE_TOOLS_TOOL_STEMS) -NativeToolSet = Literal["packaged", "runtime", "tools"] -ELF_DEBUG_SECTION = re.compile(r"\]\s+\.(debug_[^\s]+|symtab|strtab)\s") -MACHO_MAGICS = { - b"\xfe\xed\xfa\xce", - b"\xce\xfa\xed\xfe", - b"\xfe\xed\xfa\xcf", - b"\xcf\xfa\xed\xfe", - b"\xca\xfe\xba\xbe", - b"\xbe\xba\xfe\xca", -} -DEV_RUNTIME_DIRS = ( - PurePosixPath("include"), - PurePosixPath("lib/pkgconfig"), - PurePosixPath("lib/postgresql/pgxs"), -) -DEV_RUNTIME_SUFFIXES = (".a", ".la", ".pdb") -WINDOWS_DEV_RUNTIME_SUFFIXES = (".lib",) - - -@dataclass(frozen=True) -class NativeFile: - path: Path - kind: str - archive: bool = False - - -def fail(message: str) -> NoReturn: - print(f"optimize_native_runtime_payload.py: {message}", file=sys.stderr) - raise SystemExit(1) - - -def rel(path: Path) -> str: - try: - return path.relative_to(ROOT).as_posix() - except ValueError: - return str(path) - - -def read_prefix(path: Path, size: int = 8) -> bytes: - try: - with path.open("rb") as file: - return file.read(size) - except OSError as error: - fail(f"failed to read {path}: {error}") - - -def classify_native_file(path: Path) -> NativeFile | None: - prefix = read_prefix(path) - if prefix.startswith(b"\x7fELF"): - return NativeFile(path, "elf") - if prefix[:4] in MACHO_MAGICS: - return NativeFile(path, "macho") - if prefix.startswith(b"MZ"): - return NativeFile(path, "pe") - if prefix.startswith(b"!\n"): - return NativeFile(path, "archive", archive=True) - return None - - -def is_windows_target(target: str | None, runtime_dir: Path | None = None) -> bool: - if target is not None and target.startswith("windows-"): - return True - if runtime_dir is None: - return False - bin_dir = runtime_dir / "bin" - return any((bin_dir / f"{stem}.exe").exists() for stem in NATIVE_PACKAGED_TOOL_STEMS) - - -def required_runtime_tools(target: str | None, runtime_dir: Path | None = None) -> tuple[str, ...]: - if is_windows_target(target, runtime_dir): - return tuple(f"{stem}.exe" for stem in NATIVE_RUNTIME_TOOL_STEMS) - return NATIVE_RUNTIME_TOOL_STEMS - - -def required_tools_package_tools( - target: str | None, runtime_dir: Path | None = None -) -> tuple[str, ...]: - if is_windows_target(target, runtime_dir): - return tuple(f"{stem}.exe" for stem in NATIVE_TOOLS_TOOL_STEMS) - return NATIVE_TOOLS_TOOL_STEMS - - -def packaged_runtime_tools(target: str | None, runtime_dir: Path | None = None) -> tuple[str, ...]: - if is_windows_target(target, runtime_dir): - return tuple(f"{stem}.exe" for stem in NATIVE_PACKAGED_TOOL_STEMS) - return NATIVE_PACKAGED_TOOL_STEMS - - -def runtime_tools_for_set( - target: str | None, - runtime_dir: Path | None = None, - *, - tool_set: NativeToolSet = "packaged", -) -> tuple[str, ...]: - if tool_set == "runtime": - return required_runtime_tools(target, runtime_dir) - if tool_set == "tools": - return required_tools_package_tools(target, runtime_dir) - return packaged_runtime_tools(target, runtime_dir) - - -def required_runtime_member_paths(target: str | None, *, prefix: str) -> list[str]: - return [f"{prefix.rstrip('/')}/{tool}" for tool in required_runtime_tools(target)] - - -def required_tools_member_paths(target: str | None, *, prefix: str) -> list[str]: - return [f"{prefix.rstrip('/')}/{tool}" for tool in required_tools_package_tools(target)] - - -def runtime_dir_for(root: Path) -> Path | None: - for candidate in [ - root / "runtime", - root / "oliphaunt" / "runtime" / "files", - ]: - if candidate.is_dir(): - return candidate - if (root / "bin").is_dir() and ((root / "share").is_dir() or (root / "lib").is_dir()): - return root - return None - - -def remove_path(path: Path) -> None: - if path.is_dir(): - shutil.rmtree(path) - elif path.exists(): - path.unlink() - - -def prune_empty_dirs(root: Path) -> None: - if not root.is_dir(): - return - for path in sorted((item for item in root.rglob("*") if item.is_dir()), reverse=True): - try: - path.rmdir() - except OSError: - pass - - -def is_dev_runtime_file(relative: PurePosixPath, *, windows: bool) -> bool: - name = relative.name.lower() - if name.endswith(DEV_RUNTIME_SUFFIXES): - return True - if windows and name.endswith(WINDOWS_DEV_RUNTIME_SUFFIXES): - return True - return False - - -def prune_runtime_payload( - root: Path, - target: str | None = None, - *, - tool_set: NativeToolSet = "packaged", -) -> None: - runtime_dir = runtime_dir_for(root) - if runtime_dir is None: - return - - windows = is_windows_target(target, runtime_dir) - required_tools = set(runtime_tools_for_set(target, runtime_dir, tool_set=tool_set)) - bin_dir = runtime_dir / "bin" - if bin_dir.is_dir(): - for path in sorted(bin_dir.iterdir()): - name = path.name - if windows: - if name.lower().endswith(".exe") and name not in required_tools: - remove_path(path) - elif name not in required_tools: - remove_path(path) - - if tool_set == "tools" and runtime_dir.is_dir(): - for path in sorted(runtime_dir.iterdir()): - if path.name != "bin": - remove_path(path) - - for relative in DEV_RUNTIME_DIRS: - remove_path(runtime_dir.joinpath(*relative.parts)) - - for path in sorted(runtime_dir.rglob("*"), reverse=True): - if path.is_dir() and path.name.endswith(".dSYM"): - remove_path(path) - continue - if not path.is_file(): - continue - relative = PurePosixPath(path.relative_to(runtime_dir).as_posix()) - if is_dev_runtime_file(relative, windows=windows): - remove_path(path) - - prune_empty_dirs(runtime_dir) - - -def strip_supported_for_target(target: str | None) -> bool: - if target is None: - return True - if target.startswith(("linux-", "android-")): - return sys.platform.startswith("linux") - if target.startswith(("macos-", "ios-")): - return sys.platform == "darwin" - if target.startswith("windows-"): - return bool( - os.environ.get("OLIPHAUNT_PE_STRIP") - or os.environ.get("OLIPHAUNT_STRIP") - or shutil.which("llvm-strip") - or sys.platform == "win32" - ) - return True - - -def strip_payload(root: Path) -> None: - result = subprocess.run( - [ - str(ROOT / "tools/dev/bun.sh"), - "tools/release/strip_native_release_binaries.mjs", - str(root), - ], - cwd=ROOT, - check=False, - ) - if result.returncode != 0: - fail(f"failed to strip native payload under {rel(root)}") - - -def iter_files(root: Path) -> list[Path]: - return sorted(path for path in root.rglob("*") if path.is_file()) - - -def file_output(path: Path) -> str | None: - file_tool = shutil.which("file") - if file_tool is None: - return None - result = subprocess.run( - [file_tool, str(path)], - check=False, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - if result.returncode != 0: - return None - return result.stdout - - -def elf_debug_errors(path: Path) -> list[str]: - readelf = shutil.which("readelf") - if readelf is not None: - result = subprocess.run( - [readelf, "-S", str(path)], - check=False, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - if result.returncode != 0: - return [f"{rel(path)} could not be inspected with readelf: {result.stderr.strip()}"] - sections = sorted({match.group(1) for match in ELF_DEBUG_SECTION.finditer(result.stdout)}) - return [f"{rel(path)} contains unstripped ELF section .{section}" for section in sections] - - output = file_output(path) - if output is not None and ("not stripped" in output or "with debug_info" in output): - return [f"{rel(path)} appears to contain unstripped ELF debug/symbol data"] - return [] - - -def validate_native_files(root: Path) -> list[str]: - errors: list[str] = [] - for path in iter_files(root): - native = classify_native_file(path) - if native is None: - continue - if native.kind == "elf" and not native.archive: - errors.extend(elf_debug_errors(path)) - return errors - - -def validate_runtime_tree( - root: Path, - target: str | None, - require_runtime: bool, - *, - tool_set: NativeToolSet = "packaged", -) -> list[str]: - errors: list[str] = [] - runtime_dir = runtime_dir_for(root) - if runtime_dir is None: - if require_runtime: - errors.append(f"{rel(root)} is missing a runtime tree") - return errors - - windows = is_windows_target(target, runtime_dir) - required_tools = set(runtime_tools_for_set(target, runtime_dir, tool_set=tool_set)) - bin_dir = runtime_dir / "bin" - if require_runtime and not bin_dir.is_dir(): - errors.append(f"{rel(runtime_dir)} is missing bin") - if bin_dir.is_dir(): - for tool in sorted(required_tools): - path = bin_dir / tool - if not path.is_file(): - errors.append(f"{rel(runtime_dir)} is missing required runtime tool bin/{tool}") - continue - if not windows and not os.access(path, os.X_OK): - errors.append(f"{rel(path)} must be executable") - for path in sorted(bin_dir.iterdir()): - if windows: - if path.name.lower().endswith(".exe") and path.name not in required_tools: - errors.append(f"{rel(path)} is an extra Windows runtime executable") - elif path.name not in required_tools: - errors.append(f"{rel(path)} is an extra runtime tool") - - if tool_set == "tools" and runtime_dir.is_dir(): - allowed = {PurePosixPath("bin") / tool for tool in required_tools} - for path in sorted(runtime_dir.rglob("*")): - if not path.is_file(): - continue - relative = PurePosixPath(path.relative_to(runtime_dir).as_posix()) - if relative not in allowed: - errors.append(f"{rel(path)} is not part of the native tools payload") - - for relative in DEV_RUNTIME_DIRS: - path = runtime_dir.joinpath(*relative.parts) - if path.exists(): - errors.append(f"{rel(path)} is a development-only runtime path") - - for path in sorted(runtime_dir.rglob("*")): - if path.is_dir() and path.name.endswith(".dSYM"): - errors.append(f"{rel(path)} is a development-only debug symbol bundle") - continue - if not path.is_file(): - continue - relative = PurePosixPath(path.relative_to(runtime_dir).as_posix()) - if is_dev_runtime_file(relative, windows=windows): - errors.append(f"{rel(path)} is a development-only runtime file") - - return errors - - -def validate_payload( - root: Path, - target: str | None = None, - *, - require_runtime: bool = True, - tool_set: NativeToolSet = "packaged", -) -> None: - errors = [ - *validate_runtime_tree( - root, - target, - require_runtime=require_runtime, - tool_set=tool_set, - ), - *validate_native_files(root), - ] - if errors: - for error in errors: - print(error, file=sys.stderr) - fail(f"{rel(root)} is not an optimized native runtime payload") - - -def optimize_payload( - root: Path, - target: str | None = None, - *, - strip: bool | Literal["auto"] = "auto", - require_runtime: bool = True, - tool_set: NativeToolSet = "packaged", -) -> None: - prune_runtime_payload(root, target, tool_set=tool_set) - should_strip = strip is True or (strip == "auto" and strip_supported_for_target(target)) - if should_strip: - strip_payload(root) - validate_payload(root, target, require_runtime=require_runtime, tool_set=tool_set) - - -def parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("root", type=Path) - parser.add_argument("--target", default=None) - parser.add_argument("--check", action="store_true", help="validate without mutating the payload") - parser.add_argument( - "--no-strip", - action="store_true", - help="prune but skip native binary stripping before validation", - ) - parser.add_argument( - "--allow-missing-runtime", - action="store_true", - help="validate native files even when the archive is a library-only mobile payload", - ) - parser.add_argument( - "--tool-set", - choices=("packaged", "runtime", "tools"), - default="packaged", - help="which packaged runtime bin tools are expected in the payload", - ) - return parser.parse_args(argv) - - -def main(argv: list[str]) -> int: - args = parse_args(argv) - root = args.root.resolve() - if not root.exists(): - fail(f"payload root does not exist: {root}") - if args.check: - validate_payload( - root, - args.target, - require_runtime=not args.allow_missing_runtime, - tool_set=args.tool_set, - ) - return 0 - optimize_payload( - root, - args.target, - strip=False if args.no_strip else "auto", - require_runtime=not args.allow_missing_runtime, - tool_set=args.tool_set, - ) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) diff --git a/tools/release/package-liboliphaunt-linux-assets.sh b/tools/release/package-liboliphaunt-linux-assets.sh index 29fa7b01..a3a78bf9 100755 --- a/tools/release/package-liboliphaunt-linux-assets.sh +++ b/tools/release/package-liboliphaunt-linux-assets.sh @@ -82,10 +82,10 @@ for tool in pg_dump psql; do done echo "==> Optimizing staged liboliphaunt $target_id release payload" -python3 tools/release/optimize_native_runtime_payload.py "$stage" --target "$target_id" --tool-set runtime +tools/dev/bun.sh tools/release/optimize_native_runtime_payload.mjs "$stage" --target "$target_id" --tool-set runtime echo "==> Optimizing staged oliphaunt-tools $target_id release payload" -python3 tools/release/optimize_native_runtime_payload.py "$tools_stage" --target "$target_id" --tool-set tools +tools/dev/bun.sh tools/release/optimize_native_runtime_payload.mjs "$tools_stage" --target "$target_id" --tool-set tools echo "==> Smoke testing staged liboliphaunt $target_id release layout" env \ diff --git a/tools/release/package-liboliphaunt-macos-assets.sh b/tools/release/package-liboliphaunt-macos-assets.sh index 949293ff..44c98911 100755 --- a/tools/release/package-liboliphaunt-macos-assets.sh +++ b/tools/release/package-liboliphaunt-macos-assets.sh @@ -74,10 +74,10 @@ for tool in pg_dump psql; do done echo "==> Optimizing staged liboliphaunt $target_id release payload" -python3 tools/release/optimize_native_runtime_payload.py "$stage" --target "$target_id" --tool-set runtime +tools/dev/bun.sh tools/release/optimize_native_runtime_payload.mjs "$stage" --target "$target_id" --tool-set runtime echo "==> Optimizing staged oliphaunt-tools $target_id release payload" -python3 tools/release/optimize_native_runtime_payload.py "$tools_stage" --target "$target_id" --tool-set tools +tools/dev/bun.sh tools/release/optimize_native_runtime_payload.mjs "$tools_stage" --target "$target_id" --tool-set tools echo "==> Smoke testing staged liboliphaunt $target_id release layout" env \ diff --git a/tools/release/package-liboliphaunt-windows-assets.ps1 b/tools/release/package-liboliphaunt-windows-assets.ps1 index eefd7013..4947d0ce 100644 --- a/tools/release/package-liboliphaunt-windows-assets.ps1 +++ b/tools/release/package-liboliphaunt-windows-assets.ps1 @@ -147,13 +147,13 @@ if (Test-Path $StagedIcu) { } Write-Output "==> Optimizing staged liboliphaunt $TargetId release payload" -python tools/release/optimize_native_runtime_payload.py $Stage --target $TargetId --tool-set runtime +bun tools/release/optimize_native_runtime_payload.mjs $Stage --target $TargetId --tool-set runtime if ($LASTEXITCODE -ne 0) { Fail "failed to optimize staged Windows liboliphaunt release payload" } Write-Output "==> Optimizing staged oliphaunt-tools $TargetId release payload" -python tools/release/optimize_native_runtime_payload.py $ToolsStage --target $TargetId --tool-set tools +bun tools/release/optimize_native_runtime_payload.mjs $ToolsStage --target $TargetId --tool-set tools if ($LASTEXITCODE -ne 0) { Fail "failed to optimize staged Windows oliphaunt-tools release payload" } diff --git a/tools/release/package_liboliphaunt_cargo_artifacts.py b/tools/release/package_liboliphaunt_cargo_artifacts.py index f8ba5fc8..4c32d390 100644 --- a/tools/release/package_liboliphaunt_cargo_artifacts.py +++ b/tools/release/package_liboliphaunt_cargo_artifacts.py @@ -17,7 +17,6 @@ from typing import NoReturn import artifact_targets -import optimize_native_runtime_payload import product_metadata @@ -74,6 +73,20 @@ def cargo_package_name(target_id: str, *, package_base: str = PRODUCT) -> str: return f"{package_base}-{target_id}" +def optimize_native_payload(payload_root: Path, target: str, *, tool_set: str) -> None: + run( + [ + "tools/dev/bun.sh", + "tools/release/optimize_native_runtime_payload.mjs", + str(payload_root), + "--target", + target, + "--tool-set", + tool_set, + ] + ) + + def cargo_links_name(target_id: str, *, artifact_product: str = PRODUCT) -> str: product = artifact_product.replace("-", "_") return f"oliphaunt_artifact_{product}_{target_id.replace('-', '_')}" @@ -738,12 +751,12 @@ def package_target( extract_archive(archive, extracted_root) tools_root = source_root / f"{target.target}-tools-extracted" extract_archive(tools_archive, tools_root) - optimize_native_runtime_payload.optimize_payload( + optimize_native_payload( extracted_root, target.target, tool_set="runtime", ) - optimize_native_runtime_payload.optimize_payload( + optimize_native_payload( tools_root, target.target, tool_set="tools", diff --git a/tools/release/release.py b/tools/release/release.py index c8aca5b9..ec88155d 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -19,7 +19,6 @@ import artifact_targets import extension_artifact_targets -import optimize_native_runtime_payload import package_liboliphaunt_cargo_artifacts import package_liboliphaunt_wasix_cargo_artifacts import product_metadata @@ -33,6 +32,12 @@ "tools/dev/bun.sh", "tools/release/check_registry_publication.mjs", ] +NATIVE_PAYLOAD_POLICY = json.loads( + (ROOT / "tools/release/native-runtime-payload-policy.json").read_text(encoding="utf-8") +) +NATIVE_RUNTIME_TOOL_STEMS = tuple(NATIVE_PAYLOAD_POLICY["nativeRuntimeToolStems"]) +NATIVE_TOOLS_TOOL_STEMS = tuple(NATIVE_PAYLOAD_POLICY["nativeToolsToolStems"]) +NATIVE_PACKAGED_TOOL_STEMS = (*NATIVE_RUNTIME_TOOL_STEMS, *NATIVE_TOOLS_TOOL_STEMS) def fail(message: str) -> NoReturn: @@ -47,6 +52,52 @@ def run(args: list[str], *, cwd: Path = ROOT, env: dict[str, str] | None = None) raise SystemExit(result.returncode) +def is_windows_native_target(target: str | None, runtime_dir: Path | None = None) -> bool: + if target is not None and target.startswith("windows-"): + return True + if runtime_dir is None: + return False + bin_dir = runtime_dir / "bin" + return any((bin_dir / f"{stem}.exe").exists() for stem in NATIVE_PACKAGED_TOOL_STEMS) + + +def required_native_runtime_tools(target: str | None, runtime_dir: Path | None = None) -> tuple[str, ...]: + if is_windows_native_target(target, runtime_dir): + return tuple(f"{stem}.exe" for stem in NATIVE_RUNTIME_TOOL_STEMS) + return NATIVE_RUNTIME_TOOL_STEMS + + +def required_native_tools_package_tools( + target: str | None, + runtime_dir: Path | None = None, +) -> tuple[str, ...]: + if is_windows_native_target(target, runtime_dir): + return tuple(f"{stem}.exe" for stem in NATIVE_TOOLS_TOOL_STEMS) + return NATIVE_TOOLS_TOOL_STEMS + + +def required_runtime_member_paths(target: str | None, *, prefix: str) -> list[str]: + return [f"{prefix.rstrip('/')}/{tool}" for tool in required_native_runtime_tools(target)] + + +def required_tools_member_paths(target: str | None, *, prefix: str) -> list[str]: + return [f"{prefix.rstrip('/')}/{tool}" for tool in required_native_tools_package_tools(target)] + + +def run_native_payload_optimizer(root: Path, target: str, *, tool_set: str) -> None: + run( + [ + "tools/dev/bun.sh", + "tools/release/optimize_native_runtime_payload.mjs", + str(root), + "--target", + target, + "--tool-set", + tool_set, + ] + ) + + def output(args: list[str], *, cwd: Path = ROOT) -> str: return subprocess.check_output(args, cwd=cwd, text=True).strip() @@ -2427,7 +2478,7 @@ def stage_liboliphaunt_npm_payloads( ) extract_tar_tree(archive, "runtime", stage / "runtime") ensure_native_tools_absent_from_runtime(stage, target.target) - optimize_native_runtime_payload.optimize_payload(stage, target.target, tool_set="runtime") + run_native_payload_optimizer(stage, target.target, tool_set="runtime") stages[package_name] = stage return stages @@ -2435,7 +2486,7 @@ def stage_liboliphaunt_npm_payloads( def ensure_native_tools_absent_from_runtime(stage: Path, target: str) -> None: runtime_dir = stage / "runtime" leaked_tools: list[str] = [] - for tool in optimize_native_runtime_payload.required_tools_package_tools(target, runtime_dir): + for tool in required_native_tools_package_tools(target, runtime_dir): path = runtime_dir / "bin" / tool if path.exists(): leaked_tools.append(f"runtime/bin/{tool}") @@ -2472,14 +2523,14 @@ def stage_liboliphaunt_tools_npm_payloads( target=target.target, ) archive = asset_dir / target.asset_name(version) - for tool in optimize_native_runtime_payload.required_tools_package_tools(target.target): + for tool in required_native_tools_package_tools(target.target): member = f"runtime/bin/{tool}" destination = stage / member if archive.name.endswith(".zip"): extract_zip_file(archive, member, destination, mode=0o755) else: extract_tar_file(archive, member, destination) - optimize_native_runtime_payload.optimize_payload(stage, target.target, tool_set="tools") + run_native_payload_optimizer(stage, target.target, tool_set="tools") stages[package_name] = stage return stages @@ -2600,7 +2651,7 @@ def liboliphaunt_npm_tarballs( continue if target.library_relative_path is None: fail(f"{target.id} must declare library_relative_path for npm artifact package publication") - runtime_members = optimize_native_runtime_payload.required_runtime_member_paths( + runtime_members = required_runtime_member_paths( target.target, prefix="package/runtime/bin", ) @@ -2623,7 +2674,7 @@ def liboliphaunt_npm_tarballs( ): if targets is not None and target.target not in targets: continue - runtime_members = optimize_native_runtime_payload.required_tools_member_paths( + runtime_members = required_tools_member_paths( target.target, prefix="package/runtime/bin", ) From a45efaeb0ec6723f30455b72fefeb67d7bce9663 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Fri, 26 Jun 2026 23:46:59 +0000 Subject: [PATCH 133/137] chore: port release version check to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 7 +- docs/internal/IMPLEMENTATION_CHECKLIST.md | 2 +- tools/policy/python-entrypoints.allowlist | 1 - tools/release/check_release_versions.mjs | 692 ++++++++++++++++++ tools/release/check_release_versions.py | 440 ----------- tools/release/release.py | 7 +- 6 files changed, 702 insertions(+), 447 deletions(-) create mode 100644 tools/release/check_release_versions.mjs delete mode 100755 tools/release/check_release_versions.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 9581e966..e16cb5ea 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -1021,8 +1021,11 @@ until the current-state gates here are checked with fresh local evidence. - On 2026-06-26, the product-scoped GitHub release asset checker moved from Python to Bun. The new `check_github_release_assets.mjs` reuses the shared expected-asset and exact-extension manifest validation from the attestation - verifier, while `check_release_versions.py` now shells to the Bun checker for - released dependency asset verification. + verifier. `check_release_versions.mjs` now owns release-version and released + dependency asset verification directly in Bun. Direct smokes passed for an + empty selection, `oliphaunt-swift` plus `liboliphaunt-native`, the JS/native + dependency closure, and the React Native/Swift/Kotlin/native dependency + closure. - On 2026-06-26, native runtime payload optimization moved from Python to Bun. `optimize_native_runtime_payload.mjs` now owns pruning, stripping, and validation for root runtime payloads and split `oliphaunt-tools` payloads, diff --git a/docs/internal/IMPLEMENTATION_CHECKLIST.md b/docs/internal/IMPLEMENTATION_CHECKLIST.md index 82eab842..403c851b 100644 --- a/docs/internal/IMPLEMENTATION_CHECKLIST.md +++ b/docs/internal/IMPLEMENTATION_CHECKLIST.md @@ -984,7 +984,7 @@ Run before claiming this architecture complete: WASIX runtime/AOT, exact-extension, SDK, mobile app, `artifact-builders`, and `required` jobs before the WASIX release version bump below. - [x] Local release version freshness no longer blocks the selected product - closure. `tools/release/check_release_versions.py --products-json + closure. `tools/dev/bun.sh tools/release/check_release_versions.mjs --products-json "$(cat target/release-dry-run-local/products.json)" --head-ref HEAD` first failed because `liboliphaunt-wasix` and `oliphaunt-wasix-rust` still used `0.5.1` while legacy tag `0.5.1` points at the old release commit. The diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index 130f74a7..726eb070 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -11,7 +11,6 @@ tools/release/check_artifact_targets.py tools/release/check_consumer_shape.py tools/release/check_liboliphaunt_release_assets.py tools/release/check_release_metadata.py -tools/release/check_release_versions.py tools/release/check_staged_artifacts.py tools/release/extension_artifact_targets.py tools/release/local_registry_publish.py diff --git a/tools/release/check_release_versions.mjs b/tools/release/check_release_versions.mjs new file mode 100644 index 00000000..42766173 --- /dev/null +++ b/tools/release/check_release_versions.mjs @@ -0,0 +1,692 @@ +#!/usr/bin/env bun +import { execFileSync, spawnSync } from "node:child_process"; +import { existsSync, readFileSync } from "node:fs"; +import path from "node:path"; +import { currentVersion } from "./product-version.mjs"; + +const ROOT = path.resolve(import.meta.dir, "../.."); +const TOOL = "check_release_versions.mjs"; +const REGISTRY_TARGETS = new Set(["crates-io", "npm", "jsr", "maven-central"]); + +function fail(message) { + console.error(`${TOOL}: ${message}`); + process.exit(1); +} + +function rel(file) { + const relative = path.relative(ROOT, file); + return relative.startsWith("..") ? file : relative.split(path.sep).join("/"); +} + +function readText(relativePath) { + return readFileSync(path.join(ROOT, relativePath), "utf8"); +} + +function readJson(relativePath) { + const value = JSON.parse(readText(relativePath)); + if (value === null || Array.isArray(value) || typeof value !== "object") { + fail(`${relativePath} must contain a JSON object`); + } + return value; +} + +function readToml(relativePath) { + const file = path.join(ROOT, relativePath); + if (!existsSync(file)) { + fail(`missing ${relativePath}`); + } + const value = Bun.TOML.parse(readFileSync(file, "utf8")); + if (value === null || Array.isArray(value) || typeof value !== "object") { + fail(`${relativePath} must contain a TOML table`); + } + return value; +} + +function moonBin() { + if (process.env.MOON_BIN) { + return process.env.MOON_BIN; + } + const protoMoon = path.join(process.env.HOME ?? "", ".proto/bin/moon"); + return existsSync(protoMoon) ? protoMoon : "moon"; +} + +function gitOutput(args) { + return execFileSync("git", args, { cwd: ROOT, encoding: "utf8" }).trim(); +} + +function run(args) { + const result = spawnSync(args[0], args.slice(1), { cwd: ROOT, stdio: "inherit" }); + if (result.error) { + fail(`failed to run ${args.join(" ")}: ${result.error.message}`); + } + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + +function commandJson(args) { + const output = execFileSync(args[0], args.slice(1), { + cwd: ROOT, + encoding: "utf8", + maxBuffer: 100 * 1024 * 1024, + }); + const value = JSON.parse(output); + if (value === null || Array.isArray(value) || typeof value !== "object") { + fail(`${args[0]} did not return a JSON object`); + } + return value; +} + +function parseStableVersion(version) { + const match = /^([0-9]+)[.]([0-9]+)[.]([0-9]+)$/.exec(version); + if (!match) { + fail(`release version must be stable x.y.z for automated publish, got ${JSON.stringify(version)}`); + } + return match.slice(1).map((part) => Number.parseInt(part, 10)); +} + +function compareVersion(left, right) { + for (let index = 0; index < 3; index += 1) { + if (left[index] !== right[index]) { + return left[index] - right[index]; + } + } + return 0; +} + +function formatVersion(version) { + return version.join("."); +} + +function assertStringList(value, context) { + if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) { + fail(`${context} must be a string list`); + } + return value; +} + +function releasePleasePackagesByComponent() { + const config = readJson("release-please-config.json"); + const packages = config.packages; + if (packages === null || Array.isArray(packages) || typeof packages !== "object") { + fail("release-please-config.json must define packages"); + } + const byComponent = new Map(); + for (const [packagePath, packageConfig] of Object.entries(packages)) { + if (packageConfig === null || Array.isArray(packageConfig) || typeof packageConfig !== "object") { + fail(`${packagePath} release-please config must be an object`); + } + const component = packageConfig.component; + if (typeof component !== "string" || component.length === 0) { + fail(`${packagePath}.component must be a non-empty string`); + } + if (byComponent.has(component)) { + fail(`duplicate release-please component ${component}`); + } + byComponent.set(component, { packagePath, packageConfig }); + } + return { config, byComponent }; +} + +function moonProjectsById() { + const data = commandJson([moonBin(), "query", "projects"]); + const projects = data.projects; + if (!Array.isArray(projects)) { + fail("moon query projects did not return a projects array"); + } + const parsed = new Map(); + for (const project of projects) { + if (project === null || Array.isArray(project) || typeof project !== "object" || typeof project.id !== "string") { + continue; + } + const config = project.config && typeof project.config === "object" && !Array.isArray(project.config) ? project.config : {}; + const rawDeps = project.dependencies ?? config.dependsOn ?? []; + const dependencyScopes = {}; + if (Array.isArray(rawDeps)) { + for (const dependency of rawDeps) { + if (typeof dependency === "string") { + dependencyScopes[dependency] = "production"; + } else if ( + dependency !== null && + typeof dependency === "object" && + !Array.isArray(dependency) && + typeof dependency.id === "string" + ) { + dependencyScopes[dependency.id] = String(dependency.scope || "production"); + } + } + } + parsed.set(project.id, { + id: project.id, + source: project.source || config.source || "", + dependsOn: Object.keys(dependencyScopes).sort(), + dependencyScopes, + tags: Array.isArray(config.tags) ? [...config.tags].sort() : [], + project: config.project && typeof config.project === "object" && !Array.isArray(config.project) ? config.project : {}, + }); + } + return parsed; +} + +function moonReleaseProjectsByComponent(projects) { + const products = new Map(); + for (const project of projects.values()) { + const metadata = + project.project && + typeof project.project === "object" && + !Array.isArray(project.project) && + project.project.metadata && + typeof project.project.metadata === "object" && + !Array.isArray(project.project.metadata) + ? project.project.metadata + : {}; + const release = + metadata.release && typeof metadata.release === "object" && !Array.isArray(metadata.release) + ? metadata.release + : undefined; + if (!project.tags.includes("release-product")) { + if (release !== undefined) { + fail(`Moon project ${project.id} declares release metadata but is not tagged release-product`); + } + continue; + } + if (release === undefined) { + fail(`Moon release product ${project.id} must declare project.metadata.release`); + } + if (release.component !== project.id) { + fail(`Moon release product ${project.id} release.component must match the project id`); + } + if (typeof release.packagePath !== "string" || release.packagePath.length === 0) { + fail(`Moon release product ${project.id} must declare release.packagePath`); + } + if (products.has(release.component)) { + fail(`duplicate Moon release component ${release.component}`); + } + products.set(release.component, { + projectId: project.id, + projectSource: project.source, + path: release.packagePath, + release, + }); + } + if (products.size === 0) { + fail("Moon project graph does not contain any release-product projects"); + } + return products; +} + +function releasePackagePaths(projects) { + const { byComponent } = releasePleasePackagesByComponent(); + const moonProducts = moonReleaseProjectsByComponent(projects); + const moonComponents = [...moonProducts.keys()].sort(); + const releaseComponents = [...byComponent.keys()].sort(); + if (JSON.stringify(moonComponents) !== JSON.stringify(releaseComponents)) { + fail( + `Moon release-product components must match release-please components: moon=${JSON.stringify( + moonComponents, + )}, release-please=${JSON.stringify(releaseComponents)}`, + ); + } + const paths = new Map(); + for (const component of moonComponents) { + const moonPath = moonProducts.get(component).path; + const releasePath = byComponent.get(component).packagePath; + if (moonPath !== releasePath) { + fail( + `${component} Moon release.packagePath ${JSON.stringify(moonPath)} must match release-please package path ${JSON.stringify( + releasePath, + )}`, + ); + } + paths.set(component, moonPath); + } + return paths; +} + +function tagPrefix(product) { + const { config, byComponent } = releasePleasePackagesByComponent(); + const packageConfig = byComponent.get(product)?.packageConfig; + if (!packageConfig) { + fail(`unknown release-please component ${product}`); + } + if (packageConfig.component !== product) { + fail(`${product} release-please component must match product id`); + } + if (config["include-v-in-tag"] !== true) { + fail("release-please must include v in product tags"); + } + if (config["tag-separator"] !== "-") { + fail("release-please tag-separator must be '-'"); + } + return `${product}-v`; +} + +function graphProducts(projects) { + const paths = releasePackagePaths(projects); + const manifest = readJson(".release-please-manifest.json"); + const products = {}; + for (const [product, packagePath] of [...paths.entries()].sort(([left], [right]) => left.localeCompare(right))) { + const metadata = readToml(path.join(packagePath, "release.toml")); + if (metadata.id !== product) { + fail(`${packagePath}/release.toml must declare id = ${JSON.stringify(product)}`); + } + if (!(packagePath in manifest)) { + fail(`.release-please-manifest.json is missing ${packagePath}`); + } + products[product] = { + ...metadata, + path: packagePath, + tag_prefix: tagPrefix(product), + }; + } + return products; +} + +function loadGraph() { + const moonProjects = moonProjectsById(); + return { + policy: { + repository: "f0rr0/oliphaunt", + default_branch: "main", + versioning: "independent", + }, + products: graphProducts(moonProjects), + moon_projects: Object.fromEntries(moonProjects), + }; +} + +function parseProducts(raw, graph) { + const products = graph.products; + if (products === null || Array.isArray(products) || typeof products !== "object") { + fail("release metadata must define [products.] entries"); + } + if (raw === undefined) { + return Object.keys(products).sort(); + } + const value = JSON.parse(raw); + if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) { + fail("--products-json must be a JSON string list"); + } + const unknown = value.filter((product) => !(product in products)).sort(); + if (unknown.length > 0) { + fail(`unknown release products: ${unknown.join(", ")}`); + } + return value; +} + +function registryCommand(args) { + return ["tools/dev/bun.sh", "tools/release/check_registry_publication.mjs", ...args]; +} + +function registryRun(args) { + run(registryCommand(args)); +} + +function registryJson(args) { + return commandJson(registryCommand(args)); +} + +function registryAssertProductPublication(product, { requirePublished, versionOverride } = {}) { + const args = ["--product", product, requirePublished ? "--require-published" : "--require-unpublished"]; + if (versionOverride !== undefined) { + args.push("--version", versionOverride); + } + registryRun(args); +} + +function registryReportProductPublication(product) { + registryRun(["--product", product, "--report"]); +} + +function registryQueryProductPublication(product) { + const data = registryJson(["query-product-publication", "--product", product]); + if (!Array.isArray(data.packages) || !Array.isArray(data.missing) || !Array.isArray(data.published)) { + fail("registry publication helper returned malformed publication status"); + } + return data; +} + +function verifyGithubReleaseAssets(product, version) { + run([ + "tools/dev/bun.sh", + "tools/release/check_github_release_assets.mjs", + product, + "--version", + version, + "--default-assets", + ]); +} + +function tagMatchPattern(prefix) { + return prefix ? `${prefix}[0-9]*` : "[0-9]*"; +} + +function tagPrefixes(config) { + if (typeof config.tag_prefix !== "string" || config.tag_prefix.length === 0) { + fail("release products must declare tag_prefix"); + } + const legacyPrefixes = config.legacy_tag_prefixes ?? []; + assertStringList(legacyPrefixes, "legacy_tag_prefixes"); + return [config.tag_prefix, ...legacyPrefixes]; +} + +function productTags(prefix) { + const output = execFileSync("git", ["tag", "--list", tagMatchPattern(prefix)], { + cwd: ROOT, + encoding: "utf8", + }); + return output + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean); +} + +function tagVersion(prefix, tag) { + if (!tag.startsWith(prefix)) { + return undefined; + } + const version = tag.slice(prefix.length); + if (!/^[0-9]+[.][0-9]+[.][0-9]+$/.test(version)) { + return undefined; + } + return parseStableVersion(version); +} + +function tagCommit(tag) { + return gitOutput(["rev-list", "-n", "1", tag]); +} + +function tagExists(tag) { + const result = spawnSync("git", ["rev-parse", "--verify", "--quiet", `refs/tags/${tag}^{commit}`], { + cwd: ROOT, + stdio: "ignore", + }); + return result.status === 0; +} + +function commitForRef(ref) { + return gitOutput(["rev-parse", `${ref}^{commit}`]); +} + +function reactNativeCompatibilityVersions() { + const packageJson = JSON.parse(readText("src/sdks/react-native/package.json")); + const metadata = packageJson.oliphaunt; + if (metadata === null || Array.isArray(metadata) || typeof metadata !== "object") { + fail("React Native package.json must declare oliphaunt compatibility metadata"); + } + if (typeof metadata.swiftSdkVersion !== "string" || typeof metadata.kotlinSdkVersion !== "string") { + fail("React Native compatibility metadata must include Swift and Kotlin SDK versions"); + } + return [metadata.swiftSdkVersion, metadata.kotlinSdkVersion]; +} + +function typescriptCompatibilityVersions() { + const packageJson = JSON.parse(readText("src/sdks/js/package.json")); + const metadata = packageJson.oliphaunt; + if (metadata === null || Array.isArray(metadata) || typeof metadata !== "object") { + fail("TypeScript package.json must declare oliphaunt compatibility metadata"); + } + if ( + typeof metadata.liboliphauntVersion !== "string" || + typeof metadata.brokerVersion !== "string" || + typeof metadata.nodeDirectAddonVersion !== "string" + ) { + fail("TypeScript compatibility metadata must include liboliphaunt, broker, and Node direct versions"); + } + return [metadata.liboliphauntVersion, metadata.brokerVersion, metadata.nodeDirectAddonVersion]; +} + +async function dependencyVersionFor(consumer, dependency) { + if (consumer === "oliphaunt-swift" && dependency === "liboliphaunt-native") { + return readText("src/sdks/swift/LIBOLIPHAUNT_VERSION").trim(); + } + if (consumer === "oliphaunt-react-native" && dependency === "oliphaunt-swift") { + return reactNativeCompatibilityVersions()[0]; + } + if (consumer === "oliphaunt-react-native" && dependency === "oliphaunt-kotlin") { + return reactNativeCompatibilityVersions()[1]; + } + if (consumer === "oliphaunt-js" && dependency === "liboliphaunt-native") { + return typescriptCompatibilityVersions()[0]; + } + if (consumer === "oliphaunt-js" && dependency === "oliphaunt-broker") { + return typescriptCompatibilityVersions()[1]; + } + if (consumer === "oliphaunt-js" && dependency === "oliphaunt-node-direct") { + return typescriptCompatibilityVersions()[2]; + } + return currentVersion(dependency); +} + +async function validateProduct(product, config, headRef) { + if (typeof config.tag_prefix !== "string" || config.tag_prefix.length === 0) { + fail(`${product} must declare tag_prefix`); + } + const version = await currentVersion(product); + const current = parseStableVersion(version); + const currentTag = `${config.tag_prefix}${version}`; + const headCommit = commitForRef(headRef); + const tags = productTags(config.tag_prefix); + if (tags.includes(currentTag)) { + const currentTagCommit = tagCommit(currentTag); + if (currentTagCommit !== headCommit) { + fail( + `${product} version ${version} is already tagged as ${currentTag} at ${currentTagCommit}, not release commit ${headCommit}; merge the release-please release PR before publishing`, + ); + } + return true; + } + const previousVersions = []; + for (const candidatePrefix of tagPrefixes(config)) { + for (const tag of productTags(candidatePrefix)) { + const parsed = tagVersion(candidatePrefix, tag); + if (parsed !== undefined) { + previousVersions.push(parsed); + } + } + } + if (previousVersions.length > 0) { + const latest = previousVersions.reduce((max, candidate) => + compareVersion(candidate, max) > 0 ? candidate : max, + ); + if (compareVersion(current, latest) <= 0) { + fail( + `${product} version ${version} is not newer than latest tagged version ${formatVersion( + latest, + )}; merge the release-please release PR before publishing`, + ); + } + } + return false; +} + +async function validateRegistryPublication(products, graph, currentTagAtHead, headRef) { + const graphProducts = graph.products; + const headCommit = commitForRef(headRef); + for (const product of products) { + const config = graphProducts[product]; + const targets = assertStringList(config.publish_targets ?? [], `${product}.publish_targets`); + const registryTargets = targets.filter((target) => REGISTRY_TARGETS.has(target)); + if (registryTargets.length === 0) { + continue; + } + if (currentTagAtHead[product] === true) { + if (registryTargets.includes("crates-io")) { + registryAssertProductPublication(product, { requirePublished: true }); + } else { + registryReportProductPublication(product); + } + continue; + } + const { packages, published } = registryQueryProductPublication(product); + if (packages.length === 0) { + console.log(`${product} has no external registry packages to check`); + continue; + } + if (published.length > 0) { + if (typeof config.tag_prefix !== "string" || config.tag_prefix.length === 0) { + fail(`${product} must declare tag_prefix`); + } + const version = await currentVersion(product); + const currentTag = `${config.tag_prefix}${version}`; + fail( + `${product} version ${version} is already published in public registries: ${published + .map((item) => String(item.label)) + .join( + ", ", + )}; the matching product tag ${currentTag} is missing or does not point at release commit ${headCommit}. If this was an intentional first package identity bootstrap, create and push that product tag at the same release commit, then rerun the release workflow as a completion run. Otherwise merge the release-please release PR before publishing.`, + ); + } + console.log( + `${product} registry unpublished check passed: ${packages.map((item) => String(item.label)).join(", ")}`, + ); + } +} + +function releaseProductProjectId(product, products, projects) { + if (product in projects) { + return product; + } + const packagePath = products[product]?.path; + if (typeof packagePath !== "string" || packagePath.length === 0) { + fail(`release product ${product} is missing package path metadata`); + } + const matches = Object.values(projects) + .filter((project) => packagePath === project.source || packagePath.startsWith(`${project.source}/`)) + .sort((left, right) => right.source.length - left.source.length); + if (matches.length === 0) { + fail(`release product ${product} has no owning Moon project for ${packagePath}`); + } + return matches[0].id; +} + +function validateReleasedDependencyArtifacts(consumer, dependency, dependencyVersion, graph) { + const dependencyConfig = graph.products[dependency]; + if (dependencyConfig === null || Array.isArray(dependencyConfig) || typeof dependencyConfig !== "object") { + fail(`${consumer} declares unknown release dependency ${dependency}`); + } + const targets = assertStringList(dependencyConfig.publish_targets ?? [], `${dependency}.publish_targets`); + const registryTargets = targets.filter((target) => REGISTRY_TARGETS.has(target)); + if (registryTargets.length > 0) { + registryAssertProductPublication(dependency, { + requirePublished: true, + versionOverride: dependencyVersion, + }); + } + if (targets.includes("github-release-assets")) { + verifyGithubReleaseAssets(dependency, dependencyVersion); + } +} + +function validateDependencyTag(consumer, dependency, dependencyVersion, graph, selected) { + parseStableVersion(dependencyVersion); + if (selected.has(dependency)) { + return; + } + const dependencyConfig = graph.products[dependency]; + if (dependencyConfig === null || Array.isArray(dependencyConfig) || typeof dependencyConfig !== "object") { + fail(`${consumer} declares unknown release dependency ${dependency}`); + } + if (typeof dependencyConfig.tag_prefix !== "string" || dependencyConfig.tag_prefix.length === 0) { + fail(`${dependency} must declare tag_prefix`); + } + const tag = `${dependencyConfig.tag_prefix}${dependencyVersion}`; + if (!tagExists(tag)) { + fail( + `${consumer} depends on ${dependency} ${dependencyVersion}, but release tag ${tag} does not exist and ${dependency} is not selected for this release`, + ); + } + validateReleasedDependencyArtifacts(consumer, dependency, dependencyVersion, graph); +} + +async function validateReleaseDependencies(products, graph) { + const selected = new Set(products); + const graphProducts = graph.products; + const moonProjects = graph.moon_projects; + if (moonProjects === null || Array.isArray(moonProjects) || typeof moonProjects !== "object") { + fail("Moon project graph is missing from release metadata"); + } + const productProject = Object.fromEntries( + Object.keys(graphProducts).map((product) => [ + product, + releaseProductProjectId(product, graphProducts, moonProjects), + ]), + ); + const projectProduct = Object.fromEntries( + Object.entries(productProject).map(([product, project]) => [project, product]), + ); + for (const product of products) { + const config = graphProducts[product]; + if (config === null || Array.isArray(config) || typeof config !== "object") { + fail(`selected product ${product} is missing from release metadata`); + } + const project = moonProjects[productProject[product]] ?? {}; + const dependencies = (Array.isArray(project.dependsOn) ? project.dependsOn : []) + .filter((dependency) => dependency in projectProduct) + .map((dependency) => projectProduct[dependency]); + for (const dependency of dependencies) { + validateDependencyTag( + product, + dependency, + await dependencyVersionFor(product, dependency), + graph, + selected, + ); + } + } +} + +function parseArgs(argv) { + const args = { + productsJson: undefined, + headRef: "HEAD", + checkRegistries: false, + }; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === "--products-json") { + if (index + 1 >= argv.length) { + fail("--products-json requires a value"); + } + args.productsJson = argv[index + 1]; + index += 1; + } else if (value.startsWith("--products-json=")) { + args.productsJson = value.slice("--products-json=".length); + } else if (value === "--head-ref") { + if (index + 1 >= argv.length) { + fail("--head-ref requires a value"); + } + args.headRef = argv[index + 1]; + index += 1; + } else if (value.startsWith("--head-ref=")) { + args.headRef = value.slice("--head-ref=".length); + } else if (value === "--check-registries") { + args.checkRegistries = true; + } else if (value === "-h" || value === "--help") { + console.log("usage: tools/release/check_release_versions.mjs [--products-json JSON] [--head-ref REF] [--check-registries]"); + process.exit(0); + } else { + fail(`unknown argument ${value}`); + } + } + return args; +} + +async function main(argv) { + const args = parseArgs(argv); + const graph = loadGraph(); + const selected = parseProducts(args.productsJson, graph); + const currentTagAtHead = {}; + for (const product of selected) { + currentTagAtHead[product] = await validateProduct(product, graph.products[product], args.headRef); + } + await validateReleaseDependencies(selected, graph); + if (args.checkRegistries) { + await validateRegistryPublication(selected, graph, currentTagAtHead, args.headRef); + } + console.log("release version checks passed"); +} + +if (import.meta.main) { + await main(Bun.argv.slice(2)); +} diff --git a/tools/release/check_release_versions.py b/tools/release/check_release_versions.py deleted file mode 100755 index 923fd729..00000000 --- a/tools/release/check_release_versions.py +++ /dev/null @@ -1,440 +0,0 @@ -#!/usr/bin/env python3 -"""Validate selected product versions are publishable from current tags.""" - -from __future__ import annotations - -import argparse -import json -import re -import subprocess -import sys -from pathlib import Path -from typing import NoReturn - -import product_metadata -import release_plan - - -ROOT = Path(__file__).resolve().parents[2] -REGISTRY_TARGETS = { - "crates-io", - "npm", - "jsr", - "maven-central", -} - - -def fail(message: str) -> NoReturn: - print(f"check_release_versions.py: {message}", file=sys.stderr) - raise SystemExit(1) - - -def load_graph() -> dict: - return release_plan.load_graph() - - -def parse_products(raw: str | None, graph: dict) -> list[str]: - products = graph.get("products") - if not isinstance(products, dict): - fail("release metadata must define [products.] entries") - if raw is None: - return sorted(products) - value = json.loads(raw) - if not isinstance(value, list) or not all(isinstance(item, str) for item in value): - fail("--products-json must be a JSON string list") - unknown = sorted(set(value) - set(products)) - if unknown: - fail(f"unknown release products: {', '.join(unknown)}") - return value - - -def parse_stable_version(version: str) -> tuple[int, int, int]: - match = re.fullmatch(r"([0-9]+)[.]([0-9]+)[.]([0-9]+)", version) - if not match: - fail(f"release version must be stable x.y.z for automated publish, got {version!r}") - return tuple(int(part) for part in match.groups()) - - -def git_output(args: list[str]) -> str: - return subprocess.check_output(["git", *args], cwd=ROOT, text=True).strip() - - -def registry_command(args: list[str]) -> list[str]: - return [ - "tools/dev/bun.sh", - "tools/release/check_registry_publication.mjs", - *args, - ] - - -def registry_run(args: list[str]) -> None: - result = subprocess.run(registry_command(args), cwd=ROOT, check=False) - if result.returncode != 0: - raise SystemExit(result.returncode) - - -def registry_json(args: list[str]) -> dict: - output = subprocess.check_output(registry_command(args), cwd=ROOT, text=True) - value = json.loads(output) - if not isinstance(value, dict): - fail("registry publication helper did not return a JSON object") - return value - - -def registry_assert_product_publication( - product: str, - *, - require_published: bool, - version_override: str | None = None, -) -> None: - args = [ - "--product", - product, - "--require-published" if require_published else "--require-unpublished", - ] - if version_override is not None: - args.extend(["--version", version_override]) - registry_run(args) - - -def registry_report_product_publication(product: str) -> None: - registry_run(["--product", product, "--report"]) - - -def registry_query_product_publication(product: str) -> tuple[list[dict], list[dict], list[dict]]: - data = registry_json(["query-product-publication", "--product", product]) - packages = data.get("packages") - missing = data.get("missing") - published = data.get("published") - if not isinstance(packages, list) or not isinstance(missing, list) or not isinstance(published, list): - fail("registry publication helper returned malformed publication status") - return packages, missing, published - - -def verify_github_release_assets(product: str, version: str) -> None: - result = subprocess.run( - [ - "tools/dev/bun.sh", - "tools/release/check_github_release_assets.mjs", - product, - "--version", - version, - "--default-assets", - ], - cwd=ROOT, - check=False, - ) - if result.returncode != 0: - raise SystemExit(result.returncode) - - -def tag_match_pattern(prefix: str) -> str: - return f"{prefix}[0-9]*" if prefix else "[0-9]*" - - -def tag_prefixes(config: dict) -> list[str]: - prefix = config.get("tag_prefix") - if not isinstance(prefix, str) or not prefix: - fail("release products must declare tag_prefix") - legacy_prefixes = config.get("legacy_tag_prefixes", []) - if not isinstance(legacy_prefixes, list) or not all( - isinstance(item, str) for item in legacy_prefixes - ): - fail("legacy_tag_prefixes must be a string list when present") - return [prefix, *legacy_prefixes] - - -def product_tags(prefix: str) -> list[str]: - output = subprocess.check_output( - ["git", "tag", "--list", tag_match_pattern(prefix)], - cwd=ROOT, - text=True, - ) - return [line.strip() for line in output.splitlines() if line.strip()] - - -def tag_version(prefix: str, tag: str) -> tuple[int, int, int] | None: - if not tag.startswith(prefix): - return None - version = tag[len(prefix) :] - if not re.fullmatch(r"[0-9]+[.][0-9]+[.][0-9]+", version): - return None - return parse_stable_version(version) - - -def tag_commit(tag: str) -> str: - return git_output(["rev-list", "-n", "1", tag]) - - -def tag_exists(tag: str) -> bool: - result = subprocess.run( - ["git", "rev-parse", "--verify", "--quiet", f"refs/tags/{tag}^{{commit}}"], - cwd=ROOT, - check=False, - text=True, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - return result.returncode == 0 - - -def commit_for_ref(ref: str) -> str: - return git_output(["rev-parse", f"{ref}^{{commit}}"]) - - -def read_text(path: str) -> str: - return (ROOT / path).read_text(encoding="utf-8") - - -def react_native_compatibility_versions() -> tuple[str, str]: - package = json.loads(read_text("src/sdks/react-native/package.json")) - metadata = package.get("oliphaunt") - if not isinstance(metadata, dict): - fail("React Native package.json must declare oliphaunt compatibility metadata") - swift_version = metadata.get("swiftSdkVersion") - kotlin_version = metadata.get("kotlinSdkVersion") - if not isinstance(swift_version, str) or not isinstance(kotlin_version, str): - fail("React Native compatibility metadata must include Swift and Kotlin SDK versions") - return swift_version, kotlin_version - - -def typescript_compatibility_versions() -> tuple[str, str, str]: - package = json.loads(read_text("src/sdks/js/package.json")) - metadata = package.get("oliphaunt") - if not isinstance(metadata, dict): - fail("TypeScript package.json must declare oliphaunt compatibility metadata") - liboliphaunt_version = metadata.get("liboliphauntVersion") - broker_version = metadata.get("brokerVersion") - node_direct_version = metadata.get("nodeDirectAddonVersion") - if ( - not isinstance(liboliphaunt_version, str) - or not isinstance(broker_version, str) - or not isinstance(node_direct_version, str) - ): - fail("TypeScript compatibility metadata must include liboliphaunt, broker, and Node direct versions") - return liboliphaunt_version, broker_version, node_direct_version - - -def dependency_version_for(consumer: str, dependency: str) -> str: - if consumer == "oliphaunt-swift" and dependency == "liboliphaunt-native": - return read_text("src/sdks/swift/LIBOLIPHAUNT_VERSION").strip() - if consumer == "oliphaunt-react-native" and dependency == "oliphaunt-swift": - swift_version, _ = react_native_compatibility_versions() - return swift_version - if consumer == "oliphaunt-react-native" and dependency == "oliphaunt-kotlin": - _, kotlin_version = react_native_compatibility_versions() - return kotlin_version - if consumer == "oliphaunt-js" and dependency == "liboliphaunt-native": - liboliphaunt_version, _, _ = typescript_compatibility_versions() - return liboliphaunt_version - if consumer == "oliphaunt-js" and dependency == "oliphaunt-broker": - _, broker_version, _ = typescript_compatibility_versions() - return broker_version - if consumer == "oliphaunt-js" and dependency == "oliphaunt-node-direct": - _, _, node_direct_version = typescript_compatibility_versions() - return node_direct_version - return product_metadata.read_current_version(dependency) - - -def validate_product(product: str, config: dict, head_ref: str) -> bool: - prefix = config.get("tag_prefix") - if not isinstance(prefix, str) or not prefix: - fail(f"{product} must declare tag_prefix") - version = product_metadata.read_current_version(product) - current = parse_stable_version(version) - current_tag = f"{prefix}{version}" - head_commit = commit_for_ref(head_ref) - tags = product_tags(prefix) - if current_tag in tags: - current_tag_commit = tag_commit(current_tag) - if current_tag_commit != head_commit: - fail( - f"{product} version {version} is already tagged as {current_tag} " - f"at {current_tag_commit}, not release commit {head_commit}; " - "merge the release-please release PR before publishing" - ) - return True - previous_versions = [ - parsed - for candidate_prefix in tag_prefixes(config) - for tag in product_tags(candidate_prefix) - if (parsed := tag_version(candidate_prefix, tag)) is not None - ] - if previous_versions and current <= max(previous_versions): - latest = ".".join(str(part) for part in max(previous_versions)) - fail( - f"{product} version {version} is not newer than latest tagged version {latest}; " - "merge the release-please release PR before publishing" - ) - return False - - -def validate_registry_publication( - products: list[str], - graph: dict, - current_tag_at_head: dict[str, bool], - head_ref: str, -) -> None: - graph_products = graph.get("products") - if not isinstance(graph_products, dict): - fail("release metadata must define [products.] entries") - head_commit = commit_for_ref(head_ref) - for product in products: - config = graph_products[product] - targets = config.get("publish_targets", []) - if not isinstance(targets, list) or not all(isinstance(item, str) for item in targets): - fail(f"{product}.publish_targets must be a string list") - registry_targets = set(targets) & REGISTRY_TARGETS - if not registry_targets: - continue - if current_tag_at_head.get(product, False): - if "crates-io" in registry_targets: - registry_assert_product_publication( - product, - require_published=True, - ) - else: - registry_report_product_publication(product) - continue - packages, _, published = registry_query_product_publication(product) - if not packages: - print(f"{product} has no external registry packages to check") - continue - if published: - prefix = config.get("tag_prefix") - if not isinstance(prefix, str) or not prefix: - fail(f"{product} must declare tag_prefix") - version = product_metadata.read_current_version(product) - current_tag = f"{prefix}{version}" - fail( - f"{product} version {version} is already published in public registries: " - + ", ".join(str(package["label"]) for package in published) - + f"; the matching product tag {current_tag} is missing or does not " - f"point at release commit {head_commit}. If this was an intentional " - "first package identity bootstrap, create and push that product tag at " - "the same release commit, then rerun the release workflow as a completion " - "run. Otherwise merge the release-please release PR before publishing." - ) - print( - f"{product} registry unpublished check passed: " - + ", ".join(str(package["label"]) for package in packages) - ) - - -def validate_dependency_tag( - consumer: str, - dependency: str, - dependency_version: str, - graph: dict, - selected: set[str], -) -> None: - parse_stable_version(dependency_version) - if dependency in selected: - return - dependency_config = graph["products"].get(dependency) - if not isinstance(dependency_config, dict): - fail(f"{consumer} declares unknown release dependency {dependency}") - prefix = dependency_config.get("tag_prefix") - if not isinstance(prefix, str) or not prefix: - fail(f"{dependency} must declare tag_prefix") - tag = f"{prefix}{dependency_version}" - if not tag_exists(tag): - fail( - f"{consumer} depends on {dependency} {dependency_version}, but release tag " - f"{tag} does not exist and {dependency} is not selected for this release" - ) - validate_released_dependency_artifacts(consumer, dependency, dependency_version, graph) - - -def validate_released_dependency_artifacts( - consumer: str, - dependency: str, - dependency_version: str, - graph: dict, -) -> None: - dependency_config = graph["products"].get(dependency) - if not isinstance(dependency_config, dict): - fail(f"{consumer} declares unknown release dependency {dependency}") - targets = dependency_config.get("publish_targets", []) - if not isinstance(targets, list) or not all(isinstance(item, str) for item in targets): - fail(f"{dependency}.publish_targets must be a string list") - registry_targets = set(targets) & REGISTRY_TARGETS - if registry_targets: - registry_assert_product_publication( - dependency, - require_published=True, - version_override=dependency_version, - ) - if "github-release-assets" in targets: - verify_github_release_assets(dependency, dependency_version) - - -def validate_release_dependencies(products: list[str], graph: dict) -> None: - selected = set(products) - graph_products = graph.get("products") - if not isinstance(graph_products, dict): - fail("release metadata must define [products.] entries") - moon_projects = graph.get("moon_projects") - if not isinstance(moon_projects, dict): - fail("Moon project graph is missing from release metadata") - product_project = { - product: release_plan.release_product_project_id(product, graph_products, moon_projects) - for product in graph_products - } - project_product = {project: product for product, project in product_project.items()} - for product in products: - config = graph_products.get(product) - if not isinstance(config, dict): - fail(f"selected product {product} is missing from release metadata") - project = moon_projects.get(product_project[product], {}) - dependencies = [ - project_product[dependency] - for dependency in project.get("dependsOn", []) - if dependency in project_product - ] - for dependency in dependencies: - validate_dependency_tag( - product, - dependency, - dependency_version_for(product, dependency), - graph, - selected, - ) - - -def parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--products-json", help="JSON list of selected product ids") - parser.add_argument( - "--head-ref", - default="HEAD", - help="release commit ref; an existing current-version tag is allowed only if it points here", - ) - parser.add_argument( - "--check-registries", - action="store_true", - help="also validate selected product versions against external package registries", - ) - return parser.parse_args(argv) - - -def main(argv: list[str]) -> int: - args = parse_args(argv) - graph = load_graph() - selected = parse_products(args.products_json, graph) - current_tag_at_head: dict[str, bool] = {} - for product in selected: - current_tag_at_head[product] = validate_product( - product, - graph["products"][product], - args.head_ref, - ) - validate_release_dependencies(selected, graph) - if args.check_registries: - validate_registry_publication(selected, graph, current_tag_at_head, args.head_ref) - print("release version checks passed") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) diff --git a/tools/release/release.py b/tools/release/release.py index ec88155d..f7168ed3 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -1785,7 +1785,7 @@ def command_check_registries(args: list[str]) -> None: if not args: print("No release products selected; registry publication checks skipped.") return - run(["tools/release/check_release_versions.py", *args, "--check-registries"]) + run(["tools/dev/bun.sh", "tools/release/check_release_versions.mjs", *args, "--check-registries"]) if require_identities: products_json = passthrough_value(args, "--products-json") if products_json is None: @@ -1861,7 +1861,7 @@ def consumer_shape_scope_args(args: list[str]) -> list[str]: def command_verify_release(args: list[str]) -> None: - run(["tools/release/check_release_versions.py", *args, "--check-registries"]) + run(["tools/dev/bun.sh", "tools/release/check_release_versions.mjs", *args, "--check-registries"]) command_consumer_shape(["--require-ready", *consumer_shape_scope_args(args)]) run(["tools/dev/bun.sh", "tools/release/verify_github_release_attestations.mjs", *args]) @@ -3098,7 +3098,8 @@ def publish_typescript_npm_jsr(head_ref: str) -> None: verify_release_tag("oliphaunt-js", head_ref) run( [ - "tools/release/check_release_versions.py", + "tools/dev/bun.sh", + "tools/release/check_release_versions.mjs", "--products-json", '["oliphaunt-js"]', "--head-ref", From 941bbebb51397a80219cc46976c0b741475d7b8a Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 00:10:24 +0000 Subject: [PATCH 134/137] chore: share bun release graph planner --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 9 + tools/release/check_release_versions.mjs | 306 +------- tools/release/release-graph.mjs | 666 ++++++++++++++++++ tools/release/release.py | 7 +- tools/release/release_plan.mjs | 158 +++++ 5 files changed, 858 insertions(+), 288 deletions(-) create mode 100644 tools/release/release-graph.mjs create mode 100644 tools/release/release_plan.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index e16cb5ea..9bc27c87 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -1026,6 +1026,15 @@ until the current-state gates here are checked with fresh local evidence. empty selection, `oliphaunt-swift` plus `liboliphaunt-native`, the JS/native dependency closure, and the React Native/Swift/Kotlin/native dependency closure. +- On 2026-06-26, public release planning moved onto shared Bun graph tooling. + `release-graph.mjs` owns release-please/Moon graph loading, release ordering, + path affectedness, and product-tag planning for Bun release helpers. + `release_plan.mjs` now backs `tools/release/release.py plan`; parity checks + matched the old Python planner for docs-only changed-file JSON, release-tool + changed-file JSON, and the release workflow + `--from-product-tags --include-current-tags --format github-output` mode. + The old Python `release_plan.py` remains as an internal module for the + still-Python graph and release-policy checkers until that cluster is ported. - On 2026-06-26, native runtime payload optimization moved from Python to Bun. `optimize_native_runtime_payload.mjs` now owns pruning, stripping, and validation for root runtime payloads and split `oliphaunt-tools` payloads, diff --git a/tools/release/check_release_versions.mjs b/tools/release/check_release_versions.mjs index 42766173..457338d0 100644 --- a/tools/release/check_release_versions.mjs +++ b/tools/release/check_release_versions.mjs @@ -1,10 +1,20 @@ #!/usr/bin/env bun import { execFileSync, spawnSync } from "node:child_process"; -import { existsSync, readFileSync } from "node:fs"; -import path from "node:path"; +import { readFileSync } from "node:fs"; import { currentVersion } from "./product-version.mjs"; +import { + ROOT, + assertStringList as graphAssertStringList, + commandJson, + compareVersion, + formatVersion, + loadGraph, + parseStableVersion as graphParseStableVersion, + releaseProductProjectId as graphReleaseProductProjectId, + tagMatchPattern, + tagPrefixes as graphTagPrefixes, +} from "./release-graph.mjs"; -const ROOT = path.resolve(import.meta.dir, "../.."); const TOOL = "check_release_versions.mjs"; const REGISTRY_TARGETS = new Set(["crates-io", "npm", "jsr", "maven-central"]); @@ -13,41 +23,8 @@ function fail(message) { process.exit(1); } -function rel(file) { - const relative = path.relative(ROOT, file); - return relative.startsWith("..") ? file : relative.split(path.sep).join("/"); -} - function readText(relativePath) { - return readFileSync(path.join(ROOT, relativePath), "utf8"); -} - -function readJson(relativePath) { - const value = JSON.parse(readText(relativePath)); - if (value === null || Array.isArray(value) || typeof value !== "object") { - fail(`${relativePath} must contain a JSON object`); - } - return value; -} - -function readToml(relativePath) { - const file = path.join(ROOT, relativePath); - if (!existsSync(file)) { - fail(`missing ${relativePath}`); - } - const value = Bun.TOML.parse(readFileSync(file, "utf8")); - if (value === null || Array.isArray(value) || typeof value !== "object") { - fail(`${relativePath} must contain a TOML table`); - } - return value; -} - -function moonBin() { - if (process.env.MOON_BIN) { - return process.env.MOON_BIN; - } - const protoMoon = path.join(process.env.HOME ?? "", ".proto/bin/moon"); - return existsSync(protoMoon) ? protoMoon : "moon"; + return readFileSync(`${ROOT}/${relativePath}`, "utf8"); } function gitOutput(args) { @@ -64,235 +41,12 @@ function run(args) { } } -function commandJson(args) { - const output = execFileSync(args[0], args.slice(1), { - cwd: ROOT, - encoding: "utf8", - maxBuffer: 100 * 1024 * 1024, - }); - const value = JSON.parse(output); - if (value === null || Array.isArray(value) || typeof value !== "object") { - fail(`${args[0]} did not return a JSON object`); - } - return value; -} - function parseStableVersion(version) { - const match = /^([0-9]+)[.]([0-9]+)[.]([0-9]+)$/.exec(version); - if (!match) { - fail(`release version must be stable x.y.z for automated publish, got ${JSON.stringify(version)}`); - } - return match.slice(1).map((part) => Number.parseInt(part, 10)); -} - -function compareVersion(left, right) { - for (let index = 0; index < 3; index += 1) { - if (left[index] !== right[index]) { - return left[index] - right[index]; - } - } - return 0; -} - -function formatVersion(version) { - return version.join("."); + return graphParseStableVersion(version, TOOL); } function assertStringList(value, context) { - if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) { - fail(`${context} must be a string list`); - } - return value; -} - -function releasePleasePackagesByComponent() { - const config = readJson("release-please-config.json"); - const packages = config.packages; - if (packages === null || Array.isArray(packages) || typeof packages !== "object") { - fail("release-please-config.json must define packages"); - } - const byComponent = new Map(); - for (const [packagePath, packageConfig] of Object.entries(packages)) { - if (packageConfig === null || Array.isArray(packageConfig) || typeof packageConfig !== "object") { - fail(`${packagePath} release-please config must be an object`); - } - const component = packageConfig.component; - if (typeof component !== "string" || component.length === 0) { - fail(`${packagePath}.component must be a non-empty string`); - } - if (byComponent.has(component)) { - fail(`duplicate release-please component ${component}`); - } - byComponent.set(component, { packagePath, packageConfig }); - } - return { config, byComponent }; -} - -function moonProjectsById() { - const data = commandJson([moonBin(), "query", "projects"]); - const projects = data.projects; - if (!Array.isArray(projects)) { - fail("moon query projects did not return a projects array"); - } - const parsed = new Map(); - for (const project of projects) { - if (project === null || Array.isArray(project) || typeof project !== "object" || typeof project.id !== "string") { - continue; - } - const config = project.config && typeof project.config === "object" && !Array.isArray(project.config) ? project.config : {}; - const rawDeps = project.dependencies ?? config.dependsOn ?? []; - const dependencyScopes = {}; - if (Array.isArray(rawDeps)) { - for (const dependency of rawDeps) { - if (typeof dependency === "string") { - dependencyScopes[dependency] = "production"; - } else if ( - dependency !== null && - typeof dependency === "object" && - !Array.isArray(dependency) && - typeof dependency.id === "string" - ) { - dependencyScopes[dependency.id] = String(dependency.scope || "production"); - } - } - } - parsed.set(project.id, { - id: project.id, - source: project.source || config.source || "", - dependsOn: Object.keys(dependencyScopes).sort(), - dependencyScopes, - tags: Array.isArray(config.tags) ? [...config.tags].sort() : [], - project: config.project && typeof config.project === "object" && !Array.isArray(config.project) ? config.project : {}, - }); - } - return parsed; -} - -function moonReleaseProjectsByComponent(projects) { - const products = new Map(); - for (const project of projects.values()) { - const metadata = - project.project && - typeof project.project === "object" && - !Array.isArray(project.project) && - project.project.metadata && - typeof project.project.metadata === "object" && - !Array.isArray(project.project.metadata) - ? project.project.metadata - : {}; - const release = - metadata.release && typeof metadata.release === "object" && !Array.isArray(metadata.release) - ? metadata.release - : undefined; - if (!project.tags.includes("release-product")) { - if (release !== undefined) { - fail(`Moon project ${project.id} declares release metadata but is not tagged release-product`); - } - continue; - } - if (release === undefined) { - fail(`Moon release product ${project.id} must declare project.metadata.release`); - } - if (release.component !== project.id) { - fail(`Moon release product ${project.id} release.component must match the project id`); - } - if (typeof release.packagePath !== "string" || release.packagePath.length === 0) { - fail(`Moon release product ${project.id} must declare release.packagePath`); - } - if (products.has(release.component)) { - fail(`duplicate Moon release component ${release.component}`); - } - products.set(release.component, { - projectId: project.id, - projectSource: project.source, - path: release.packagePath, - release, - }); - } - if (products.size === 0) { - fail("Moon project graph does not contain any release-product projects"); - } - return products; -} - -function releasePackagePaths(projects) { - const { byComponent } = releasePleasePackagesByComponent(); - const moonProducts = moonReleaseProjectsByComponent(projects); - const moonComponents = [...moonProducts.keys()].sort(); - const releaseComponents = [...byComponent.keys()].sort(); - if (JSON.stringify(moonComponents) !== JSON.stringify(releaseComponents)) { - fail( - `Moon release-product components must match release-please components: moon=${JSON.stringify( - moonComponents, - )}, release-please=${JSON.stringify(releaseComponents)}`, - ); - } - const paths = new Map(); - for (const component of moonComponents) { - const moonPath = moonProducts.get(component).path; - const releasePath = byComponent.get(component).packagePath; - if (moonPath !== releasePath) { - fail( - `${component} Moon release.packagePath ${JSON.stringify(moonPath)} must match release-please package path ${JSON.stringify( - releasePath, - )}`, - ); - } - paths.set(component, moonPath); - } - return paths; -} - -function tagPrefix(product) { - const { config, byComponent } = releasePleasePackagesByComponent(); - const packageConfig = byComponent.get(product)?.packageConfig; - if (!packageConfig) { - fail(`unknown release-please component ${product}`); - } - if (packageConfig.component !== product) { - fail(`${product} release-please component must match product id`); - } - if (config["include-v-in-tag"] !== true) { - fail("release-please must include v in product tags"); - } - if (config["tag-separator"] !== "-") { - fail("release-please tag-separator must be '-'"); - } - return `${product}-v`; -} - -function graphProducts(projects) { - const paths = releasePackagePaths(projects); - const manifest = readJson(".release-please-manifest.json"); - const products = {}; - for (const [product, packagePath] of [...paths.entries()].sort(([left], [right]) => left.localeCompare(right))) { - const metadata = readToml(path.join(packagePath, "release.toml")); - if (metadata.id !== product) { - fail(`${packagePath}/release.toml must declare id = ${JSON.stringify(product)}`); - } - if (!(packagePath in manifest)) { - fail(`.release-please-manifest.json is missing ${packagePath}`); - } - products[product] = { - ...metadata, - path: packagePath, - tag_prefix: tagPrefix(product), - }; - } - return products; -} - -function loadGraph() { - const moonProjects = moonProjectsById(); - return { - policy: { - repository: "f0rr0/oliphaunt", - default_branch: "main", - versioning: "independent", - }, - products: graphProducts(moonProjects), - moon_projects: Object.fromEntries(moonProjects), - }; + return graphAssertStringList(value, context, TOOL); } function parseProducts(raw, graph) { @@ -323,7 +77,7 @@ function registryRun(args) { } function registryJson(args) { - return commandJson(registryCommand(args)); + return commandJson(registryCommand(args), TOOL); } function registryAssertProductPublication(product, { requirePublished, versionOverride } = {}) { @@ -357,17 +111,8 @@ function verifyGithubReleaseAssets(product, version) { ]); } -function tagMatchPattern(prefix) { - return prefix ? `${prefix}[0-9]*` : "[0-9]*"; -} - function tagPrefixes(config) { - if (typeof config.tag_prefix !== "string" || config.tag_prefix.length === 0) { - fail("release products must declare tag_prefix"); - } - const legacyPrefixes = config.legacy_tag_prefixes ?? []; - assertStringList(legacyPrefixes, "legacy_tag_prefixes"); - return [config.tag_prefix, ...legacyPrefixes]; + return graphTagPrefixes(config, TOOL); } function productTags(prefix) { @@ -544,20 +289,7 @@ async function validateRegistryPublication(products, graph, currentTagAtHead, he } function releaseProductProjectId(product, products, projects) { - if (product in projects) { - return product; - } - const packagePath = products[product]?.path; - if (typeof packagePath !== "string" || packagePath.length === 0) { - fail(`release product ${product} is missing package path metadata`); - } - const matches = Object.values(projects) - .filter((project) => packagePath === project.source || packagePath.startsWith(`${project.source}/`)) - .sort((left, right) => right.source.length - left.source.length); - if (matches.length === 0) { - fail(`release product ${product} has no owning Moon project for ${packagePath}`); - } - return matches[0].id; + return graphReleaseProductProjectId(product, products, projects, TOOL); } function validateReleasedDependencyArtifacts(consumer, dependency, dependencyVersion, graph) { diff --git a/tools/release/release-graph.mjs b/tools/release/release-graph.mjs new file mode 100644 index 00000000..28de1323 --- /dev/null +++ b/tools/release/release-graph.mjs @@ -0,0 +1,666 @@ +import { execFileSync, spawnSync } from "node:child_process"; +import { existsSync, readFileSync } from "node:fs"; +import path from "node:path"; +import crypto from "node:crypto"; + +export const ROOT = path.resolve(import.meta.dir, "../.."); +export const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"; +export const RELEASE_DEPENDENCY_SCOPES = new Set(["production", "peer"]); + +const GENERATED_PATH_PARTS = new Set([ + ".build", + ".cxx", + ".expo", + ".gradle", + ".kotlin", + ".moon", + ".next", + ".source", + "DerivedData", + "Pods", + "__pycache__", + "dist", + "lib", + "node_modules", + "out", + "target", +]); + +export function fail(prefix, message) { + console.error(`${prefix}: ${message}`); + process.exit(1); +} + +export function rel(file) { + const relative = path.relative(ROOT, file); + return relative.startsWith("..") ? file : relative.split(path.sep).join("/"); +} + +export function compareText(left, right) { + return left < right ? -1 : left > right ? 1 : 0; +} + +export function readJson(relativePath, prefix) { + const value = JSON.parse(readFileSync(path.join(ROOT, relativePath), "utf8")); + if (value === null || Array.isArray(value) || typeof value !== "object") { + fail(prefix, `${relativePath} must contain a JSON object`); + } + return value; +} + +export function readToml(relativePath, prefix) { + const file = path.join(ROOT, relativePath); + if (!existsSync(file)) { + fail(prefix, `missing ${relativePath}`); + } + const value = Bun.TOML.parse(readFileSync(file, "utf8")); + if (value === null || Array.isArray(value) || typeof value !== "object") { + fail(prefix, `${relativePath} must contain a TOML table`); + } + return value; +} + +export function moonBin() { + if (process.env.MOON_BIN) { + return process.env.MOON_BIN; + } + const protoMoon = path.join(process.env.HOME ?? "", ".proto/bin/moon"); + return existsSync(protoMoon) ? protoMoon : "moon"; +} + +export function commandJson(args, prefix) { + const output = execFileSync(args[0], args.slice(1), { + cwd: ROOT, + encoding: "utf8", + maxBuffer: 100 * 1024 * 1024, + }); + const value = JSON.parse(output); + if (value === null || Array.isArray(value) || typeof value !== "object") { + fail(prefix, `${args[0]} did not return a JSON object`); + } + return value; +} + +export function gitOutput(args) { + return execFileSync("git", args, { cwd: ROOT, encoding: "utf8" }).trim(); +} + +export function runGit(args) { + return execFileSync("git", args, { cwd: ROOT, encoding: "utf8" }); +} + +export function parseStableVersion(version, prefix = "release-graph") { + const match = /^([0-9]+)[.]([0-9]+)[.]([0-9]+)$/.exec(version); + if (!match) { + fail(prefix, `release version must be stable x.y.z for automated publish, got ${JSON.stringify(version)}`); + } + return match.slice(1).map((part) => Number.parseInt(part, 10)); +} + +export function compareVersion(left, right) { + for (let index = 0; index < 3; index += 1) { + if (left[index] !== right[index]) { + return left[index] - right[index]; + } + } + return 0; +} + +export function formatVersion(version) { + return version.join("."); +} + +export function assertStringList(value, context, prefix = "release-graph") { + if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) { + fail(prefix, `${context} must be a string list`); + } + return value; +} + +function releasePleasePackagesByComponent(prefix) { + const config = readJson("release-please-config.json", prefix); + const packages = config.packages; + if (packages === null || Array.isArray(packages) || typeof packages !== "object") { + fail(prefix, "release-please-config.json must define packages"); + } + const byComponent = new Map(); + for (const [packagePath, packageConfig] of Object.entries(packages)) { + if (packageConfig === null || Array.isArray(packageConfig) || typeof packageConfig !== "object") { + fail(prefix, `${packagePath} release-please config must be an object`); + } + const component = packageConfig.component; + if (typeof component !== "string" || component.length === 0) { + fail(prefix, `${packagePath}.component must be a non-empty string`); + } + if (byComponent.has(component)) { + fail(prefix, `duplicate release-please component ${component}`); + } + byComponent.set(component, { packagePath, packageConfig }); + } + return { config, byComponent }; +} + +export function moonProjectsById(prefix = "release-graph") { + const data = commandJson([moonBin(), "query", "projects"], prefix); + const projects = data.projects; + if (!Array.isArray(projects)) { + fail(prefix, "moon query projects did not return a projects array"); + } + const parsed = new Map(); + for (const project of projects) { + if (project === null || Array.isArray(project) || typeof project !== "object" || typeof project.id !== "string") { + continue; + } + const config = project.config && typeof project.config === "object" && !Array.isArray(project.config) ? project.config : {}; + const rawDeps = project.dependencies ?? config.dependsOn ?? []; + const dependencyScopes = {}; + if (Array.isArray(rawDeps)) { + for (const dependency of rawDeps) { + if (typeof dependency === "string") { + dependencyScopes[dependency] = "production"; + } else if ( + dependency !== null && + typeof dependency === "object" && + !Array.isArray(dependency) && + typeof dependency.id === "string" + ) { + dependencyScopes[dependency.id] = String(dependency.scope || "production"); + } + } + } + parsed.set(project.id, { + id: project.id, + source: project.source || config.source || "", + dependsOn: Object.keys(dependencyScopes).sort(compareText), + dependencyScopes, + tags: Array.isArray(config.tags) ? [...config.tags].sort(compareText) : [], + project: config.project && typeof config.project === "object" && !Array.isArray(config.project) ? config.project : {}, + }); + } + return parsed; +} + +function moonReleaseProjectsByComponent(projects, prefix) { + const products = new Map(); + for (const project of projects.values()) { + const metadata = + project.project && + typeof project.project === "object" && + !Array.isArray(project.project) && + project.project.metadata && + typeof project.project.metadata === "object" && + !Array.isArray(project.project.metadata) + ? project.project.metadata + : {}; + const release = + metadata.release && typeof metadata.release === "object" && !Array.isArray(metadata.release) + ? metadata.release + : undefined; + if (!project.tags.includes("release-product")) { + if (release !== undefined) { + fail(prefix, `Moon project ${project.id} declares release metadata but is not tagged release-product`); + } + continue; + } + if (release === undefined) { + fail(prefix, `Moon release product ${project.id} must declare project.metadata.release`); + } + if (release.component !== project.id) { + fail(prefix, `Moon release product ${project.id} release.component must match the project id`); + } + if (typeof release.packagePath !== "string" || release.packagePath.length === 0) { + fail(prefix, `Moon release product ${project.id} must declare release.packagePath`); + } + if (products.has(release.component)) { + fail(prefix, `duplicate Moon release component ${release.component}`); + } + products.set(release.component, { + projectId: project.id, + projectSource: project.source, + path: release.packagePath, + release, + }); + } + if (products.size === 0) { + fail(prefix, "Moon project graph does not contain any release-product projects"); + } + return products; +} + +function releasePackagePaths(projects, prefix) { + const { byComponent } = releasePleasePackagesByComponent(prefix); + const moonProducts = moonReleaseProjectsByComponent(projects, prefix); + const moonComponents = [...moonProducts.keys()].sort(compareText); + const releaseComponents = [...byComponent.keys()].sort(compareText); + if (JSON.stringify(moonComponents) !== JSON.stringify(releaseComponents)) { + fail( + prefix, + `Moon release-product components must match release-please components: moon=${JSON.stringify( + moonComponents, + )}, release-please=${JSON.stringify(releaseComponents)}`, + ); + } + const paths = new Map(); + for (const component of moonComponents) { + const moonPath = moonProducts.get(component).path; + const releasePath = byComponent.get(component).packagePath; + if (moonPath !== releasePath) { + fail( + prefix, + `${component} Moon release.packagePath ${JSON.stringify(moonPath)} must match release-please package path ${JSON.stringify( + releasePath, + )}`, + ); + } + paths.set(component, moonPath); + } + return paths; +} + +export function tagPrefix(product, prefix = "release-graph") { + const { config, byComponent } = releasePleasePackagesByComponent(prefix); + const packageConfig = byComponent.get(product)?.packageConfig; + if (!packageConfig) { + fail(prefix, `unknown release-please component ${product}`); + } + if (packageConfig.component !== product) { + fail(prefix, `${product} release-please component must match product id`); + } + if (config["include-v-in-tag"] !== true) { + fail(prefix, "release-please must include v in product tags"); + } + if (config["tag-separator"] !== "-") { + fail(prefix, "release-please tag-separator must be '-'"); + } + return `${product}-v`; +} + +function graphProducts(projects, prefix) { + const paths = releasePackagePaths(projects, prefix); + const manifest = readJson(".release-please-manifest.json", prefix); + const products = {}; + for (const [product, packagePath] of [...paths.entries()].sort(([left], [right]) => compareText(left, right))) { + const metadata = readToml(path.join(packagePath, "release.toml"), prefix); + if (metadata.id !== product) { + fail(prefix, `${packagePath}/release.toml must declare id = ${JSON.stringify(product)}`); + } + if (!(packagePath in manifest)) { + fail(prefix, `.release-please-manifest.json is missing ${packagePath}`); + } + products[product] = { + ...metadata, + path: packagePath, + tag_prefix: tagPrefix(product, prefix), + }; + } + return products; +} + +export function loadGraph(prefix = "release-graph") { + const moonProjects = moonProjectsById(prefix); + return { + policy: { + repository: "f0rr0/oliphaunt", + default_branch: "main", + versioning: "independent", + }, + products: graphProducts(moonProjects, prefix), + moon_projects: Object.fromEntries(moonProjects), + }; +} + +export function tagMatchPattern(prefix) { + return prefix ? `${prefix}[0-9]*` : "[0-9]*"; +} + +export function tagPrefixes(config, prefix = "release-graph") { + if (typeof config.tag_prefix !== "string" || config.tag_prefix.length === 0) { + fail(prefix, "release products must declare tag_prefix"); + } + const legacyPrefixes = config.legacy_tag_prefixes ?? []; + assertStringList(legacyPrefixes, "legacy_tag_prefixes", prefix); + return [config.tag_prefix, ...legacyPrefixes]; +} + +export function latestTagForPrefix(prefix, headRef) { + const result = spawnSync("git", ["describe", "--tags", "--abbrev=0", "--match", tagMatchPattern(prefix), headRef], { + cwd: ROOT, + encoding: "utf8", + }); + return result.status === 0 ? result.stdout.trim() : ""; +} + +export function latestProductTag(productConfig, headRef, prefix = "release-graph") { + for (const candidatePrefix of tagPrefixes(productConfig, prefix)) { + const tag = latestTagForPrefix(candidatePrefix, headRef); + if (tag) { + return tag; + } + } + return EMPTY_TREE; +} + +export function commitForRef(ref) { + return gitOutput(["rev-parse", `${ref}^{commit}`]); +} + +export function changedFilesFromRefs(baseRef, headRef, prefix = "release-graph") { + try { + const output = + baseRef === EMPTY_TREE + ? runGit(["diff", "--name-only", baseRef, headRef, "--"]) + : runGit(["diff", "--name-only", `${baseRef}...${headRef}`, "--"]); + return output.split(/\r?\n/).filter(Boolean).sort(compareText); + } catch (error) { + fail(prefix, `failed to read changed files between ${baseRef} and ${headRef}: ${error.message}`); + } +} + +export function isGeneratedLocalState(candidate) { + if (candidate.startsWith("target/")) { + return true; + } + return candidate.split(/[\\/]/).some((part) => GENERATED_PATH_PARTS.has(part)); +} + +export function normalizeFiles(files) { + const normalized = new Set(); + for (const file of files) { + let candidate = file.trim().replaceAll("\\", "/"); + if (candidate.startsWith("./")) { + candidate = candidate.slice(2); + } + if (candidate && !isGeneratedLocalState(candidate)) { + normalized.add(candidate); + } + } + return [...normalized].sort(compareText); +} + +function splitPatterns(patterns) { + const includes = []; + const excludes = []; + for (const pattern of patterns) { + if (pattern.startsWith("!")) { + excludes.push(pattern.slice(1)); + } else { + includes.push(pattern); + } + } + return { includes, excludes }; +} + +function globPatternToRegExp(pattern) { + let text = ""; + for (const char of pattern) { + if (char === "*") { + text += ".*"; + } else if ("\\^$+?.()|{}[]".includes(char)) { + text += `\\${char}`; + } else { + text += char; + } + } + return new RegExp(`^${text}$`, "u"); +} + +function matchesAny(candidate, patterns) { + return patterns.some((pattern) => globPatternToRegExp(pattern).test(candidate)); +} + +export function productMatches(candidate, patterns) { + const { includes, excludes } = splitPatterns(patterns); + return matchesAny(candidate, includes) && !matchesAny(candidate, excludes); +} + +export function ownerProjectForPath(projects, candidate) { + if (isGeneratedLocalState(candidate)) { + return undefined; + } + const matches = Object.values(projects) + .filter( + (project) => + project.source === "." || candidate === project.source || candidate.startsWith(`${project.source}/`), + ) + .sort((left, right) => right.source.length - left.source.length); + return matches[0]?.id; +} + +export function dependentsByProject(projects, { releaseOnly = false } = {}) { + const dependents = Object.fromEntries(Object.keys(projects).map((project) => [project, new Set()])); + for (const [project, config] of Object.entries(projects)) { + const scopes = config.dependencyScopes ?? {}; + for (const dependency of config.dependsOn ?? []) { + if (releaseOnly && !RELEASE_DEPENDENCY_SCOPES.has(scopes[dependency] ?? "production")) { + continue; + } + if (!(dependency in dependents)) { + dependents[dependency] = new Set(); + } + dependents[dependency].add(project); + } + } + return dependents; +} + +export function downstreamProjects(projects, direct, { releaseOnly = false } = {}) { + const dependents = dependentsByProject(projects, { releaseOnly }); + const selected = new Set(direct); + const queue = [...selected].sort(compareText); + while (queue.length > 0) { + const current = queue.shift(); + for (const downstream of [...(dependents[current] ?? [])].sort(compareText)) { + if (!selected.has(downstream)) { + selected.add(downstream); + queue.push(downstream); + } + } + } + return selected; +} + +export function releaseProductProjectId(product, products, projects, prefix = "release-graph") { + if (product in projects) { + return product; + } + const packagePath = products[product]?.path; + if (typeof packagePath !== "string" || packagePath.length === 0) { + fail(prefix, `release product ${product} is missing package path metadata`); + } + const matches = Object.values(projects) + .filter((project) => packagePath === project.source || packagePath.startsWith(`${project.source}/`)) + .sort((left, right) => right.source.length - left.source.length); + if (matches.length === 0) { + fail(prefix, `release product ${product} has no owning Moon project for ${packagePath}`); + } + return matches[0].id; +} + +export function releaseProductsForProjects(products, projects, projectIds, prefix = "release-graph") { + const selectedProjects = new Set(projectIds); + const selected = new Set(); + for (const product of Object.keys(products)) { + const projectId = releaseProductProjectId(product, products, projects, prefix); + if (selectedProjects.has(projectId)) { + selected.add(product); + } + } + return selected; +} + +export function releaseOrder(products, projects, selected, prefix = "release-graph") { + const selectedSet = new Set(selected); + const productProject = Object.fromEntries( + Object.keys(products).map((product) => [product, releaseProductProjectId(product, products, projects, prefix)]), + ); + const ordered = []; + const remaining = new Set(selectedSet); + while (remaining.size > 0) { + const ready = []; + for (const product of [...remaining].sort(compareText)) { + const projectId = productProject[product]; + const projectConfig = projects[projectId] ?? {}; + const scopes = projectConfig.dependencyScopes ?? {}; + const deps = new Set( + (projectConfig.dependsOn ?? []).filter((dependency) => + RELEASE_DEPENDENCY_SCOPES.has(scopes[dependency] ?? "production"), + ), + ); + const selectedDeps = Object.entries(productProject) + .filter(([candidate, candidateProject]) => selectedSet.has(candidate) && deps.has(candidateProject)) + .map(([candidate]) => candidate); + if (selectedDeps.every((dependency) => ordered.includes(dependency))) { + ready.push(product); + } + } + if (ready.length === 0) { + fail(prefix, `Moon release product graph has a dependency cycle: ${JSON.stringify([...remaining].sort(compareText))}`); + } + for (const product of ready) { + ordered.push(product); + remaining.delete(product); + } + } + return ordered; +} + +export function docsOnlyChange(files) { + return files.length > 0 && files.every( + (file) => file.startsWith("docs/") || file.startsWith("src/docs/") || file === "README.md", + ); +} + +export function buildPlan(graph, files, prefix = "release-graph") { + const products = graph.products; + const projects = graph.moon_projects; + if (products === null || Array.isArray(products) || typeof products !== "object") { + fail(prefix, "release metadata must define [products.] entries"); + } + if (projects === null || Array.isArray(projects) || typeof projects !== "object") { + fail(prefix, "Moon project graph is missing from release plan metadata"); + } + const directProjects = new Set( + files.map((file) => ownerProjectForPath(projects, file)).filter((project) => project !== undefined), + ); + const affectedProjects = downstreamProjects(projects, directProjects); + const releaseProjects = downstreamProjects(projects, directProjects, { releaseOnly: true }); + const releaseProductSet = releaseProductsForProjects(products, projects, releaseProjects, prefix); + const releaseProducts = releaseOrder(products, projects, releaseProductSet, prefix); + const releaseProductProjects = new Set( + releaseProducts.map((product) => releaseProductProjectId(product, products, projects, prefix)), + ); + const direct = releaseOrder( + products, + projects, + releaseProductsForProjects(products, projects, directProjects, prefix), + prefix, + ); + return finalizePlan({ + changedFiles: files, + directProducts: direct, + releaseProducts, + directMoonProjects: [...directProjects].sort(compareText), + affectedMoonProjects: [...affectedProjects].sort(compareText), + releaseMoonProjects: [...releaseProductProjects].sort(compareText), + productIds: Object.keys(products), + hasReleaseChanges: releaseProducts.length > 0, + docsOnly: releaseProducts.length === 0 && docsOnlyChange(files), + versioning: graph.policy?.versioning ?? "independent", + extensionSelection: "exact-sql-extension", + }); +} + +export function buildPlanFromProductTags(graph, headRef, { includeCurrentTags = false, prefix = "release-graph" } = {}) { + const products = graph.products; + const direct = new Set(); + const changed = new Set(); + const productBaseRefs = {}; + const currentTaggedProducts = new Set(); + const headCommit = includeCurrentTags ? commitForRef(headRef) : ""; + + for (const [product, config] of Object.entries(products)) { + const baseRef = latestProductTag(config, headRef, prefix); + productBaseRefs[product] = baseRef; + if (includeCurrentTags && baseRef !== EMPTY_TREE) { + const tagCommit = commitForRef(baseRef); + if (tagCommit === headCommit) { + direct.add(product); + currentTaggedProducts.add(product); + continue; + } + } + const productFiles = changedFilesFromRefs(baseRef, headRef, prefix); + for (const file of productFiles) { + changed.add(file); + } + const productPlan = buildPlan(graph, normalizeFiles(productFiles), prefix); + if (productPlan.releaseProducts.includes(product)) { + direct.add(product); + } + } + + const projects = graph.moon_projects; + const directProjects = new Set( + [...direct].map((product) => releaseProductProjectId(product, products, projects, prefix)), + ); + const affectedProjects = downstreamProjects(projects, directProjects); + const releaseProjects = downstreamProjects(projects, directProjects, { releaseOnly: true }); + const releaseProducts = releaseOrder( + products, + projects, + releaseProductsForProjects(products, projects, releaseProjects, prefix), + prefix, + ); + return finalizePlan({ + changedFiles: [...changed].sort(compareText), + directProducts: releaseOrder(products, projects, direct, prefix), + releaseProducts, + directMoonProjects: [...directProjects].sort(compareText), + affectedMoonProjects: [...affectedProjects].sort(compareText), + releaseMoonProjects: [...releaseProjects].sort(compareText), + productIds: Object.keys(products), + hasReleaseChanges: releaseProducts.length > 0, + docsOnly: releaseProducts.length === 0 && docsOnlyChange([...changed]), + versioning: graph.policy?.versioning ?? "independent", + extensionSelection: "exact-sql-extension", + productBaseRefs, + currentTaggedProducts: [...currentTaggedProducts].sort(compareText), + }); +} + +export function releaseProductsSlug(products) { + if (products.length === 0) { + return "none"; + } + const shortNames = { + "liboliphaunt-native": "native", + }; + return products.map((product) => shortNames[product] ?? product.replace("oliphaunt-", "")).join("-"); +} + +function stableJson(value) { + if (Array.isArray(value)) { + return `[${value.map(stableJson).join(",")}]`; + } + if (value !== null && typeof value === "object") { + return `{${Object.keys(value) + .sort(compareText) + .map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`) + .join(",")}}`; + } + return JSON.stringify(value); +} + +export function finalizePlan(plan) { + const hashInput = { + changedFiles: plan.changedFiles ?? [], + directProducts: plan.directProducts ?? [], + releaseProducts: plan.releaseProducts ?? [], + productBaseRefs: plan.productBaseRefs ?? {}, + currentTaggedProducts: plan.currentTaggedProducts ?? [], + }; + const digest = crypto.createHash("sha256").update(stableJson(hashInput)).digest("hex").slice(0, 12); + plan.planHash = digest; + plan.releaseBranch = `release/${releaseProductsSlug(plan.releaseProducts ?? [])}-${digest}`; + return plan; +} diff --git a/tools/release/release.py b/tools/release/release.py index f7168ed3..b84ac071 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -1755,7 +1755,12 @@ def run_product_publish_dry_runs(products: list[str], *, allow_dirty: bool, head def command_plan(args: list[str]) -> None: - raise SystemExit(release_plan.main(args)) + result = subprocess.run( + ["tools/dev/bun.sh", "tools/release/release_plan.mjs", *args], + cwd=ROOT, + check=False, + ) + raise SystemExit(result.returncode) def command_check(args: list[str]) -> None: diff --git a/tools/release/release_plan.mjs b/tools/release/release_plan.mjs new file mode 100644 index 00000000..f34d8ae4 --- /dev/null +++ b/tools/release/release_plan.mjs @@ -0,0 +1,158 @@ +#!/usr/bin/env bun +import { + buildPlan, + buildPlanFromProductTags, + changedFilesFromRefs, + compareText, + loadGraph, + normalizeFiles, +} from "./release-graph.mjs"; + +const TOOL = "release_plan.mjs"; + +function fail(message) { + console.error(`${TOOL}: ${message}`); + process.exit(2); +} + +function sortedValue(value) { + if (Array.isArray(value)) { + return value.map(sortedValue); + } + if (value !== null && typeof value === "object") { + return Object.fromEntries( + Object.keys(value) + .sort(compareText) + .map((key) => [key, sortedValue(value[key])]), + ); + } + return value; +} + +function printJson(plan) { + console.log(JSON.stringify(sortedValue(plan), null, 2)); +} + +function printGithubOutput(plan) { + const products = plan.releaseProducts; + const extensionProducts = products.filter((product) => product.startsWith("oliphaunt-extension-")).sort(compareText); + console.log(`has_release_changes=${String(plan.hasReleaseChanges).toLowerCase()}`); + console.log(`has_extension_products=${String(extensionProducts.length > 0).toLowerCase()}`); + console.log(`docs_only=${String(plan.docsOnly).toLowerCase()}`); + console.log(`products_csv=${products.join(",")}`); + console.log(`products_json=${JSON.stringify(products)}`); + console.log(`extension_products_json=${JSON.stringify(extensionProducts)}`); + console.log(`plan_hash=${plan.planHash}`); + console.log(`release_branch=${plan.releaseBranch}`); + for (const product of plan.productIds ?? []) { + const key = `product_${product.replaceAll("-", "_")}`; + console.log(`${key}=${String(products.includes(product)).toLowerCase()}`); + } + console.log(`direct_products_json=${JSON.stringify(plan.directProducts)}`); + console.log(`product_base_refs_json=${JSON.stringify(plan.productBaseRefs ?? {})}`); +} + +function printText(plan) { + const changedFiles = plan.changedFiles ?? []; + if (changedFiles.length === 0) { + console.log("No changed files were provided; no product release is planned."); + } else if (plan.hasReleaseChanges) { + console.log(`Release products: ${plan.releaseProducts.join(", ")}`); + console.log(`Direct products: ${plan.directProducts.join(", ")}`); + } else { + console.log("No product release is planned for these changes."); + } +} + +function parseArgs(argv) { + const args = { + baseRef: undefined, + headRef: "HEAD", + fromProductTags: false, + includeCurrentTags: false, + changedFiles: [], + format: "text", + }; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === "--base-ref") { + if (index + 1 >= argv.length) { + fail("--base-ref requires a value"); + } + args.baseRef = argv[index + 1]; + index += 1; + } else if (value.startsWith("--base-ref=")) { + args.baseRef = value.slice("--base-ref=".length); + } else if (value === "--head-ref") { + if (index + 1 >= argv.length) { + fail("--head-ref requires a value"); + } + args.headRef = argv[index + 1]; + index += 1; + } else if (value.startsWith("--head-ref=")) { + args.headRef = value.slice("--head-ref=".length); + } else if (value === "--from-product-tags") { + args.fromProductTags = true; + } else if (value === "--include-current-tags") { + args.includeCurrentTags = true; + } else if (value === "--changed-file") { + if (index + 1 >= argv.length) { + fail("--changed-file requires a value"); + } + args.changedFiles.push(argv[index + 1]); + index += 1; + } else if (value.startsWith("--changed-file=")) { + args.changedFiles.push(value.slice("--changed-file=".length)); + } else if (value === "--format") { + if (index + 1 >= argv.length) { + fail("--format requires a value"); + } + args.format = argv[index + 1]; + index += 1; + } else if (value.startsWith("--format=")) { + args.format = value.slice("--format=".length); + } else if (value === "-h" || value === "--help") { + console.log("usage: tools/release/release_plan.mjs [--base-ref REF] [--head-ref REF] [--from-product-tags] [--include-current-tags] [--changed-file PATH...] [--format text|json|github-output]"); + process.exit(0); + } else { + fail(`unknown argument ${value}`); + } + } + if (!["text", "json", "github-output"].includes(args.format)) { + fail("--format must be one of: text, json, github-output"); + } + return args; +} + +function planForArgs(args) { + const graph = loadGraph(TOOL); + if (args.changedFiles.length > 0) { + return buildPlan(graph, normalizeFiles(args.changedFiles), TOOL); + } + if (args.fromProductTags) { + return buildPlanFromProductTags(graph, args.headRef, { + includeCurrentTags: args.includeCurrentTags, + prefix: TOOL, + }); + } + if (args.baseRef) { + return buildPlan(graph, normalizeFiles(changedFilesFromRefs(args.baseRef, args.headRef, TOOL)), TOOL); + } + return buildPlan(graph, [], TOOL); +} + +function main(argv) { + const args = parseArgs(argv); + const plan = planForArgs(args); + if (args.format === "json") { + printJson(plan); + } else if (args.format === "github-output") { + printGithubOutput(plan); + } else { + printText(plan); + } +} + +if (import.meta.main) { + main(Bun.argv.slice(2)); +} From a261abcb80d79018425968155a40958a3d769611 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 00:35:08 +0000 Subject: [PATCH 135/137] chore: retire python release planner --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 6 +- tools/graph/graph.py | 112 +++- tools/graph/moon.yml | 6 +- tools/policy/check-release-policy.py | 58 +- tools/policy/check-repo-structure.sh | 2 +- tools/policy/python-entrypoints.allowlist | 1 - tools/release/release-graph.mjs | 87 ++- tools/release/release.py | 20 +- tools/release/release_graph_query.mjs | 182 ++++++ tools/release/release_plan.py | 534 ------------------ 10 files changed, 431 insertions(+), 577 deletions(-) create mode 100644 tools/release/release_graph_query.mjs delete mode 100644 tools/release/release_plan.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 9bc27c87..7bf4aab8 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -1033,8 +1033,10 @@ until the current-state gates here are checked with fresh local evidence. matched the old Python planner for docs-only changed-file JSON, release-tool changed-file JSON, and the release workflow `--from-product-tags --include-current-tags --format github-output` mode. - The old Python `release_plan.py` remains as an internal module for the - still-Python graph and release-policy checkers until that cluster is ported. +- On 2026-06-27, the internal graph and release-policy checkers stopped importing + the old Python `release_plan.py`. Python callers now consume the shared Bun + graph through `release_graph_query.mjs`, leaving `release-graph.mjs` as the + single release-planning authority while those checker clusters are ported. - On 2026-06-26, native runtime payload optimization moved from Python to Bun. `optimize_native_runtime_payload.mjs` now owns pruning, stripping, and validation for root runtime payloads and split `oliphaunt-tools` payloads, diff --git a/tools/graph/graph.py b/tools/graph/graph.py index 4d445c83..21c6b3ca 100755 --- a/tools/graph/graph.py +++ b/tools/graph/graph.py @@ -6,6 +6,7 @@ import argparse import json import os +import re import subprocess import sys import tomllib @@ -39,7 +40,6 @@ sys.path.insert(0, str(ROOT / "tools" / "release")) sys.path.insert(0, str(ROOT / "tools" / "graph")) -import release_plan # noqa: E402 from ci_plan import CI_JOB_TARGETS, CI_JOBS_CONFIG, plan_jobs_for_affected # noqa: E402 @@ -72,6 +72,67 @@ def run_moon(args: list[str], *, stdin: str | None = None) -> dict[str, Any]: return json.loads(output) +def bun_json(args: list[str]) -> Any: + output = subprocess.check_output(["tools/dev/bun.sh", *args], cwd=ROOT, text=True) + return json.loads(output) + + +def release_graph() -> dict[str, Any]: + value = bun_json(["tools/release/release_graph_query.mjs", "graph"]) + if not isinstance(value, dict): + fail("release graph query did not return an object") + return value + + +def release_product_projects() -> dict[str, str]: + value = bun_json(["tools/release/release_graph_query.mjs", "product-projects"]) + if not isinstance(value, dict) or not all( + isinstance(key, str) and isinstance(item, str) for key, item in value.items() + ): + fail("release graph product-project query did not return a string map") + return value + + +def release_order(products: list[str]) -> list[str]: + value = bun_json( + [ + "tools/release/release_graph_query.mjs", + "release-order", + "--products-json", + json.dumps(products, separators=(",", ":")), + ] + ) + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + fail("release graph order query did not return a string list") + return value + + +def release_plan_for_paths(paths: list[str]) -> dict[str, Any]: + args = ["tools/release/release_graph_query.mjs", "plan"] + for path in paths: + args.extend(["--changed-file", path]) + value = bun_json(args) + if not isinstance(value, dict): + fail("release graph plan query did not return an object") + return value + + +def release_plans_for_single_paths(paths: list[str]) -> dict[str, dict[str, Any]]: + value = bun_json( + [ + "tools/release/release_graph_query.mjs", + "plans-for-paths", + "--paths-json", + json.dumps(paths, separators=(",", ":")), + ] + ) + if not isinstance(value, dict) or not all( + isinstance(key, str) and isinstance(item, dict) for key, item in value.items() + ): + fail("release graph plans-for-paths query did not return a plan map") + return value + + def affected_names(value: object) -> set[str]: if isinstance(value, dict): return {str(key) for key in value} @@ -275,7 +336,7 @@ def ci_matrix(tasks: dict[str, Any]) -> dict[str, Any]: def build_graph() -> dict[str, Any]: - release_metadata = release_plan.load_graph() + release_metadata = release_graph() coverage_baseline = read_toml(COVERAGE_BASELINE_PATH) projects = {project["id"]: normalize_project(project) for project in moon_projects()} tasks_raw = moon_tasks() @@ -285,6 +346,7 @@ def build_graph() -> dict[str, Any]: } products = release_products(release_metadata) product_ids = list(products) + product_projects = release_product_projects() dependents = dependents_by_project(projects) return { "moonProjects": projects, @@ -294,15 +356,15 @@ def build_graph() -> dict[str, Any]: product: { "owner": config.get("owner"), "kind": config.get("kind"), - "moonProject": release_plan.release_product_project_id(product, products, projects), + "moonProject": product_projects[product], "tagPrefix": config.get("tag_prefix"), "publishTargets": config.get("publish_targets", []), "releaseArtifacts": config.get("release_artifacts", []), - "moonProjectExists": release_plan.release_product_project_id(product, products, projects) in projects, + "moonProjectExists": product_projects[product] in projects, } for product, config in products.items() }, - "releaseOrder": release_plan.release_order(products, projects, product_ids), + "releaseOrder": release_order(product_ids), "coverageExpectations": coverage_expectations(coverage_baseline, tasks_raw), "ciMatrix": ci_matrix(tasks_raw), "productIds": product_ids, @@ -314,11 +376,7 @@ def explain_paths(paths: list[str], graph: dict[str, Any]) -> dict[str, Any]: projects = graph["moonProjects"] dependents = graph["moonDependents"] normalized_paths = normalize_explain_paths(paths) - release_metadata = release_plan.load_graph() - release_impact = release_plan.build_plan( - release_metadata, - release_plan.normalize_files(normalized_paths), - ) + release_impact = release_plan_for_paths(normalized_paths) explanations = [] for path in normalized_paths: owner = owner_project_for_path(projects, path) @@ -354,13 +412,25 @@ def coverage_products_for_path(path: str, graph: dict[str, Any]) -> list[str]: for product, config in graph["coverageExpectations"].items(): includes = config.get("includeGlobs", []) excludes = config.get("excludeGlobs", []) - if release_plan.product_matches(path, includes) and not release_plan.product_matches( - path, excludes - ): + if product_matches(path, includes) and not product_matches(path, excludes): products.append(product) return sorted(products) +def glob_pattern_to_regex(pattern: str) -> re.Pattern[str]: + return re.compile( + "^" + "".join(".*" if char == "*" else re.escape(char) for char in pattern) + "$" + ) + + +def product_matches(path: str, patterns: list[str]) -> bool: + includes = [pattern for pattern in patterns if not pattern.startswith("!")] + excludes = [pattern[1:] for pattern in patterns if pattern.startswith("!")] + return any(glob_pattern_to_regex(pattern).match(path) for pattern in includes) and not any( + glob_pattern_to_regex(pattern).match(path) for pattern in excludes + ) + + def write_json(path: Path, value: Any) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(f"{json.dumps(value, indent=2, sort_keys=True)}\n", encoding="utf-8") @@ -471,9 +541,10 @@ def assert_dep_cache_strategy( def check_graph(graph: dict[str, Any]) -> None: projects = graph["moonProjects"] - release_products_config = release_products(release_plan.load_graph()) + release_products_config = release_products(release_graph()) + product_projects = release_product_projects() for product, config in release_products_config.items(): - project_id = release_plan.release_product_project_id(product, release_products_config, projects) + project_id = product_projects[product] project = projects.get(project_id) if project is None: fail(f"release product {product} does not have an owning Moon project") @@ -559,10 +630,13 @@ def check_graph(graph: dict[str, Any]) -> None: path = case.get("path") if not isinstance(path, str): fail(f"synthetic release case {case_id} is missing path") - release_impact = release_plan.build_plan( - release_plan.load_graph(), - release_plan.normalize_files([path]), - ) + release_case_paths = [case.get("path") for case in release_cases.values() if isinstance(case.get("path"), str)] + release_case_plans = release_plans_for_single_paths(release_case_paths) + for case_id, case in release_cases.items(): + path = case.get("path") + if not isinstance(path, str): + fail(f"synthetic release case {case_id} is missing path") + release_impact = release_case_plans[path] planned_release_products = release_impact["releaseProducts"] assert_equal_list( f"{case_id} direct release products", diff --git a/tools/graph/moon.yml b/tools/graph/moon.yml index f1ae74d9..bed4a251 100644 --- a/tools/graph/moon.yml +++ b/tools/graph/moon.yml @@ -36,7 +36,8 @@ tasks: - "/src/**/moon.yml" - "/tools/**/moon.yml" - "/tools/graph/**/*" - - "/tools/release/release_plan.py" + - "/tools/release/release-graph.mjs" + - "/tools/release/release_graph_query.mjs" outputs: - "/target/graph/**/*" options: @@ -61,7 +62,8 @@ tasks: - "/src/**/moon.yml" - "/tools/**/moon.yml" - "/tools/graph/**/*" - - "/tools/release/release_plan.py" + - "/tools/release/release-graph.mjs" + - "/tools/release/release_graph_query.mjs" outputs: - "/target/graph/**/*" options: diff --git a/tools/policy/check-release-policy.py b/tools/policy/check-release-policy.py index f8f11a84..c6ca7618 100644 --- a/tools/policy/check-release-policy.py +++ b/tools/policy/check-release-policy.py @@ -17,7 +17,6 @@ import ci_plan # noqa: E402 import artifact_targets # noqa: E402 import product_metadata # noqa: E402 -import release_plan # noqa: E402 BASE_PRODUCTS = { @@ -69,6 +68,43 @@ def read_toml(path: pathlib.Path) -> dict: return tomllib.load(handle) +def bun_json(args: list[str]) -> object: + output = subprocess.check_output(["tools/dev/bun.sh", *args], cwd=ROOT, text=True) + return json.loads(output) + + +def release_graph() -> dict: + value = bun_json(["tools/release/release_graph_query.mjs", "graph"]) + if not isinstance(value, dict): + fail("release graph query did not return an object") + return value + + +def release_product_projects() -> dict[str, str]: + value = bun_json(["tools/release/release_graph_query.mjs", "product-projects"]) + if not isinstance(value, dict) or not all( + isinstance(key, str) and isinstance(item, str) for key, item in value.items() + ): + fail("release graph product-project query did not return a string map") + return value + + +def release_plans_for_single_paths(paths: list[str]) -> dict[str, dict]: + value = bun_json( + [ + "tools/release/release_graph_query.mjs", + "plans-for-paths", + "--paths-json", + json.dumps(paths, separators=(",", ":")), + ] + ) + if not isinstance(value, dict) or not all( + isinstance(key, str) and isinstance(item, dict) for key, item in value.items() + ): + fail("release graph plans-for-paths query did not return a plan map") + return value + + def extension_product_id(sql_name: str) -> str: return "oliphaunt-extension-" + sql_name.replace("_", "-").lower() @@ -260,6 +296,7 @@ def check_release_metadata(graph: dict) -> None: ) projects = moon_projects() + product_projects = release_product_projects() for product, config in products.items(): release_path = ROOT / config["path"] / "release.toml" raw = read_toml(release_path) @@ -272,7 +309,7 @@ def check_release_metadata(graph: dict) -> None: if not config.get("tag_prefix") or not config.get("version_files") or not config.get("changelog_path"): fail(f"{product} must have release-please tag/version/changelog metadata") - project_id = release_plan.release_product_project_id(product, products, graph["moon_projects"]) + project_id = product_projects[product] project = projects.get(project_id) if project is None: fail(f"{product} has no owning Moon project") @@ -334,20 +371,21 @@ def check_release_planning(graph: dict) -> None: } | all_extension_products, } - for path, expected in contains_cases.items(): - plan = release_plan.build_plan(graph, [path]) - actual = set(plan.get("releaseProducts", [])) - if not expected <= actual: - fail(f"{path} release plan expected at least {sorted(expected)}, got {sorted(actual)}") - exact_cases = { "src/extensions/contrib/amcheck/release.toml": {"oliphaunt-extension-amcheck"}, "src/extensions/external/vector/source.toml": {"oliphaunt-extension-vector"}, "src/shared/fixtures/protocol/query-response-cases.json": set(), "docs/maintainers/release.md": set(), } + plans = release_plans_for_single_paths(sorted({*contains_cases, *exact_cases})) + for path, expected in contains_cases.items(): + plan = plans[path] + actual = set(plan.get("releaseProducts", [])) + if not expected <= actual: + fail(f"{path} release plan expected at least {sorted(expected)}, got {sorted(actual)}") + for path, expected in exact_cases.items(): - plan = release_plan.build_plan(graph, [path]) + plan = plans[path] actual = set(plan.get("releaseProducts", [])) if actual != expected: fail(f"{path} release plan expected exactly {sorted(expected)}, got {sorted(actual)}") @@ -1317,7 +1355,7 @@ def check_ci_builder_planning() -> None: def main() -> int: - graph = release_plan.load_graph() + graph = release_graph() policy = graph.get("policy") if not isinstance(policy, dict): fail("release metadata must define policy") diff --git a/tools/policy/check-repo-structure.sh b/tools/policy/check-repo-structure.sh index 63fc7bea..eac18445 100755 --- a/tools/policy/check-repo-structure.sh +++ b/tools/policy/check-repo-structure.sh @@ -550,7 +550,7 @@ require_text tools/graph/ci_plan.py 'moon_ci_job_targets' require_text tools/graph/ci_plan.py 'ci-' require_text tools/graph/ci_plan.py 'job_targets_for_jobs' reject_text tools/graph/ci_plan.py 'import plan as release_plan' -require_text tools/graph/graph.py 'import release_plan' +require_text tools/graph/graph.py 'release_graph_query.mjs' reject_text tools/graph/graph.py 'import plan as release_plan' require_text tools/graph/ci_plan.py 'WASM_RUNTIME_PORTABLE_TASK' require_text tools/graph/ci_plan.py 'WASM_RUNTIME_JOBS' diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index 726eb070..d01fe6e3 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -18,5 +18,4 @@ tools/release/package_liboliphaunt_cargo_artifacts.py tools/release/package_liboliphaunt_wasix_cargo_artifacts.py tools/release/product_metadata.py tools/release/release.py -tools/release/release_plan.py tools/release/sync_release_pr.py diff --git a/tools/release/release-graph.mjs b/tools/release/release-graph.mjs index 28de1323..71f102cf 100644 --- a/tools/release/release-graph.mjs +++ b/tools/release/release-graph.mjs @@ -257,12 +257,41 @@ function releasePackagePaths(projects, prefix) { return paths; } -export function tagPrefix(product, prefix = "release-graph") { - const { config, byComponent } = releasePleasePackagesByComponent(prefix); - const packageConfig = byComponent.get(product)?.packageConfig; - if (!packageConfig) { +function releasePleasePackage(product, prefix) { + const { byComponent } = releasePleasePackagesByComponent(prefix); + const packageInfo = byComponent.get(product); + if (!packageInfo) { fail(prefix, `unknown release-please component ${product}`); } + return packageInfo; +} + +function packageRelativePath(product, relativePath, context, prefix) { + if (typeof relativePath !== "string" || relativePath.length === 0) { + fail(prefix, `${context} must be a non-empty path string`); + } + const { packagePath } = releasePleasePackage(product, prefix); + const packageRoot = path.posix.normalize(packagePath.replaceAll("\\", "/")); + const relative = relativePath.replaceAll("\\", "/"); + const normalized = path.posix.normalize(path.posix.join(packageRoot, relative)); + if ( + path.posix.isAbsolute(relative) || + (normalized !== packageRoot && !normalized.startsWith(`${packageRoot}/`)) + ) { + fail(prefix, `${context} must stay within the product package path`); + } + return normalized; +} + +function requireExistingPath(relativePath, context, prefix) { + if (!existsSync(path.join(ROOT, relativePath))) { + fail(prefix, `${context} does not exist: ${relativePath}`); + } +} + +export function tagPrefix(product, prefix = "release-graph") { + const { config } = releasePleasePackagesByComponent(prefix); + const { packageConfig } = releasePleasePackage(product, prefix); if (packageConfig.component !== product) { fail(prefix, `${product} release-please component must match product id`); } @@ -275,6 +304,53 @@ export function tagPrefix(product, prefix = "release-graph") { return `${product}-v`; } +export function versionFiles(product, prefix = "release-graph") { + const { packageConfig } = releasePleasePackage(product, prefix); + const releaseType = packageConfig["release-type"]; + const versionFile = packageConfig["version-file"]; + let canonical; + if (typeof versionFile === "string" && versionFile.length > 0) { + canonical = packageRelativePath(product, versionFile, `${product}.version-file`, prefix); + } else if (releaseType === "rust") { + canonical = packageRelativePath(product, "Cargo.toml", `${product}.rust`, prefix); + } else if (releaseType === "node" || releaseType === "expo") { + canonical = packageRelativePath(product, "package.json", `${product}.node`, prefix); + } else { + fail( + prefix, + `${product} release-please config must declare version-file for release type ${JSON.stringify(releaseType)}`, + ); + } + + const extraFiles = packageConfig["extra-files"] ?? []; + if (!Array.isArray(extraFiles)) { + fail(prefix, `${product}.extra-files must be a list`); + } + const files = [canonical]; + for (const [index, entry] of extraFiles.entries()) { + const context = `${product}.extra-files[${index}]`; + if (typeof entry === "string") { + files.push(packageRelativePath(product, entry, context, prefix)); + } else if (entry !== null && typeof entry === "object" && !Array.isArray(entry)) { + files.push(packageRelativePath(product, entry.path, `${context}.path`, prefix)); + } else { + fail(prefix, `${context} must be a path string or object`); + } + } + for (const file of files) { + requireExistingPath(file, `${product} version file`, prefix); + } + return files; +} + +export function changelogPath(product, prefix = "release-graph") { + const { packageConfig } = releasePleasePackage(product, prefix); + const relative = packageConfig["changelog-path"] ?? "CHANGELOG.md"; + const changelog = packageRelativePath(product, relative, `${product}.changelog-path`, prefix); + requireExistingPath(changelog, `${product} changelog`, prefix); + return changelog; +} + function graphProducts(projects, prefix) { const paths = releasePackagePaths(projects, prefix); const manifest = readJson(".release-please-manifest.json", prefix); @@ -290,7 +366,10 @@ function graphProducts(projects, prefix) { products[product] = { ...metadata, path: packagePath, + changelog_path: changelogPath(product, prefix), + derived_version_files: metadata.derived_version_files ?? [], tag_prefix: tagPrefix(product, prefix), + version_files: versionFiles(product, prefix), }; } return products; diff --git a/tools/release/release.py b/tools/release/release.py index b84ac071..eee1185a 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -22,7 +22,6 @@ import package_liboliphaunt_cargo_artifacts import package_liboliphaunt_wasix_cargo_artifacts import product_metadata -import release_plan ROOT = Path(__file__).resolve().parents[2] @@ -52,6 +51,11 @@ def run(args: list[str], *, cwd: Path = ROOT, env: dict[str, str] | None = None) raise SystemExit(result.returncode) +def bun_json(args: list[str]) -> object: + output = subprocess.check_output(["tools/dev/bun.sh", *args], cwd=ROOT, text=True) + return json.loads(output) + + def is_windows_native_target(target: str | None, runtime_dir: Path | None = None) -> bool: if target is not None and target.startswith("windows-"): return True @@ -449,9 +453,17 @@ def selected_products_from_passthrough(args: list[str]) -> list[str]: unknown = sorted(set(value) - known) if unknown: fail(f"unknown release products: {', '.join(unknown)}") - selected = set(value) - graph = release_plan.load_graph() - return release_plan.release_order(graph["products"], graph["moon_projects"], selected) + ordered = bun_json( + [ + "tools/release/release_graph_query.mjs", + "release-order", + "--products-json", + json.dumps(value, separators=(",", ":")), + ] + ) + if not isinstance(ordered, list) or not all(isinstance(item, str) for item in ordered): + fail("release graph query returned an invalid release order") + return ordered def product_tag(product: str) -> str: diff --git a/tools/release/release_graph_query.mjs b/tools/release/release_graph_query.mjs new file mode 100644 index 00000000..f1f5194b --- /dev/null +++ b/tools/release/release_graph_query.mjs @@ -0,0 +1,182 @@ +#!/usr/bin/env bun +import { + buildPlan, + compareText, + loadGraph, + normalizeFiles, + releaseOrder, + releaseProductProjectId, +} from "./release-graph.mjs"; + +const TOOL = "release_graph_query.mjs"; + +function fail(message) { + console.error(`${TOOL}: ${message}`); + process.exit(2); +} + +function sortedValue(value) { + if (Array.isArray(value)) { + return value.map(sortedValue); + } + if (value !== null && typeof value === "object") { + return Object.fromEntries( + Object.keys(value) + .sort(compareText) + .map((key) => [key, sortedValue(value[key])]), + ); + } + return value; +} + +function printJson(value) { + console.log(JSON.stringify(sortedValue(value), null, 2)); +} + +function parseJsonFlag(argv, name, { required = false } = {}) { + const raw = stringFlag(argv, name, { required }); + if (raw === undefined) { + return undefined; + } + try { + return JSON.parse(raw); + } catch (error) { + fail(`--${name} must be valid JSON: ${error.message}`); + } +} + +function stringFlag(argv, name, { required = false } = {}) { + const flag = `--${name}`; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === flag) { + if (index + 1 >= argv.length) { + fail(`${flag} requires a value`); + } + return argv[index + 1]; + } + if (value.startsWith(`${flag}=`)) { + return value.slice(flag.length + 1); + } + } + if (required) { + fail(`${flag} is required`); + } + return undefined; +} + +function changedFiles(argv) { + const files = []; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === "--changed-file") { + if (index + 1 >= argv.length) { + fail("--changed-file requires a value"); + } + files.push(argv[index + 1]); + index += 1; + } else if (value.startsWith("--changed-file=")) { + files.push(value.slice("--changed-file=".length)); + } else { + fail(`unknown argument ${value}`); + } + } + return files; +} + +function assertStringList(value, label) { + if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) { + fail(`${label} must be a JSON string list`); + } + return value; +} + +function graphProductProjects(graph) { + const products = graph.products; + const projects = graph.moon_projects; + return Object.fromEntries( + Object.keys(products) + .sort(compareText) + .map((product) => [ + product, + releaseProductProjectId(product, products, projects, TOOL), + ]), + ); +} + +function runGraph() { + printJson(loadGraph(TOOL)); +} + +function runProductProjects() { + printJson(graphProductProjects(loadGraph(TOOL))); +} + +function runReleaseOrder(argv) { + const graph = loadGraph(TOOL); + const selected = assertStringList( + parseJsonFlag(argv, "products-json", { required: true }), + "--products-json", + ); + const known = new Set(Object.keys(graph.products)); + const unknown = [...new Set(selected)].filter((product) => !known.has(product)).sort(compareText); + if (unknown.length > 0) { + fail(`unknown release products: ${unknown.join(", ")}`); + } + printJson(releaseOrder(graph.products, graph.moon_projects, selected, TOOL)); +} + +function runPlan(argv) { + const graph = loadGraph(TOOL); + printJson(buildPlan(graph, normalizeFiles(changedFiles(argv)), TOOL)); +} + +function runPlansForPaths(argv) { + const paths = assertStringList( + parseJsonFlag(argv, "paths-json", { required: true }), + "--paths-json", + ); + const graph = loadGraph(TOOL); + printJson( + Object.fromEntries( + paths + .map((file) => [file, buildPlan(graph, normalizeFiles([file]), TOOL)]) + .sort(([left], [right]) => compareText(left, right)), + ), + ); +} + +function usage() { + return `usage: tools/release/release_graph_query.mjs [options] + +Commands: + graph + product-projects + release-order --products-json JSON + plan [--changed-file PATH...] + plans-for-paths --paths-json JSON +`; +} + +function main(argv) { + const [command, ...rest] = argv; + if (command === "graph") { + runGraph(); + } else if (command === "product-projects") { + runProductProjects(); + } else if (command === "release-order") { + runReleaseOrder(rest); + } else if (command === "plan") { + runPlan(rest); + } else if (command === "plans-for-paths") { + runPlansForPaths(rest); + } else if (command === "--help" || command === "-h") { + console.log(usage()); + } else { + fail(command ? `unknown command ${command}` : "missing command"); + } +} + +if (import.meta.main) { + main(Bun.argv.slice(2)); +} diff --git a/tools/release/release_plan.py b/tools/release/release_plan.py deleted file mode 100644 index f50657e8..00000000 --- a/tools/release/release_plan.py +++ /dev/null @@ -1,534 +0,0 @@ -from __future__ import annotations - -import argparse -import fnmatch -import hashlib -import json -import os -import pathlib -import subprocess -import sys -from collections import deque -from typing import Iterable - -import product_metadata - - -ROOT = pathlib.Path(__file__).resolve().parents[2] -EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904" -GENERATED_PATH_PARTS = { - ".build", - ".cxx", - ".expo", - ".gradle", - ".kotlin", - ".moon", - ".next", - ".source", - "DerivedData", - "Pods", - "__pycache__", - "dist", - "lib", - "node_modules", - "out", - "target", -} -RELEASE_DEPENDENCY_SCOPES = {"production", "peer"} - - -def fail(message: str) -> None: - raise SystemExit(message) - - -def load_graph() -> dict: - graph = product_metadata.load_graph() - graph["moon_projects"] = moon_projects_by_id() - return graph - - -def moon_bin() -> str: - if configured := os.environ.get("MOON_BIN"): - return configured - proto_moon = pathlib.Path.home() / ".proto" / "bin" / "moon" - return str(proto_moon) if proto_moon.exists() else "moon" - - -def run_git(args: list[str]) -> str: - return subprocess.check_output(["git", *args], cwd=ROOT, text=True) - - -def run_moon(args: list[str]) -> dict: - output = subprocess.check_output([moon_bin(), *args], cwd=ROOT, text=True) - return json.loads(output) - - -def moon_projects_by_id() -> dict[str, dict]: - data = run_moon(["query", "projects"]) - projects = data.get("projects") - if not isinstance(projects, list): - fail("moon query projects did not return a projects array") - - parsed: dict[str, dict] = {} - for project in projects: - if not isinstance(project, dict) or not isinstance(project.get("id"), str): - continue - config = project.get("config") if isinstance(project.get("config"), dict) else {} - raw_deps = project.get("dependencies") or config.get("dependsOn") or [] - dependencies: dict[str, str] = {} - if isinstance(raw_deps, list): - for dependency in raw_deps: - if isinstance(dependency, str): - dependencies[dependency] = "production" - elif isinstance(dependency, dict) and isinstance(dependency.get("id"), str): - dependencies[dependency["id"]] = str(dependency.get("scope") or "production") - parsed[project["id"]] = { - "id": project["id"], - "source": project.get("source") or config.get("source") or "", - "dependsOn": sorted(dependencies), - "dependencyScopes": dict(sorted(dependencies.items())), - "tags": sorted(config.get("tags") or []), - "project": config.get("project") if isinstance(config.get("project"), dict) else {}, - } - return parsed - - -def tag_match_pattern(prefix: str) -> str: - return f"{prefix}[0-9]*" if prefix else "[0-9]*" - - -def tag_prefixes(product_config: dict) -> list[str]: - prefix = product_config.get("tag_prefix") - if not isinstance(prefix, str) or not prefix: - fail("release metadata product entries must declare tag_prefix") - legacy_prefixes = product_config.get("legacy_tag_prefixes", []) - if not isinstance(legacy_prefixes, list) or not all( - isinstance(item, str) for item in legacy_prefixes - ): - fail("release metadata legacy_tag_prefixes must be a string list when present") - return [prefix, *legacy_prefixes] - - -def latest_tag_for_prefix(prefix: str, head_ref: str) -> str: - result = subprocess.run( - [ - "git", - "describe", - "--tags", - "--abbrev=0", - "--match", - tag_match_pattern(prefix), - head_ref, - ], - cwd=ROOT, - text=True, - capture_output=True, - check=False, - ) - if result.returncode == 0: - return result.stdout.strip() - return "" - - -def latest_product_tag(product_config: dict, head_ref: str) -> str: - for prefix in tag_prefixes(product_config): - if tag := latest_tag_for_prefix(prefix, head_ref): - return tag - return EMPTY_TREE - - -def commit_for_ref(ref: str) -> str: - return run_git(["rev-parse", f"{ref}^{{commit}}"]).strip() - - -def changed_files_from_refs(base_ref: str, head_ref: str) -> list[str]: - try: - if base_ref == EMPTY_TREE: - output = run_git(["diff", "--name-only", base_ref, head_ref, "--"]) - else: - output = run_git(["diff", "--name-only", f"{base_ref}...{head_ref}", "--"]) - except subprocess.CalledProcessError as error: - fail(f"failed to read changed files between {base_ref} and {head_ref}: {error}") - return sorted(line for line in output.splitlines() if line) - - -def normalize_files(files: Iterable[str]) -> list[str]: - normalized: set[str] = set() - for file in files: - path = file.strip().replace("\\", "/") - if path.startswith("./"): - path = path[2:] - if path and not is_generated_local_state(path): - normalized.add(path) - return sorted(normalized) - - -def is_generated_local_state(path: str) -> bool: - if path.startswith("target/"): - return True - return any(part in GENERATED_PATH_PARTS for part in pathlib.Path(path).parts) - - -def split_patterns(patterns: Iterable[str]) -> tuple[list[str], list[str]]: - includes: list[str] = [] - excludes: list[str] = [] - for pattern in patterns: - if pattern.startswith("!"): - excludes.append(pattern[1:]) - else: - includes.append(pattern) - return includes, excludes - - -def matches_pattern(path: str, pattern: str) -> bool: - return fnmatch.fnmatchcase(path, pattern) - - -def matches_any(path: str, patterns: Iterable[str]) -> bool: - return any(matches_pattern(path, pattern) for pattern in patterns) - - -def product_matches(path: str, patterns: Iterable[str]) -> bool: - includes, excludes = split_patterns(patterns) - return matches_any(path, includes) and not matches_any(path, excludes) - - -def owner_project_for_path(projects: dict[str, dict], path: str) -> str | None: - # Moon 2.3 exposes project sources/dependencies as JSON, but does not expose - # a non-executing stdin changed-file affectedness query. Release planning - # keeps this as a pure adapter over `moon query projects`; no hand-authored - # source globs or dependency graph are allowed here. - if is_generated_local_state(path): - return None - matches = [ - project - for project in projects.values() - if project["source"] == "." - or path == project["source"] - or path.startswith(f"{project['source']}/") - ] - matches.sort(key=lambda project: len(project["source"]), reverse=True) - return matches[0]["id"] if matches else None - - -def dependents_by_project(projects: dict[str, dict], *, release_only: bool = False) -> dict[str, set[str]]: - dependents: dict[str, set[str]] = {project: set() for project in projects} - for project, config in projects.items(): - scopes = config.get("dependencyScopes", {}) - for dependency in config.get("dependsOn", []): - if release_only and scopes.get(dependency, "production") not in RELEASE_DEPENDENCY_SCOPES: - continue - dependents.setdefault(dependency, set()).add(project) - return dependents - - -def downstream_projects( - projects: dict[str, dict], - direct: Iterable[str], - *, - release_only: bool = False, -) -> set[str]: - dependents = dependents_by_project(projects, release_only=release_only) - selected: set[str] = set(direct) - queue: deque[str] = deque(sorted(selected)) - while queue: - current = queue.popleft() - for downstream in sorted(dependents.get(current, set())): - if downstream not in selected: - selected.add(downstream) - queue.append(downstream) - return selected - - -def release_product_project_id(product: str, products: dict[str, dict], projects: dict[str, dict]) -> str: - if product in projects: - return product - package_path = products[product].get("path") - if not isinstance(package_path, str) or not package_path: - fail(f"release product {product} is missing package path metadata") - matches = [ - project - for project in projects.values() - if package_path == project["source"] or package_path.startswith(f"{project['source']}/") - ] - matches.sort(key=lambda project: len(project["source"]), reverse=True) - if not matches: - fail(f"release product {product} has no owning Moon project for {package_path}") - return matches[0]["id"] - - -def release_products_for_projects( - products: dict[str, dict], - projects: dict[str, dict], - project_ids: Iterable[str], -) -> set[str]: - selected_projects = set(project_ids) - selected: set[str] = set() - for product in products: - project_id = release_product_project_id(product, products, projects) - if project_id in selected_projects: - selected.add(product) - return selected - - -def release_order(products: dict[str, dict], projects: dict[str, dict], selected: Iterable[str]) -> list[str]: - selected_set = set(selected) - product_project = { - product: release_product_project_id(product, products, projects) - for product in products - } - ordered: list[str] = [] - remaining = set(selected_set) - while remaining: - ready: list[str] = [] - for product in sorted(remaining): - project_id = product_project[product] - project_config = projects.get(project_id, {}) - scopes = project_config.get("dependencyScopes", {}) - deps = { - dependency - for dependency in project_config.get("dependsOn", []) - if scopes.get(dependency, "production") in RELEASE_DEPENDENCY_SCOPES - } - selected_deps = { - candidate - for candidate, candidate_project in product_project.items() - if candidate in selected_set and candidate_project in deps - } - if selected_deps <= set(ordered): - ready.append(product) - if not ready: - fail(f"Moon release product graph has a dependency cycle: {sorted(remaining)}") - ordered.extend(ready) - remaining.difference_update(ready) - return ordered - - -def docs_only_change(files: Iterable[str]) -> bool: - normalized = list(files) - return bool(normalized) and all( - file.startswith("docs/") - or file.startswith("src/docs/") - or file in {"README.md"} - for file in normalized - ) - - -def build_plan(graph: dict, files: list[str]) -> dict: - products = graph.get("products") - if not isinstance(products, dict): - fail("release metadata must define [products.] entries") - projects = graph.get("moon_projects") - if not isinstance(projects, dict): - fail("Moon project graph is missing from release plan metadata") - - direct_projects = { - project - for file in files - if (project := owner_project_for_path(projects, file)) is not None - } - affected_projects = downstream_projects(projects, direct_projects) - release_projects = downstream_projects(projects, direct_projects, release_only=True) - release_product_set = release_products_for_projects(products, projects, release_projects) - release_products = release_order(products, projects, release_product_set) - release_product_projects = { - release_product_project_id(product, products, projects) - for product in release_products - } - direct = release_order( - products, - projects, - release_products_for_projects(products, projects, direct_projects), - ) - return finalize_plan({ - "changedFiles": files, - "directProducts": direct, - "releaseProducts": release_products, - "directMoonProjects": sorted(direct_projects), - "affectedMoonProjects": sorted(affected_projects), - "releaseMoonProjects": sorted(release_product_projects), - "productIds": list(products), - "hasReleaseChanges": bool(release_products), - "docsOnly": not release_products and docs_only_change(files), - "versioning": graph.get("policy", {}).get("versioning", "independent"), - "extensionSelection": "exact-sql-extension", - }) - - -def build_plan_from_product_tags( - graph: dict, - head_ref: str, - include_current_tags: bool = False, -) -> dict: - products = graph.get("products") - if not isinstance(products, dict): - fail("release metadata must define [products.] entries") - - direct: set[str] = set() - changed: set[str] = set() - product_base_refs: dict[str, str] = {} - current_tagged_products: set[str] = set() - head_commit = commit_for_ref(head_ref) if include_current_tags else "" - - for product, config in products.items(): - base_ref = latest_product_tag(config, head_ref) - product_base_refs[product] = base_ref - if include_current_tags and base_ref != EMPTY_TREE: - tag_commit = commit_for_ref(base_ref) - if tag_commit == head_commit: - direct.add(product) - current_tagged_products.add(product) - continue - product_files = changed_files_from_refs(base_ref, head_ref) - changed.update(product_files) - product_plan = build_plan(graph, normalize_files(product_files)) - if product in product_plan.get("releaseProducts", []): - direct.add(product) - - projects = graph.get("moon_projects") - if not isinstance(projects, dict): - fail("Moon project graph is missing from release plan metadata") - direct_projects = { - release_product_project_id(product, products, projects) - for product in direct - } - affected_projects = downstream_projects(projects, direct_projects) - release_projects = downstream_projects(projects, direct_projects, release_only=True) - release_products = release_order( - products, - projects, - release_products_for_projects(products, projects, release_projects), - ) - return finalize_plan({ - "changedFiles": sorted(changed), - "directProducts": release_order(products, projects, direct), - "releaseProducts": release_products, - "directMoonProjects": sorted(direct_projects), - "affectedMoonProjects": sorted(affected_projects), - "releaseMoonProjects": sorted(release_projects), - "productIds": list(products), - "hasReleaseChanges": bool(release_products), - "docsOnly": not release_products and docs_only_change(changed), - "versioning": graph.get("policy", {}).get("versioning", "independent"), - "extensionSelection": "exact-sql-extension", - "productBaseRefs": product_base_refs, - "currentTaggedProducts": sorted(current_tagged_products), - }) - - -def release_products_slug(products: list[str]) -> str: - if not products: - return "none" - short_names = { - "liboliphaunt-native": "native", - } - return "-".join(short_names.get(product, product.replace("oliphaunt-", "")) for product in products) - - -def finalize_plan(plan: dict) -> dict: - hash_input = { - "changedFiles": plan.get("changedFiles", []), - "directProducts": plan.get("directProducts", []), - "releaseProducts": plan.get("releaseProducts", []), - "productBaseRefs": plan.get("productBaseRefs", {}), - "currentTaggedProducts": plan.get("currentTaggedProducts", []), - } - digest = hashlib.sha256( - json.dumps(hash_input, sort_keys=True, separators=(",", ":")).encode("utf-8") - ).hexdigest()[:12] - plan["planHash"] = digest - plan["releaseBranch"] = f"release/{release_products_slug(plan.get('releaseProducts', []))}-{digest}" - return plan - - -def print_github_output(plan: dict) -> None: - products = plan["releaseProducts"] - extension_products = sorted(product for product in products if product.startswith("oliphaunt-extension-")) - print(f"has_release_changes={str(plan['hasReleaseChanges']).lower()}") - print(f"has_extension_products={str(bool(extension_products)).lower()}") - print(f"docs_only={str(plan['docsOnly']).lower()}") - print(f"products_csv={','.join(products)}") - print(f"products_json={json.dumps(products, separators=(',', ':'))}") - print(f"extension_products_json={json.dumps(extension_products, separators=(',', ':'))}") - print(f"plan_hash={plan['planHash']}") - print(f"release_branch={plan['releaseBranch']}") - for product in plan.get("productIds", []): - key = "product_" + product.replace("-", "_") - print(f"{key}={str(product in products).lower()}") - print( - "direct_products_json=" - f"{json.dumps(plan['directProducts'], separators=(',', ':'))}" - ) - print( - "product_base_refs_json=" - f"{json.dumps(plan.get('productBaseRefs', {}), separators=(',', ':'))}" - ) - - -def parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="Plan independent Oliphaunt product releases from changed files." - ) - parser.add_argument("--base-ref", help="base git ref for diff planning") - parser.add_argument("--head-ref", default="HEAD", help="head git ref for diff planning") - parser.add_argument( - "--from-product-tags", - action="store_true", - help="plan from each product's latest tag instead of one shared base ref", - ) - parser.add_argument( - "--include-current-tags", - action="store_true", - help="with --from-product-tags, keep products selected when their latest tag already points at HEAD", - ) - parser.add_argument( - "--changed-file", - action="append", - default=[], - help="explicit changed file; may be passed more than once", - ) - parser.add_argument( - "--format", - choices=["text", "json", "github-output"], - default="text", - help="output format", - ) - return parser.parse_args(argv) - - -def main(argv: list[str]) -> int: - args = parse_args(argv) - if args.changed_file: - files = normalize_files(args.changed_file) - graph = load_graph() - plan = build_plan(graph, files) - elif args.from_product_tags: - graph = load_graph() - plan = build_plan_from_product_tags( - graph, - args.head_ref, - include_current_tags=args.include_current_tags, - ) - elif args.base_ref: - files = changed_files_from_refs(args.base_ref, args.head_ref) - graph = load_graph() - plan = build_plan(graph, files) - else: - files = [] - graph = load_graph() - plan = build_plan(graph, files) - - if args.format == "json": - print(json.dumps(plan, indent=2, sort_keys=True)) - elif args.format == "github-output": - print_github_output(plan) - else: - changed_files = plan.get("changedFiles", []) - if not changed_files: - print("No changed files were provided; no product release is planned.") - elif plan["hasReleaseChanges"]: - print("Release products: " + ", ".join(plan["releaseProducts"])) - print("Direct products: " + ", ".join(plan["directProducts"])) - else: - print("No product release is planned for these changes.") - return 0 From 5358ed3a9622681c2d7c4d98565437b831ffe62d Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 00:52:31 +0000 Subject: [PATCH 136/137] chore: port artifact target matrix to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 13 + src/runtimes/broker/moon.yml | 2 +- src/runtimes/node-direct/moon.yml | 2 +- tools/graph/ci_plan.py | 99 ++- tools/policy/check-release-policy.py | 2 +- tools/policy/python-entrypoints.allowlist | 1 - tools/release/artifact_target_matrix.mjs | 557 ++++++++++++ tools/release/artifact_target_matrix.py | 440 ---------- tools/release/check_artifact_targets.py | 31 +- tools/release/release-artifact-targets.mjs | 827 ++++++++++++++++-- 10 files changed, 1398 insertions(+), 576 deletions(-) create mode 100644 tools/release/artifact_target_matrix.mjs delete mode 100755 tools/release/artifact_target_matrix.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 7bf4aab8..0d9f1a1c 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,19 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Ported the release artifact target matrix helper from Python to + Bun. `tools/release/artifact_target_matrix.mjs` now derives liboliphaunt + native/WASIX, broker, Node direct, React Native Android, and exact-extension + CI matrices from the shared Bun artifact target metadata in + `tools/release/release-artifact-targets.mjs`; `tools/graph/ci_plan.py` and + artifact policy checks consume that JSON surface instead of importing + `artifact_target_matrix.py`. Fresh checks passed: Python/Bun matrix parity for + every former matrix name, focused selected-extension matrix smoke, + `GITHUB_EVENT_NAME=workflow_dispatch python3 tools/graph/ci_plan.py`, focused + `WASM_TARGET=linux-x64-gnu` and `NATIVE_TARGET=linux-x64-gnu` planner probes, + `python3 tools/release/check_artifact_targets.py`, `tools/graph/graph.py + check`, `python3 tools/policy/check-release-policy.py`, `bash + tools/policy/check-repo-structure.sh`, and `git diff --check`. - 2026-06-26: `git status --short --branch` was clean on `f0rr0/reduce-oliphaunt-icu-crate-size` at commit `895ed8d` before the fresh example e2e run. diff --git a/src/runtimes/broker/moon.yml b/src/runtimes/broker/moon.yml index 3941dad9..edddc651 100644 --- a/src/runtimes/broker/moon.yml +++ b/src/runtimes/broker/moon.yml @@ -113,7 +113,7 @@ tasks: - "/tools/release/release-asset-validation.mjs" - "/tools/release/release-artifact-targets.mjs" - "/tools/policy/moon.mjs" - - "/tools/release/artifact_target_matrix.py" + - "/tools/release/artifact_target_matrix.mjs" - "/release-please-config.json" - "/.release-please-manifest.json" - "/src/**/release.toml" diff --git a/src/runtimes/node-direct/moon.yml b/src/runtimes/node-direct/moon.yml index b228523a..1b27a61b 100644 --- a/src/runtimes/node-direct/moon.yml +++ b/src/runtimes/node-direct/moon.yml @@ -79,7 +79,7 @@ tasks: - "oliphaunt-node-direct:package" inputs: - "/src/runtimes/node-direct/**/*" - - "/tools/release/artifact_target_matrix.py" + - "/tools/release/artifact_target_matrix.mjs" - "/tools/release/artifact_targets.py" - "/tools/release/check-node-direct-release-assets.mjs" - "/tools/release/release-asset-validation.mjs" diff --git a/tools/graph/ci_plan.py b/tools/graph/ci_plan.py index c4130479..9a65ed55 100644 --- a/tools/graph/ci_plan.py +++ b/tools/graph/ci_plan.py @@ -17,9 +17,6 @@ ROOT = Path(__file__).resolve().parents[2] -sys.path.insert(0, str(ROOT / "tools" / "release")) - -import artifact_target_matrix # noqa: E402 BASE_JOBS = {"affected"} @@ -99,6 +96,49 @@ def moon(args: list[str]) -> dict[str, object]: return json.loads(output) +def bun_json(args: list[str]) -> object: + output = subprocess.check_output(["tools/dev/bun.sh", *args], cwd=ROOT, text=True) + return json.loads(output) + + +def artifact_target_matrix( + matrix: str, + *, + native_target: str = "all", + wasm_target: str = "all", + selected_targets: set[str] | None = None, + selected_products: set[str] | None = None, +) -> dict[str, list[dict[str, str]]]: + args = ["tools/release/artifact_target_matrix.mjs", matrix] + if native_target != "all": + args.extend(["--native-target", native_target]) + if wasm_target != "all": + args.extend(["--wasm-target", wasm_target]) + if selected_targets is not None: + args.extend(["--selected-targets-json", json.dumps(sorted(selected_targets), separators=(",", ":"))]) + if selected_products is not None: + args.extend(["--selected-products-json", json.dumps(sorted(selected_products), separators=(",", ":"))]) + value = bun_json(args) + if not isinstance(value, dict) or not isinstance(value.get("include"), list): + raise RuntimeError(f"{matrix} matrix query did not return a matrix object") + return value + + +def artifact_target_string_list(args: list[str]) -> list[str]: + value = bun_json(["tools/release/artifact_target_matrix.mjs", *args]) + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + raise RuntimeError("artifact target query did not return a string list") + return value + + +def exact_extension_products() -> list[str]: + return artifact_target_string_list(["exact-extension-products"]) + + +def liboliphaunt_native_runtime_targets_for_surface(surface: str) -> list[str]: + return artifact_target_string_list(["runtime-targets-for-surface", "--surface", surface]) + + def affected_projects_and_tasks() -> tuple[set[str], set[str], set[str]]: output = subprocess.check_output( ["tools/dev/bun.sh", "tools/graph/affected.mjs", "summary"], @@ -219,7 +259,7 @@ def plan_jobs_for_affected( ) -> set[str]: jobs = set(ALWAYS_JOBS) jobs.update(jobs_for_targets(tasks, allowed_jobs=ALL_BUILDER_JOBS)) - if direct_projects & set(artifact_target_matrix.exact_extension_products()): + if direct_projects & set(exact_extension_products()): jobs.update({"extension-artifacts-native", "extension-artifacts-wasix", "extension-packages"}) if "react-native-sdk-package" in jobs: jobs.update(ANDROID_MOBILE_JOBS) @@ -245,7 +285,7 @@ def native_target_subset_for_jobs(jobs: set[str], tasks: set[str]) -> set[str] | if "swift-sdk-package" in jobs: targets.add("ios-xcframework") if "kotlin-sdk-package" in jobs: - targets.update(artifact_target_matrix.liboliphaunt_native_runtime_targets_for_surface("maven")) + targets.update(liboliphaunt_native_runtime_targets_for_surface("maven")) return targets or None @@ -253,7 +293,7 @@ def mobile_native_targets_for_jobs(jobs: set[str]) -> set[str]: targets: set[str] = set() for job, surface in MOBILE_JOB_SURFACES.items(): if job in jobs: - targets.update(artifact_target_matrix.liboliphaunt_native_runtime_targets_for_surface(surface)) + targets.update(liboliphaunt_native_runtime_targets_for_surface(surface)) return targets @@ -305,7 +345,8 @@ def liboliphaunt_native_desktop_runtime_matrix( native_target: str = "all", selected_targets: set[str] | None = None, ) -> dict[str, list[dict[str, str]]]: - return artifact_target_matrix.liboliphaunt_native_desktop_runtime_matrix( + return artifact_target_matrix( + "liboliphaunt-native-desktop-runtime", native_target=native_target, selected_targets=selected_targets, ) @@ -315,7 +356,8 @@ def liboliphaunt_native_android_runtime_matrix( native_target: str = "all", selected_targets: set[str] | None = None, ) -> dict[str, list[dict[str, str]]]: - return artifact_target_matrix.liboliphaunt_native_android_runtime_matrix( + return artifact_target_matrix( + "liboliphaunt-native-android-runtime", native_target=native_target, selected_targets=selected_targets, ) @@ -325,7 +367,8 @@ def liboliphaunt_native_ios_runtime_matrix( native_target: str = "all", selected_targets: set[str] | None = None, ) -> dict[str, list[dict[str, str]]]: - return artifact_target_matrix.liboliphaunt_native_ios_runtime_matrix( + return artifact_target_matrix( + "liboliphaunt-native-ios-runtime", native_target=native_target, selected_targets=selected_targets, ) @@ -335,43 +378,34 @@ def react_native_android_mobile_app_matrix( native_target: str = "all", selected_targets: set[str] | None = None, ) -> dict[str, list[dict[str, str]]]: - return artifact_target_matrix.react_native_android_mobile_app_matrix( + return artifact_target_matrix( + "react-native-android-mobile-app", native_target=native_target, selected_targets=selected_targets, ) def broker_runtime_matrix(native_target: str = "all") -> dict[str, list[dict[str, str]]]: - matrix = artifact_target_matrix.broker_runtime_matrix() - if native_target == "all": - return matrix - include = [target for target in matrix["include"] if target["target"] == native_target] - if not include: - valid_targets = ", ".join(target["target"] for target in matrix["include"]) - raise RuntimeError(f"unknown broker target {native_target}; expected one of: all, {valid_targets}") - return {"include": include} + return artifact_target_matrix("broker-runtime", native_target=native_target) def node_direct_runtime_matrix(native_target: str = "all") -> dict[str, list[dict[str, str]]]: - matrix = artifact_target_matrix.node_direct_runtime_matrix() - if native_target == "all": - return matrix - include = [target for target in matrix["include"] if target["target"] == native_target] - if not include: - valid_targets = ", ".join(target["target"] for target in matrix["include"]) - raise RuntimeError(f"unknown Node direct target {native_target}; expected one of: all, {valid_targets}") - return {"include": include} + return artifact_target_matrix("node-direct-runtime", native_target=native_target) def extension_artifacts_wasix_matrix( wasm_target: str = "all", selected_products: set[str] | None = None, ) -> dict[str, list[dict[str, str]]]: - return artifact_target_matrix.extension_artifacts_wasix_matrix(wasm_target, selected_products) + return artifact_target_matrix( + "extension-artifacts-wasix", + wasm_target=wasm_target, + selected_products=selected_products, + ) def liboliphaunt_wasix_aot_runtime_matrix(wasm_target: str = "all") -> dict[str, list[dict[str, str]]]: - return artifact_target_matrix.liboliphaunt_wasix_aot_runtime_matrix(wasm_target) + return artifact_target_matrix("liboliphaunt-wasix-aot-runtime", wasm_target=wasm_target) def extension_artifacts_native_matrix( @@ -379,7 +413,12 @@ def extension_artifacts_native_matrix( selected_targets: set[str] | None = None, selected_products: set[str] | None = None, ) -> dict[str, list[dict[str, str]]]: - return artifact_target_matrix.extension_artifacts_native_matrix(native_target, selected_targets, selected_products) + return artifact_target_matrix( + "extension-artifacts-native", + native_target=native_target, + selected_targets=selected_targets, + selected_products=selected_products, + ) def targets_for_jobs(jobs: set[str]) -> set[str]: @@ -403,7 +442,7 @@ def selected_extension_products_for_plan( ): return None - exact_products = set(artifact_target_matrix.exact_extension_products()) + exact_products = set(exact_extension_products()) selected = (direct_projects & exact_products) | { target.split(":", 1)[0] for target in tasks diff --git a/tools/policy/check-release-policy.py b/tools/policy/check-release-policy.py index c6ca7618..ab084372 100644 --- a/tools/policy/check-release-policy.py +++ b/tools/policy/check-release-policy.py @@ -654,7 +654,7 @@ def check_ci_policy() -> None: fail(f"E2E workflow must not rebuild source artifacts or invoke builder tasks: {forbidden}") release_workflow_blocks = workflow_job_blocks(".github/workflows/release.yml") - release_tool_patterns = ("tools/release/release.py", "tools/release/artifact_target_matrix.py") + release_tool_patterns = ("tools/release/release.py", "tools/release/artifact_target_matrix.mjs") missing_moon_setup = sorted( job for job, block in release_workflow_blocks.items() diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index d01fe6e3..8a3735b6 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -4,7 +4,6 @@ src/extensions/tools/check-extension-model.py tools/graph/ci_plan.py tools/graph/graph.py tools/policy/check-release-policy.py -tools/release/artifact_target_matrix.py tools/release/artifact_targets.py tools/release/build-extension-ci-artifacts.py tools/release/check_artifact_targets.py diff --git a/tools/release/artifact_target_matrix.mjs b/tools/release/artifact_target_matrix.mjs new file mode 100644 index 00000000..5b460437 --- /dev/null +++ b/tools/release/artifact_target_matrix.mjs @@ -0,0 +1,557 @@ +#!/usr/bin/env bun +import { appendFileSync } from "node:fs"; + +import { + allArtifactTargets, + compareText, + exactExtensionProducts, + extensionArtifactTargets, + fail, + liboliphauntAndroidAbi, + liboliphauntNativeBuildRoot, + liboliphauntNativeCiArtifactRoot, + publishedExtensionTargetIds, +} from "./release-artifact-targets.mjs"; + +const PREFIX = "artifact_target_matrix.mjs"; + +function sortedValue(value) { + if (Array.isArray(value)) { + return value.map(sortedValue); + } + if (value !== null && typeof value === "object") { + return Object.fromEntries( + Object.keys(value) + .sort(compareText) + .map((key) => [key, sortedValue(value[key])]), + ); + } + return value; +} + +function printJson(value, { compact = false } = {}) { + console.log(JSON.stringify(sortedValue(value), null, compact ? 0 : 2)); +} + +function parseJsonFlag(argv, name) { + const raw = stringFlag(argv, name); + if (raw === undefined || raw === "") { + return undefined; + } + try { + return JSON.parse(raw); + } catch (error) { + fail(PREFIX, `--${name} must be valid JSON: ${error.message}`); + } +} + +function stringFlag(argv, name) { + const flag = `--${name}`; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === flag) { + if (index + 1 >= argv.length) { + fail(PREFIX, `${flag} requires a value`); + } + return argv[index + 1]; + } + if (value.startsWith(`${flag}=`)) { + return value.slice(flag.length + 1); + } + } + return undefined; +} + +function parseOptions(argv) { + const options = { + githubOutput: false, + nativeTarget: stringFlag(argv, "native-target") ?? "all", + wasmTarget: stringFlag(argv, "wasm-target") ?? "all", + selectedTargets: stringSet(parseJsonFlag(argv, "selected-targets-json"), "--selected-targets-json"), + selectedProducts: stringSet(parseJsonFlag(argv, "selected-products-json"), "--selected-products-json"), + }; + const knownFlags = new Set([ + "--github-output", + "--native-target", + "--wasm-target", + "--selected-targets-json", + "--selected-products-json", + ]); + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + const name = value.includes("=") ? value.slice(0, value.indexOf("=")) : value; + if (name === "--github-output") { + options.githubOutput = true; + continue; + } + if (knownFlags.has(name)) { + if (!value.includes("=")) { + index += 1; + } + continue; + } + fail(PREFIX, `unknown argument ${value}`); + } + return options; +} + +function stringSet(value, label) { + if (value === undefined) { + return undefined; + } + if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) { + fail(PREFIX, `${label} must be a JSON string list`); + } + return new Set(value); +} + +function filterRuntimeMatrix(predicate, { nativeTarget = "all", selectedTargets = undefined, label }) { + let include = liboliphauntNativeRuntimeMatrix().include.filter((item) => predicate(item.target)); + if (nativeTarget !== "all") { + include = include.filter((item) => item.target === nativeTarget); + } + if (selectedTargets !== undefined) { + include = include.filter((item) => selectedTargets.has(item.target)); + } + if (include.length === 0) { + fail(PREFIX, `no published liboliphaunt-native ${label} targets matched the selected CI plan`); + } + return { include }; +} + +export function liboliphauntNativeRuntimeMatrix() { + const include = allArtifactTargets( + { + product: "liboliphaunt-native", + kind: "native-runtime", + publishedOnly: true, + }, + PREFIX, + ).map((target) => { + if (!target.runner) { + fail(PREFIX, `${target.id} must declare runner`); + } + return { + target: target.target, + runner: target.runner, + "build-root": liboliphauntNativeBuildRoot(target.target), + "ci-artifact-root": liboliphauntNativeCiArtifactRoot(target.target), + }; + }); + if (include.length === 0) { + fail(PREFIX, "no published liboliphaunt-native native-runtime targets"); + } + return { include }; +} + +export function liboliphauntNativeDesktopRuntimeMatrix(nativeTarget = "all", selectedTargets = undefined) { + return filterRuntimeMatrix((target) => /^(linux|macos|windows)-/u.test(target), { + nativeTarget, + selectedTargets, + label: "desktop", + }); +} + +export function liboliphauntNativeAndroidRuntimeMatrix(nativeTarget = "all", selectedTargets = undefined) { + return filterRuntimeMatrix((target) => target.startsWith("android-"), { + nativeTarget, + selectedTargets, + label: "Android", + }); +} + +export function liboliphauntNativeIosRuntimeMatrix(nativeTarget = "all", selectedTargets = undefined) { + return filterRuntimeMatrix((target) => target === "ios-xcframework", { + nativeTarget, + selectedTargets, + label: "iOS", + }); +} + +export function liboliphauntNativeRuntimeTargetsForSurface(surface) { + const targets = allArtifactTargets( + { + product: "liboliphaunt-native", + kind: "native-runtime", + surface, + publishedOnly: true, + }, + PREFIX, + ).map((target) => target.target); + if (targets.length === 0) { + fail(PREFIX, `no published liboliphaunt-native native-runtime targets for surface ${surface}`); + } + return targets.sort(compareText); +} + +export function reactNativeAndroidMobileAppMatrix(nativeTarget = "all", selectedTargets = undefined) { + const include = []; + for (const target of allArtifactTargets( + { + product: "liboliphaunt-native", + kind: "native-runtime", + surface: "react-native-android", + publishedOnly: true, + }, + PREFIX, + )) { + if (nativeTarget !== "all" && target.target !== nativeTarget) { + continue; + } + if (selectedTargets !== undefined && !selectedTargets.has(target.target)) { + continue; + } + include.push({ + target: target.target, + abi: liboliphauntAndroidAbi(target.target), + "build-root": liboliphauntNativeBuildRoot(target.target), + }); + } + if (include.length === 0) { + const validTargets = liboliphauntNativeRuntimeTargetsForSurface("react-native-android").join(", "); + fail(PREFIX, `no React Native Android app targets matched; expected one of: all, ${validTargets}`); + } + include.sort((left, right) => compareText(left.target, right.target)); + return { include }; +} + +export function extensionArtifactsNativeMatrix( + nativeTarget = "all", + selectedTargets = undefined, + selectedProducts = undefined, +) { + const runtimeTargets = new Map( + allArtifactTargets( + { + product: "liboliphaunt-native", + kind: "native-runtime", + publishedOnly: true, + }, + PREFIX, + ) + .filter((target) => target.extensionArtifacts) + .map((target) => [target.target, target]), + ); + const byTarget = new Map(); + for (const extensionTarget of extensionArtifactTargets({ family: "native", publishedOnly: true }, PREFIX)) { + if (selectedProducts !== undefined && !selectedProducts.has(extensionTarget.product)) { + continue; + } + if (nativeTarget !== "all" && extensionTarget.target !== nativeTarget) { + continue; + } + if (selectedTargets !== undefined && !selectedTargets.has(extensionTarget.target)) { + continue; + } + const runtimeTarget = runtimeTargets.get(extensionTarget.target); + if (!runtimeTarget) { + fail( + PREFIX, + `${extensionTarget.product} declares native extension target ${extensionTarget.target}, but liboliphaunt-native does not publish it`, + ); + } + if (!runtimeTarget.runner) { + fail(PREFIX, `${runtimeTarget.id} must declare runner`); + } + const group = + byTarget.get(extensionTarget.target) ?? + { + target: extensionTarget.target, + runner: runtimeTarget.runner, + buildRoot: liboliphauntNativeBuildRoot(extensionTarget.target), + ciArtifactRoot: liboliphauntNativeCiArtifactRoot(extensionTarget.target), + extensions: new Set(), + sqlNames: new Set(), + }; + group.extensions.add(extensionTarget.product); + group.sqlNames.add(extensionTarget.sqlName); + byTarget.set(extensionTarget.target, group); + } + const include = [...byTarget.values()].map((group) => { + const extensions = [...group.extensions].sort(compareText); + const sqlNames = [...group.sqlNames].sort(compareText); + return { + extensions_csv: extensions.join(","), + sql_names_csv: sqlNames.join(","), + extension_count: String(extensions.length), + target: group.target, + runner: group.runner, + "build-root": group.buildRoot, + "ci-artifact-root": group.ciArtifactRoot, + }; + }); + if (include.length === 0) { + const validTargets = publishedExtensionTargetIds({ family: "native" }, PREFIX).join(", "); + fail(PREFIX, `unknown native extension artifact target ${nativeTarget}; expected one of: all, ${validTargets}`); + } + include.sort((left, right) => compareText(left.target, right.target)); + return { include }; +} + +export function extensionArtifactsWasixMatrix(wasmTarget = "all", selectedProducts = undefined) { + const byTarget = new Map(); + const extensionTargets = extensionArtifactTargets({ family: "wasix", publishedOnly: true }, PREFIX); + for (const target of allArtifactTargets( + { + product: "liboliphaunt-wasix", + publishedOnly: true, + }, + PREFIX, + )) { + if (target.kind !== "wasix-runtime") { + continue; + } + const extensionTargetId = target.target === "portable" ? "wasix-portable" : target.target; + if (wasmTarget !== "all" && target.target !== wasmTarget) { + continue; + } + for (const declared of extensionTargets) { + if (selectedProducts !== undefined && !selectedProducts.has(declared.product)) { + continue; + } + if (declared.target !== extensionTargetId) { + continue; + } + const group = + byTarget.get(declared.target) ?? + { + target: declared.target, + runner: target.runner ?? "ubuntu-latest", + runtimeKind: target.kind, + triple: target.triple ?? "", + extensions: new Set(), + sqlNames: new Set(), + }; + group.extensions.add(declared.product); + group.sqlNames.add(declared.sqlName); + byTarget.set(declared.target, group); + } + } + const include = [...byTarget.values()].map((group) => { + const extensions = [...group.extensions].sort(compareText); + const sqlNames = [...group.sqlNames].sort(compareText); + return { + extensions_csv: extensions.join(","), + sql_names_csv: sqlNames.join(","), + extension_count: String(extensions.length), + target: group.target, + runner: group.runner, + "runtime-kind": group.runtimeKind, + triple: group.triple, + }; + }); + if (include.length === 0) { + const validTargets = allArtifactTargets( + { + product: "liboliphaunt-wasix", + publishedOnly: true, + }, + PREFIX, + ) + .filter((target) => target.kind === "wasix-runtime") + .map((target) => target.target) + .join(", "); + fail(PREFIX, `unknown WASIX extension artifact target ${wasmTarget}; expected one of: all, ${validTargets}`); + } + include.sort((left, right) => compareText(left.target, right.target)); + return { include }; +} + +export function liboliphauntWasixAotRuntimeMatrix(wasmTarget = "all") { + const include = []; + for (const target of allArtifactTargets( + { + product: "liboliphaunt-wasix", + kind: "wasix-aot-runtime", + publishedOnly: true, + }, + PREFIX, + )) { + if (wasmTarget !== "all" && !new Set([target.target, target.triple]).has(wasmTarget)) { + continue; + } + if (!target.runner) { + fail(PREFIX, `${target.id} must declare runner`); + } + if (!target.triple) { + fail(PREFIX, `${target.id} must declare triple`); + } + if (!target.llvmUrl) { + fail(PREFIX, `${target.id} must declare llvm_url`); + } + include.push({ + os: target.runner, + target: target.triple, + target_id: target.target, + package: `liboliphaunt-wasix-aot-${target.triple}`, + artifact: `liboliphaunt-wasix-runtime-aot-${target.target}`, + llvm_url: target.llvmUrl, + }); + } + if (include.length === 0) { + const validTargets = allArtifactTargets( + { + product: "liboliphaunt-wasix", + kind: "wasix-aot-runtime", + publishedOnly: true, + }, + PREFIX, + ) + .map((target) => target.target) + .join(", "); + fail(PREFIX, `unknown WASIX AOT runtime target ${wasmTarget}; expected one of: all, ${validTargets}`); + } + include.sort((left, right) => compareText(left.target_id, right.target_id)); + return { include }; +} + +export function brokerRuntimeMatrix(nativeTarget = "all") { + const matrix = { + include: allArtifactTargets( + { + product: "oliphaunt-broker", + kind: "broker-helper", + publishedOnly: true, + }, + PREFIX, + ).map((target) => { + if (!target.runner) { + fail(PREFIX, `${target.id} must declare runner`); + } + return { + target: target.target, + runner: target.runner, + }; + }), + }; + return filterDesktopRuntimeMatrix(matrix, nativeTarget, "broker"); +} + +export function nodeDirectRuntimeMatrix(nativeTarget = "all") { + const matrix = { + include: allArtifactTargets( + { + product: "oliphaunt-node-direct", + kind: "node-direct-addon", + publishedOnly: true, + }, + PREFIX, + ).map((target) => { + if (!target.runner) { + fail(PREFIX, `${target.id} must declare runner`); + } + return { + target: target.target, + runner: target.runner, + }; + }), + }; + return filterDesktopRuntimeMatrix(matrix, nativeTarget, "Node direct"); +} + +function filterDesktopRuntimeMatrix(matrix, nativeTarget, label) { + if (matrix.include.length === 0) { + fail(PREFIX, `no published ${label} targets`); + } + if (nativeTarget === "all") { + return matrix; + } + const include = matrix.include.filter((target) => target.target === nativeTarget); + if (include.length === 0) { + const validTargets = matrix.include.map((target) => target.target).join(", "); + fail(PREFIX, `unknown ${label} target ${nativeTarget}; expected one of: all, ${validTargets}`); + } + return { include }; +} + +function matrixByName(name, options) { + switch (name) { + case "liboliphaunt-native-runtime": + return liboliphauntNativeRuntimeMatrix(); + case "liboliphaunt-native-desktop-runtime": + return liboliphauntNativeDesktopRuntimeMatrix(options.nativeTarget, options.selectedTargets); + case "liboliphaunt-native-android-runtime": + return liboliphauntNativeAndroidRuntimeMatrix(options.nativeTarget, options.selectedTargets); + case "liboliphaunt-native-ios-runtime": + return liboliphauntNativeIosRuntimeMatrix(options.nativeTarget, options.selectedTargets); + case "react-native-android-mobile-app": + return reactNativeAndroidMobileAppMatrix(options.nativeTarget, options.selectedTargets); + case "extension-artifacts-native": + return extensionArtifactsNativeMatrix(options.nativeTarget, options.selectedTargets, options.selectedProducts); + case "extension-artifacts-wasix": + return extensionArtifactsWasixMatrix(options.wasmTarget, options.selectedProducts); + case "liboliphaunt-wasix-aot-runtime": + return liboliphauntWasixAotRuntimeMatrix(options.wasmTarget); + case "broker-runtime": + return brokerRuntimeMatrix(options.nativeTarget); + case "node-direct-runtime": + return nodeDirectRuntimeMatrix(options.nativeTarget); + default: + fail(PREFIX, `unknown matrix ${name}`); + } +} + +function emitGithubOutput(name, value) { + const rendered = JSON.stringify(sortedValue(value)); + const outputPath = process.env.GITHUB_OUTPUT; + if (outputPath) { + appendFileSync(outputPath, `${name}=${rendered}\n`, "utf8"); + } + console.log(`${name}=${rendered}`); +} + +function usage() { + return `usage: tools/release/artifact_target_matrix.mjs [options] + +Matrices: + liboliphaunt-native-runtime + liboliphaunt-native-desktop-runtime + liboliphaunt-native-android-runtime + liboliphaunt-native-ios-runtime + react-native-android-mobile-app + extension-artifacts-native + extension-artifacts-wasix + liboliphaunt-wasix-aot-runtime + broker-runtime + node-direct-runtime + +Options: + --github-output + --native-target TARGET + --wasm-target TARGET + --selected-targets-json JSON + --selected-products-json JSON + --surface SURFACE +`; +} + +function main(argv) { + const [command, ...rest] = argv; + if (!command || command === "--help" || command === "-h") { + console.log(usage()); + return; + } + if (command === "exact-extension-products") { + printJson(exactExtensionProducts(PREFIX)); + return; + } + if (command === "runtime-targets-for-surface") { + const surface = stringFlag(rest, "surface"); + if (!surface) { + fail(PREFIX, "runtime-targets-for-surface requires --surface"); + } + printJson(liboliphauntNativeRuntimeTargetsForSurface(surface)); + return; + } + const options = parseOptions(rest); + const matrix = matrixByName(command, options); + if (options.githubOutput) { + emitGithubOutput("matrix", matrix); + } else { + printJson(matrix); + } +} + +if (import.meta.main) { + main(Bun.argv.slice(2)); +} diff --git a/tools/release/artifact_target_matrix.py b/tools/release/artifact_target_matrix.py deleted file mode 100755 index 7bf61349..00000000 --- a/tools/release/artifact_target_matrix.py +++ /dev/null @@ -1,440 +0,0 @@ -#!/usr/bin/env python3 -"""Emit GitHub Actions matrices derived from release artifact targets.""" - -from __future__ import annotations - -import argparse -from dataclasses import dataclass, field -import json -import os -from pathlib import Path -from typing import Iterable - -import artifact_targets -import extension_artifact_targets -import product_metadata - - -@dataclass -class ExtensionTargetGroup: - target: str - runner: str - extensions: set[str] = field(default_factory=set) - sql_names: set[str] = field(default_factory=set) - build_root: str | None = None - ci_artifact_root: str | None = None - runtime_kind: str | None = None - triple: str | None = None - - -def build_root_for_liboliphaunt_target(target_id: str) -> str: - return artifact_targets.liboliphaunt_native_build_root(target_id) - - -def ci_artifact_root_for_liboliphaunt_target(target_id: str) -> str: - return artifact_targets.liboliphaunt_native_ci_artifact_root(target_id) - - -def liboliphaunt_native_runtime_matrix() -> dict[str, list[dict[str, str]]]: - include: list[dict[str, str]] = [] - for target in artifact_targets.artifact_targets( - product="liboliphaunt-native", - kind="native-runtime", - published_only=True, - ): - if target.runner is None: - product_metadata.fail(f"{target.id} must declare runner") - include.append( - { - "target": target.target, - "runner": target.runner, - "build-root": build_root_for_liboliphaunt_target(target.target), - "ci-artifact-root": ci_artifact_root_for_liboliphaunt_target(target.target), - } - ) - if not include: - product_metadata.fail("no published liboliphaunt-native native-runtime targets") - return {"include": include} - - -def _filtered_liboliphaunt_native_runtime_matrix( - predicate, - *, - native_target: str = "all", - selected_targets: set[str] | None = None, - label: str, -) -> dict[str, list[dict[str, str]]]: - include = [ - item - for item in liboliphaunt_native_runtime_matrix()["include"] - if predicate(item["target"]) - ] - if native_target != "all": - include = [item for item in include if item["target"] == native_target] - if selected_targets is not None: - include = [item for item in include if item["target"] in selected_targets] - if not include: - product_metadata.fail(f"no published liboliphaunt-native {label} targets matched the selected CI plan") - return {"include": include} - - -def liboliphaunt_native_desktop_runtime_matrix( - *, - native_target: str = "all", - selected_targets: set[str] | None = None, -) -> dict[str, list[dict[str, str]]]: - return _filtered_liboliphaunt_native_runtime_matrix( - lambda target: target.startswith(("linux-", "macos-", "windows-")), - native_target=native_target, - selected_targets=selected_targets, - label="desktop", - ) - - -def liboliphaunt_native_android_runtime_matrix( - *, - native_target: str = "all", - selected_targets: set[str] | None = None, -) -> dict[str, list[dict[str, str]]]: - return _filtered_liboliphaunt_native_runtime_matrix( - lambda target: target.startswith("android-"), - native_target=native_target, - selected_targets=selected_targets, - label="Android", - ) - - -def liboliphaunt_native_ios_runtime_matrix( - *, - native_target: str = "all", - selected_targets: set[str] | None = None, -) -> dict[str, list[dict[str, str]]]: - return _filtered_liboliphaunt_native_runtime_matrix( - lambda target: target == "ios-xcframework", - native_target=native_target, - selected_targets=selected_targets, - label="iOS", - ) - - -def extension_artifacts_native_matrix( - native_target: str = "all", - selected_targets: set[str] | None = None, - selected_products: set[str] | None = None, -) -> dict[str, list[dict[str, str]]]: - by_target: dict[str, ExtensionTargetGroup] = {} - runtime_targets = { - target.target: target - for target in artifact_targets.artifact_targets( - product="liboliphaunt-native", - kind="native-runtime", - published_only=True, - ) - if target.extension_artifacts - } - for extension_target in extension_artifact_targets.artifact_targets( - family="native", - published_only=True, - ): - if selected_products is not None and extension_target.product not in selected_products: - continue - target_id = extension_target.target - if native_target != "all" and target_id != native_target: - continue - if selected_targets is not None and target_id not in selected_targets: - continue - runtime_target = runtime_targets.get(target_id) - if runtime_target is None: - product_metadata.fail(f"{extension_target.product} declares native extension target {target_id}, but liboliphaunt-native does not publish it") - if runtime_target.runner is None: - product_metadata.fail(f"{runtime_target.id} must declare runner") - grouped = by_target.setdefault( - target_id, - ExtensionTargetGroup( - target=target_id, - runner=runtime_target.runner, - build_root=build_root_for_liboliphaunt_target(target_id), - ci_artifact_root=ci_artifact_root_for_liboliphaunt_target(target_id), - ), - ) - grouped.extensions.add(extension_target.product) - grouped.sql_names.add(extension_target.sql_name) - include: list[dict[str, str]] = [] - for item in by_target.values(): - extensions = sorted(item.extensions) - sql_names = sorted(item.sql_names) - if item.build_root is None or item.ci_artifact_root is None: - raise AssertionError(f"native extension group {item.target} is missing native build metadata") - include.append( - { - "extensions_csv": ",".join(extensions), - "sql_names_csv": ",".join(sql_names), - "extension_count": str(len(extensions)), - "target": item.target, - "runner": item.runner, - "build-root": item.build_root, - "ci-artifact-root": item.ci_artifact_root, - } - ) - if not include: - valid_targets = ", ".join(extension_artifact_targets.published_target_ids(family="native")) - product_metadata.fail(f"unknown native extension artifact target {native_target}; expected one of: all, {valid_targets}") - include.sort(key=lambda item: item["target"]) - return {"include": include} - - -def liboliphaunt_native_runtime_targets_for_surface(surface: str) -> list[str]: - targets = [ - target.target - for target in artifact_targets.artifact_targets( - product="liboliphaunt-native", - kind="native-runtime", - surface=surface, - published_only=True, - ) - ] - if not targets: - product_metadata.fail(f"no published liboliphaunt-native native-runtime targets for surface {surface}") - return sorted(targets) - - -def react_native_android_mobile_app_matrix( - *, - native_target: str = "all", - selected_targets: set[str] | None = None, -) -> dict[str, list[dict[str, str]]]: - include: list[dict[str, str]] = [] - for target in artifact_targets.artifact_targets( - product="liboliphaunt-native", - kind="native-runtime", - surface="react-native-android", - published_only=True, - ): - if native_target != "all" and target.target != native_target: - continue - if selected_targets is not None and target.target not in selected_targets: - continue - abi = artifact_targets.liboliphaunt_android_abi(target.target) - include.append( - { - "target": target.target, - "abi": abi, - "build-root": build_root_for_liboliphaunt_target(target.target), - } - ) - if not include: - valid_targets = ", ".join(liboliphaunt_native_runtime_targets_for_surface("react-native-android")) - product_metadata.fail(f"no React Native Android app targets matched; expected one of: all, {valid_targets}") - include.sort(key=lambda item: item["target"]) - return {"include": include} - - -def extension_artifacts_wasix_matrix( - wasm_target: str = "all", - selected_products: set[str] | None = None, -) -> dict[str, list[dict[str, str]]]: - by_target: dict[str, ExtensionTargetGroup] = {} - extension_targets = extension_artifact_targets.artifact_targets(family="wasix", published_only=True) - for target in artifact_targets.artifact_targets( - product="liboliphaunt-wasix", - published_only=True, - ): - if target.kind != "wasix-runtime": - continue - extension_target = "wasix-portable" if target.target == "portable" else target.target - if wasm_target != "all" and target.target != wasm_target: - continue - for declared in extension_targets: - if selected_products is not None and declared.product not in selected_products: - continue - if declared.target != extension_target: - continue - grouped = by_target.setdefault( - declared.target, - ExtensionTargetGroup( - target=declared.target, - runner=target.runner or "ubuntu-latest", - runtime_kind=target.kind, - triple=target.triple or "", - ), - ) - grouped.extensions.add(declared.product) - grouped.sql_names.add(declared.sql_name) - include: list[dict[str, str]] = [] - for item in by_target.values(): - extensions = sorted(item.extensions) - sql_names = sorted(item.sql_names) - if item.runtime_kind is None or item.triple is None: - raise AssertionError(f"WASIX extension group {item.target} is missing runtime metadata") - include.append( - { - "extensions_csv": ",".join(extensions), - "sql_names_csv": ",".join(sql_names), - "extension_count": str(len(extensions)), - "target": item.target, - "runner": item.runner, - "runtime-kind": item.runtime_kind, - "triple": item.triple, - } - ) - if not include: - valid_targets = ", ".join( - target.target - for target in artifact_targets.artifact_targets( - product="liboliphaunt-wasix", - published_only=True, - ) - if target.kind == "wasix-runtime" - ) - product_metadata.fail(f"unknown WASIX extension artifact target {wasm_target}; expected one of: all, {valid_targets}") - include.sort(key=lambda item: item["target"]) - return {"include": include} - - -def liboliphaunt_wasix_aot_runtime_matrix(wasm_target: str = "all") -> dict[str, list[dict[str, str]]]: - include: list[dict[str, str]] = [] - for target in artifact_targets.artifact_targets( - product="liboliphaunt-wasix", - kind="wasix-aot-runtime", - published_only=True, - ): - if wasm_target != "all" and wasm_target not in {target.target, target.triple}: - continue - if target.runner is None: - product_metadata.fail(f"{target.id} must declare runner") - if target.triple is None: - product_metadata.fail(f"{target.id} must declare triple") - if target.llvm_url is None: - product_metadata.fail(f"{target.id} must declare llvm_url") - include.append( - { - "os": target.runner, - "target": target.triple, - "target_id": target.target, - "package": f"liboliphaunt-wasix-aot-{target.triple}", - "artifact": f"liboliphaunt-wasix-runtime-aot-{target.target}", - "llvm_url": target.llvm_url, - } - ) - if not include: - valid_targets = ", ".join( - target.target - for target in artifact_targets.artifact_targets( - product="liboliphaunt-wasix", - kind="wasix-aot-runtime", - published_only=True, - ) - ) - product_metadata.fail(f"unknown WASIX AOT runtime target {wasm_target}; expected one of: all, {valid_targets}") - include.sort(key=lambda item: item["target_id"]) - return {"include": include} - - -def exact_extension_products() -> list[str]: - return sorted({target.product for target in extension_artifact_targets.artifact_targets()}) - - -def broker_runtime_matrix() -> dict[str, list[dict[str, str]]]: - include: list[dict[str, str]] = [] - for target in artifact_targets.artifact_targets( - product="oliphaunt-broker", - kind="broker-helper", - published_only=True, - ): - if target.runner is None: - product_metadata.fail(f"{target.id} must declare runner") - include.append( - { - "target": target.target, - "runner": target.runner, - } - ) - if not include: - product_metadata.fail("no published oliphaunt-broker helper targets") - return {"include": include} - - -def node_direct_runtime_matrix() -> dict[str, list[dict[str, str]]]: - include: list[dict[str, str]] = [] - for target in artifact_targets.artifact_targets( - product="oliphaunt-node-direct", - kind="node-direct-addon", - published_only=True, - ): - if target.runner is None: - product_metadata.fail(f"{target.id} must declare runner") - include.append( - { - "target": target.target, - "runner": target.runner, - } - ) - if not include: - product_metadata.fail("no published oliphaunt-node-direct targets") - return {"include": include} - - -def emit_github_output(name: str, value: object) -> None: - rendered = json.dumps(value, sort_keys=True, separators=(",", ":")) - output_path = os.environ.get("GITHUB_OUTPUT") - if output_path: - with Path(output_path).open("a", encoding="utf-8") as handle: - print(f"{name}={rendered}", file=handle) - print(f"{name}={rendered}") - - -def main(argv: Iterable[str] | None = None) -> int: - parser = argparse.ArgumentParser() - parser.add_argument( - "matrix", - choices=[ - "liboliphaunt-native-runtime", - "liboliphaunt-native-desktop-runtime", - "liboliphaunt-native-android-runtime", - "liboliphaunt-native-ios-runtime", - "react-native-android-mobile-app", - "extension-artifacts-native", - "extension-artifacts-wasix", - "liboliphaunt-wasix-aot-runtime", - "broker-runtime", - "node-direct-runtime", - ], - help="matrix shape to emit", - ) - parser.add_argument("--github-output", action="store_true", help="write matrix=... to $GITHUB_OUTPUT") - args = parser.parse_args(list(argv) if argv is not None else None) - - product_metadata.load_graph() - match args.matrix: - case "liboliphaunt-native-runtime": - matrix = liboliphaunt_native_runtime_matrix() - case "liboliphaunt-native-desktop-runtime": - matrix = liboliphaunt_native_desktop_runtime_matrix() - case "liboliphaunt-native-android-runtime": - matrix = liboliphaunt_native_android_runtime_matrix() - case "liboliphaunt-native-ios-runtime": - matrix = liboliphaunt_native_ios_runtime_matrix() - case "react-native-android-mobile-app": - matrix = react_native_android_mobile_app_matrix() - case "extension-artifacts-native": - matrix = extension_artifacts_native_matrix() - case "extension-artifacts-wasix": - matrix = extension_artifacts_wasix_matrix() - case "liboliphaunt-wasix-aot-runtime": - matrix = liboliphaunt_wasix_aot_runtime_matrix() - case "broker-runtime": - matrix = broker_runtime_matrix() - case "node-direct-runtime": - matrix = node_direct_runtime_matrix() - case _: - raise AssertionError(args.matrix) - - if args.github_output: - emit_github_output("matrix", matrix) - else: - print(json.dumps(matrix, indent=2, sort_keys=True)) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py index 74f60284..55cdcc7b 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -3,12 +3,13 @@ from __future__ import annotations +import json +import subprocess import sys import tomllib from pathlib import Path from typing import NoReturn -import artifact_target_matrix import artifact_targets import extension_artifact_targets import product_metadata @@ -40,6 +41,18 @@ def read_toml(path: Path) -> dict: return data +def bun_json(args: list[str]) -> object: + output = subprocess.check_output(["tools/dev/bun.sh", *args], cwd=ROOT, text=True) + return json.loads(output) + + +def artifact_target_matrix(matrix: str) -> dict[str, list[dict[str, str]]]: + value = bun_json(["tools/release/artifact_target_matrix.mjs", matrix]) + if not isinstance(value, dict) or not isinstance(value.get("include"), list): + fail(f"{matrix} matrix query did not return a matrix object") + return value + + def ts_template(asset: str) -> str: return asset.replace("{version}", "${version}") @@ -1090,7 +1103,7 @@ def validate_target_matrices() -> None: ): require_text( "tools/graph/ci_plan.py", - f"artifact_target_matrix.{helper}", + "tools/release/artifact_target_matrix.mjs", f"CI affected planner must derive {helper} from release metadata artifact targets", ) if "broker_runtime_matrix" not in ci or "fromJson(needs.affected.outputs.broker_runtime_matrix)" not in ci: @@ -1146,10 +1159,10 @@ def validate_target_matrices() -> None: fail("release workflow must not define separate native asset builder jobs; CI owns runtime/helper artifacts") if "artifact_target_matrix.py native-release-hosts" in release: fail("release workflow must not use the removed native-release-hosts matrix") - if "artifact_target_matrix" not in planner: - fail("shared affected planner must import the release artifact target matrix helper") + if "tools/release/artifact_target_matrix.mjs" not in planner: + fail("shared affected planner must query the release artifact target matrix helper") - liboliphaunt_matrix = artifact_target_matrix.liboliphaunt_native_runtime_matrix() + liboliphaunt_matrix = artifact_target_matrix("liboliphaunt-native-runtime") liboliphaunt_targets = {item["target"] for item in liboliphaunt_matrix["include"]} expected_liboliphaunt_targets = { target.target @@ -1165,7 +1178,7 @@ def validate_target_matrices() -> None: f"{sorted(liboliphaunt_targets)} vs {sorted(expected_liboliphaunt_targets)}" ) - extension_native_matrix = artifact_target_matrix.extension_artifacts_native_matrix() + extension_native_matrix = artifact_target_matrix("extension-artifacts-native") extension_native_pairs = { (product, item["target"]) for item in extension_native_matrix["include"] @@ -1182,7 +1195,7 @@ def validate_target_matrices() -> None: f"{sorted(extension_native_pairs)} vs {sorted(expected_extension_native_pairs)}" ) - broker_matrix = artifact_target_matrix.broker_runtime_matrix() + broker_matrix = artifact_target_matrix("broker-runtime") broker_targets = {item["target"] for item in broker_matrix["include"]} expected_broker_targets = { target.target @@ -1198,7 +1211,7 @@ def validate_target_matrices() -> None: f"{sorted(broker_targets)} vs {sorted(expected_broker_targets)}" ) - node_direct_matrix = artifact_target_matrix.node_direct_runtime_matrix() + node_direct_matrix = artifact_target_matrix("node-direct-runtime") node_direct_targets = {item["target"] for item in node_direct_matrix["include"]} expected_node_direct_targets = { target.target @@ -1214,7 +1227,7 @@ def validate_target_matrices() -> None: f"{sorted(node_direct_targets)} vs {sorted(expected_node_direct_targets)}" ) - extension_wasix_matrix = artifact_target_matrix.extension_artifacts_wasix_matrix() + extension_wasix_matrix = artifact_target_matrix("extension-artifacts-wasix") extension_wasix_pairs = { (product, item["target"]) for item in extension_wasix_matrix["include"] diff --git a/tools/release/release-artifact-targets.mjs b/tools/release/release-artifact-targets.mjs index 7852d2d9..d481b472 100644 --- a/tools/release/release-artifact-targets.mjs +++ b/tools/release/release-artifact-targets.mjs @@ -1,37 +1,101 @@ +import { existsSync, readFileSync } from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; -import { runMoon } from "../policy/moon.mjs"; +import { loadGraph } from "./release-graph.mjs"; export const ROOT = path.resolve(import.meta.dir, "../.."); -const DESKTOP_TARGETS = { +export const DESKTOP_TARGETS = { "linux-arm64-gnu": { + triple: "aarch64-unknown-linux-gnu", + runner: "ubuntu-24.04-arm", archive: "tar.gz", - brokerExecutable: "bin/oliphaunt-broker", - nodeDirectLibrary: "oliphaunt_node.node", + npmOs: "linux", + npmCpu: "arm64", + npmLibc: "glibc", + liboliphauntNpmPackage: "@oliphaunt/liboliphaunt-linux-arm64-gnu", + liboliphauntToolsNpmPackage: "@oliphaunt/tools-linux-arm64-gnu", + brokerNpmPackage: "@oliphaunt/broker-linux-arm64-gnu", + nodePackage: "@oliphaunt/node-direct-linux-arm64-gnu", + wasixLlvmUrl: "https://github.com/wasmerio/llvm-custom-builds/releases/download/22.x/llvm-linux-aarch64.tar.xz", }, "linux-x64-gnu": { + triple: "x86_64-unknown-linux-gnu", + runner: "ubuntu-latest", archive: "tar.gz", - brokerExecutable: "bin/oliphaunt-broker", - nodeDirectLibrary: "oliphaunt_node.node", + npmOs: "linux", + npmCpu: "x64", + npmLibc: "glibc", + liboliphauntNpmPackage: "@oliphaunt/liboliphaunt-linux-x64-gnu", + liboliphauntToolsNpmPackage: "@oliphaunt/tools-linux-x64-gnu", + brokerNpmPackage: "@oliphaunt/broker-linux-x64-gnu", + nodePackage: "@oliphaunt/node-direct-linux-x64-gnu", + wasixLlvmUrl: "https://github.com/wasmerio/llvm-custom-builds/releases/download/22.x/llvm-linux-amd64.tar.xz", }, "macos-arm64": { + triple: "aarch64-apple-darwin", + runner: "macos-latest", + archive: "tar.gz", + npmOs: "darwin", + npmCpu: "arm64", + liboliphauntNpmPackage: "@oliphaunt/liboliphaunt-darwin-arm64", + liboliphauntToolsNpmPackage: "@oliphaunt/tools-darwin-arm64", + brokerNpmPackage: "@oliphaunt/broker-darwin-arm64", + nodePackage: "@oliphaunt/node-direct-darwin-arm64", + wasixLlvmUrl: "https://github.com/wasmerio/llvm-custom-builds/releases/download/22.x/llvm-darwin-aarch64.tar.xz", + }, + "macos-x64": { + triple: "x86_64-apple-darwin", + runner: "macos-latest", archive: "tar.gz", - brokerExecutable: "bin/oliphaunt-broker", - nodeDirectLibrary: "oliphaunt_node.node", }, "windows-x64-msvc": { + triple: "x86_64-pc-windows-msvc", + runner: "windows-latest", archive: "zip", - brokerExecutable: "bin/oliphaunt-broker.exe", - nodeDirectLibrary: "oliphaunt_node.node", + npmOs: "win32", + npmCpu: "x64", + liboliphauntNpmPackage: "@oliphaunt/liboliphaunt-win32-x64-msvc", + liboliphauntToolsNpmPackage: "@oliphaunt/tools-win32-x64-msvc", + brokerNpmPackage: "@oliphaunt/broker-win32-x64-msvc", + nodePackage: "@oliphaunt/node-direct-win32-x64-msvc", + wasixLlvmUrl: "https://github.com/wasmerio/llvm-custom-builds/releases/download/22.x/llvm-windows-amd64.tar.xz", }, }; +export const MOBILE_TARGETS = { + "android-arm64-v8a": { + triple: "aarch64-linux-android", + runner: "ubuntu-latest", + androidAbi: "arm64-v8a", + }, + "android-x86_64": { + triple: "x86_64-linux-android", + runner: "ubuntu-latest", + androidAbi: "x86_64", + }, + "ios-xcframework": { + triple: "ios-xcframework", + runner: "macos-26", + }, +}; + +const NATIVE_RUNTIME_TARGETS = { ...DESKTOP_TARGETS, ...MOBILE_TARGETS }; +const WASIX_TARGETS = new Set(["portable", "linux-arm64-gnu", "linux-x64-gnu", "macos-arm64", "windows-x64-msvc"]); +const BROKER_TARGETS = new Set(["linux-arm64-gnu", "linux-x64-gnu", "macos-arm64", "windows-x64-msvc"]); +const NODE_DIRECT_TARGETS = BROKER_TARGETS; const PRODUCT_PRESETS = { + "liboliphaunt-native": "liboliphaunt-native", + "liboliphaunt-wasix": "liboliphaunt-wasix", "oliphaunt-broker": "broker-helper", "oliphaunt-node-direct": "node-direct-addon", }; +const EXTENSION_FAMILIES = new Set(["native", "wasix"]); +const EXTENSION_KINDS = new Set(["native-dynamic", "native-static-registry", "wasix-runtime"]); +const EXTENSION_STATUSES = new Set(["supported", "planned", "unsupported"]); + +const graphCache = new Map(); export function fail(prefix, message) { console.error(`${prefix}: ${message}`); @@ -43,11 +107,502 @@ export function compareText(left, right) { } export function rel(file) { - return path.relative(ROOT, file).split(path.sep).join("/"); + const relative = path.relative(ROOT, file); + return relative.startsWith("..") ? file : relative.split(path.sep).join("/"); +} + +function graph(prefix) { + if (!graphCache.has(prefix)) { + graphCache.set(prefix, loadGraph(prefix)); + } + return graphCache.get(prefix); +} + +function archiveAsset(productPrefix, target, archive) { + return `${productPrefix}-{version}-${target}.${archive}`; +} + +function assertStringList(value, label, prefix) { + if (!Array.isArray(value) || !value.every((item) => typeof item === "string" && item)) { + fail(prefix, `${label} must be a non-empty string list`); + } + return value; +} + +function artifactTargetConfig(product, expectedPreset, prefix) { + const release = releaseMetadata(product, prefix); + const config = release.artifactTargets; + if (typeof config !== "object" || config === null || Array.isArray(config)) { + fail(prefix, `Moon release metadata for ${product} must declare artifactTargets`); + } + if (config.preset !== expectedPreset) { + fail(prefix, `Moon release metadata for ${product} artifactTargets.preset must be ${expectedPreset}`); + } + return config; +} + +function publishedTargets(product, expectedPreset, knownTargets, prefix) { + const config = artifactTargetConfig(product, expectedPreset, prefix); + const targets = assertStringList(config.publishedTargets ?? [], `${product}.publishedTargets`, prefix); + const duplicates = [...new Set(targets.filter((target, index) => targets.indexOf(target) !== index))]; + if (duplicates.length > 0) { + fail(prefix, `Moon release metadata for ${product} artifactTargets.publishedTargets contains duplicates`); + } + const unknown = targets.filter((target) => !knownTargets.has(target)).sort(compareText); + if (unknown.length > 0) { + fail(prefix, `Moon release metadata for ${product} declares unknown artifact target(s): ${unknown.join(", ")}`); + } + return targets; +} + +function plannedTargets(product, expectedPreset, knownTargets, prefix) { + const value = artifactTargetConfig(product, expectedPreset, prefix).plannedTargets ?? {}; + if (typeof value !== "object" || value === null || Array.isArray(value)) { + fail(prefix, `Moon release metadata for ${product} artifactTargets.plannedTargets must be an object`); + } + const parsed = new Map(); + for (const [target, details] of Object.entries(value)) { + if (!knownTargets.has(target)) { + fail(prefix, `Moon release metadata for ${product} declares unknown planned artifact target ${target}`); + } + const reason = details?.unsupportedReason; + if (typeof reason !== "string" || reason.trim().length < 40) { + fail(prefix, `Moon release metadata for ${product} planned target ${target} must declare a concrete unsupportedReason`); + } + parsed.set(target, details); + } + return parsed; +} + +function nativeLibraryRelativePath(target) { + if (target.startsWith("android-")) { + return `jni/${MOBILE_TARGETS[target].androidAbi}/liboliphaunt.so`; + } + if (target === "ios-xcframework") { + return "liboliphaunt.xcframework"; + } + if (target.startsWith("macos-")) { + return "lib/liboliphaunt.dylib"; + } + if (target.startsWith("linux-")) { + return "lib/liboliphaunt.so"; + } + if (target === "windows-x64-msvc") { + return "bin/oliphaunt.dll"; + } + fail("release-artifact-targets.mjs", `unsupported liboliphaunt native target ${target}`); +} + +function nativeSurfaces(target) { + if (target.startsWith("android-")) { + return ["github-release", "maven", "react-native-android"]; + } + if (target === "ios-xcframework") { + return ["github-release", "swiftpm", "react-native-ios"]; + } + return ["github-release", "rust-native-direct", "typescript-native-direct"]; +} + +export function liboliphauntNativeBuildRoot(target) { + if (!(target in NATIVE_RUNTIME_TARGETS)) { + fail("release-artifact-targets.mjs", `unknown liboliphaunt-native target ${target}`); + } + const roots = { + "macos-arm64": "target/liboliphaunt-pg18", + "android-arm64-v8a": "target/liboliphaunt-pg18-android-arm64", + "android-x86_64": "target/liboliphaunt-pg18-android-x86_64", + "ios-xcframework": "target/liboliphaunt-ios-xcframework", + }; + return roots[target] ?? `target/liboliphaunt-pg18-${target}`; +} + +export function liboliphauntNativeCiArtifactRoot(target) { + if (!(target in NATIVE_RUNTIME_TARGETS)) { + fail("release-artifact-targets.mjs", `unknown liboliphaunt-native target ${target}`); + } + return `target/liboliphaunt-native-ci/${target}`; +} + +export function liboliphauntAndroidAbi(target) { + const abi = MOBILE_TARGETS[target]?.androidAbi; + if (!abi) { + fail("release-artifact-targets.mjs", `unsupported React Native Android runtime target ${target}`); + } + return abi; +} + +function liboliphauntNativeRows(prefix) { + const product = "liboliphaunt-native"; + const published = new Set( + publishedTargets(product, PRODUCT_PRESETS[product], new Set(Object.keys(NATIVE_RUNTIME_TARGETS)), prefix), + ); + const planned = plannedTargets(product, PRODUCT_PRESETS[product], new Set(Object.keys(NATIVE_RUNTIME_TARGETS)), prefix); + const rows = []; + for (const target of [...new Set([...published, ...planned.keys()])].sort(compareText)) { + const platform = NATIVE_RUNTIME_TARGETS[target]; + const publishedTarget = published.has(target); + const row = { + id: `${product}.${target}`, + product, + kind: "native-runtime", + target, + triple: platform.triple, + runner: platform.runner, + asset: archiveAsset("liboliphaunt", target, platform.archive ?? "tar.gz"), + library_relative_path: nativeLibraryRelativePath(target), + npm_package: platform.liboliphauntNpmPackage, + npm_os: platform.npmOs, + npm_cpu: platform.npmCpu, + npm_libc: platform.npmLibc, + surfaces: nativeSurfaces(target), + published: publishedTarget, + _source_file: "Moon release metadata", + }; + if (!publishedTarget) { + row.tier = "planned"; + row.unsupported_reason = planned.get(target).unsupportedReason; + } + rows.push(row); + } + rows.push( + { + id: `${product}.apple-spm-xcframework`, + product, + kind: "apple-swiftpm-binary", + target: "apple-spm-xcframework", + triple: "apple-xcframework", + runner: "macos-latest", + asset: "liboliphaunt-{version}-apple-spm-xcframework.zip", + surfaces: ["github-release", "swiftpm"], + published: true, + _source_file: "Moon release metadata", + }, + { + id: `${product}.runtime-resources`, + product, + kind: "runtime-resources", + target: "portable", + asset: "liboliphaunt-{version}-runtime-resources.tar.gz", + surfaces: ["github-release", "rust-native-direct", "typescript-native-direct", "swiftpm", "maven"], + published: true, + _source_file: "Moon release metadata", + }, + { + id: `${product}.icu-data`, + product, + kind: "icu-data", + target: "portable", + asset: "liboliphaunt-{version}-icu-data.tar.gz", + npm_package: "@oliphaunt/icu", + surfaces: [ + "github-release", + "rust-native-direct", + "typescript-native-direct", + "swiftpm", + "maven", + "react-native-ios", + "react-native-android", + ], + published: true, + _source_file: "Moon release metadata", + }, + { + id: `${product}.package-size`, + product, + kind: "package-footprint", + target: "portable", + asset: "liboliphaunt-{version}-package-size.tsv", + surfaces: [ + "github-release", + "swiftpm", + "maven", + "react-native-ios", + "react-native-android", + "rust-native-direct", + "typescript-native-direct", + ], + published: true, + _source_file: "Moon release metadata", + }, + { + id: `${product}.checksums`, + product, + kind: "checksums", + target: "portable", + asset: "liboliphaunt-{version}-release-assets.sha256", + surfaces: ["github-release"], + published: true, + _source_file: "Moon release metadata", + }, + ); + for (const target of [...published].filter((item) => item in DESKTOP_TARGETS).sort(compareText)) { + const platform = DESKTOP_TARGETS[target]; + rows.push({ + id: `${product}.tools-${target}`, + product, + kind: "native-tools", + target, + triple: platform.triple, + runner: platform.runner, + asset: archiveAsset("oliphaunt-tools", target, platform.archive), + npm_package: platform.liboliphauntToolsNpmPackage, + npm_os: platform.npmOs, + npm_cpu: platform.npmCpu, + npm_libc: platform.npmLibc, + surfaces: ["github-release", "rust-native-direct", "typescript-native-direct"], + published: true, + _source_file: "Moon release metadata", + }); + } + return rows; +} + +function liboliphauntWasixRows(prefix) { + const product = "liboliphaunt-wasix"; + const published = new Set(publishedTargets(product, PRODUCT_PRESETS[product], WASIX_TARGETS, prefix)); + if (!published.has("portable")) { + fail(prefix, `Moon release metadata for ${product} must publish the portable runtime target`); + } + const rows = [ + { + id: `${product}.runtime-portable`, + product, + kind: "wasix-runtime", + target: "portable", + asset: "liboliphaunt-wasix-{version}-runtime-portable.tar.zst", + surfaces: ["github-release"], + published: true, + _source_file: "Moon release metadata", + }, + { + id: `${product}.icu-data`, + product, + kind: "icu-data", + target: "portable", + asset: "liboliphaunt-wasix-{version}-icu-data.tar.zst", + surfaces: ["github-release"], + published: true, + _source_file: "Moon release metadata", + }, + ]; + for (const target of [...published].filter((item) => item !== "portable").sort(compareText)) { + const platform = DESKTOP_TARGETS[target]; + rows.push({ + id: `${product}.aot-${target}`, + product, + kind: "wasix-aot-runtime", + target, + triple: platform.triple, + runner: platform.runner, + llvm_url: platform.wasixLlvmUrl, + asset: `liboliphaunt-wasix-{version}-runtime-aot-${target}.tar.zst`, + surfaces: ["github-release"], + published: true, + _source_file: "Moon release metadata", + }); + } + rows.push({ + id: `${product}.checksums`, + product, + kind: "checksums", + target: "portable", + asset: "liboliphaunt-wasix-{version}-release-assets.sha256", + surfaces: ["github-release"], + published: true, + _source_file: "Moon release metadata", + }); + return rows; +} + +function brokerRows(prefix) { + const product = "oliphaunt-broker"; + const rows = []; + for (const target of publishedTargets(product, PRODUCT_PRESETS[product], BROKER_TARGETS, prefix).sort(compareText)) { + const platform = DESKTOP_TARGETS[target]; + rows.push({ + id: `${product}.${target}`, + product, + kind: "broker-helper", + target, + triple: platform.triple, + runner: platform.runner, + asset: archiveAsset(product, target, platform.archive), + executable_relative_path: target === "windows-x64-msvc" ? "bin/oliphaunt-broker.exe" : "bin/oliphaunt-broker", + npm_package: platform.brokerNpmPackage, + npm_os: platform.npmOs, + npm_cpu: platform.npmCpu, + npm_libc: platform.npmLibc, + surfaces: ["github-release", "rust-broker", "typescript-broker"], + published: true, + _source_file: "Moon release metadata", + }); + } + rows.push({ + id: `${product}.checksums`, + product, + kind: "checksums", + target: "portable", + asset: "oliphaunt-broker-{version}-release-assets.sha256", + surfaces: ["github-release", "rust-broker", "typescript-broker"], + published: true, + _source_file: "Moon release metadata", + }); + return rows; +} + +function nodeDirectRows(prefix) { + const product = "oliphaunt-node-direct"; + const rows = []; + for (const target of publishedTargets(product, PRODUCT_PRESETS[product], NODE_DIRECT_TARGETS, prefix).sort(compareText)) { + const platform = DESKTOP_TARGETS[target]; + rows.push({ + id: `${product}.${target}`, + product, + kind: "node-direct-addon", + target, + triple: platform.triple, + runner: platform.runner, + asset: archiveAsset(product, target, platform.archive), + library_relative_path: "oliphaunt_node.node", + npm_package: platform.nodePackage, + npm_os: platform.npmOs, + npm_cpu: platform.npmCpu, + npm_libc: platform.npmLibc, + surfaces: ["github-release", "npm-optional"], + published: true, + _source_file: "Moon release metadata", + }); + } + rows.push({ + id: `${product}.checksums`, + product, + kind: "checksums", + target: "portable", + asset: "oliphaunt-node-direct-{version}-release-assets.sha256", + surfaces: ["github-release"], + published: true, + _source_file: "Moon release metadata", + }); + return rows; +} + +function rawArtifactTargetRows(prefix) { + return [ + ...liboliphauntNativeRows(prefix), + ...liboliphauntWasixRows(prefix), + ...brokerRows(prefix), + ...nodeDirectRows(prefix), + ]; +} + +function stringField(row, key, id, required, prefix) { + const value = row[key]; + if (typeof value === "string" && value.length > 0) { + return value; + } + if (required) { + fail(prefix, `artifact target ${id}.${key} must be a non-empty string`); + } + if (value !== undefined && value !== null) { + fail(prefix, `artifact target ${id}.${key} must be a string`); + } + return undefined; +} + +function normalizeArtifactTarget(row, prefix) { + const id = stringField(row, "id", "", true, prefix); + const target = { + id, + product: stringField(row, "product", id, true, prefix), + kind: stringField(row, "kind", id, true, prefix), + target: stringField(row, "target", id, true, prefix), + asset: stringField(row, "asset", id, true, prefix), + published: row.published, + surfaces: assertStringList(row.surfaces, `${id}.surfaces`, prefix), + triple: stringField(row, "triple", id, false, prefix), + runner: stringField(row, "runner", id, false, prefix), + libraryRelativePath: stringField(row, "library_relative_path", id, false, prefix), + executableRelativePath: stringField(row, "executable_relative_path", id, false, prefix), + npmPackage: stringField(row, "npm_package", id, false, prefix), + npmOs: stringField(row, "npm_os", id, false, prefix), + npmCpu: stringField(row, "npm_cpu", id, false, prefix), + npmLibc: stringField(row, "npm_libc", id, false, prefix), + llvmUrl: stringField(row, "llvm_url", id, false, prefix), + extensionArtifacts: row.extension_artifacts ?? true, + }; + if (typeof target.published !== "boolean") { + fail(prefix, `artifact target ${id}.published must be true or false`); + } + if (typeof target.extensionArtifacts !== "boolean") { + fail(prefix, `artifact target ${id}.extension_artifacts must be true or false`); + } + return target; +} + +export function allArtifactTargets( + { + product = undefined, + kind = undefined, + surface = undefined, + publishedOnly = false, + } = {}, + prefix = "release-artifact-targets.mjs", +) { + const products = graph(prefix).products; + const seen = new Set(); + return rawArtifactTargetRows(prefix) + .map((row) => normalizeArtifactTarget(row, prefix)) + .filter((target) => { + if (seen.has(target.id)) { + fail(prefix, `duplicate artifact target id ${target.id}`); + } + seen.add(target.id); + if (!products[target.product]) { + fail(prefix, `artifact target ${target.id} references unknown product ${target.product}`); + } + if (product !== undefined && target.product !== product) { + return false; + } + if (kind !== undefined && target.kind !== kind) { + return false; + } + if (surface !== undefined && !target.surfaces.includes(surface)) { + return false; + } + if (publishedOnly && !target.published) { + return false; + } + return true; + }); } -function archiveAsset(product, target, archive) { - return `${product}-{version}-${target}.${archive}`; +export function artifactTargets(product, kind, prefix) { + return allArtifactTargets({ product, kind, publishedOnly: true }, prefix); +} + +export function releaseMetadata(product, prefix) { + const release = graph(prefix).moon_projects?.[product]?.project?.metadata?.release; + if (!release) { + fail(prefix, `Moon release metadata does not include ${product}`); + } + if (release.component !== product) { + fail(prefix, `Moon release metadata for ${product} must use matching component`); + } + if (typeof release.packagePath !== "string" || !release.packagePath) { + fail(prefix, `Moon release metadata for ${product} must declare packagePath`); + } + const expectedPreset = PRODUCT_PRESETS[product]; + if (expectedPreset !== undefined) { + const artifactTargets = release.artifactTargets; + if ( + typeof artifactTargets !== "object" || + artifactTargets === null || + artifactTargets.preset !== expectedPreset + ) { + fail(prefix, `Moon release metadata for ${product} must use artifactTargets preset ${expectedPreset}`); + } + } + return release; } function parseCargoVersion(text, file, prefix) { @@ -80,49 +635,6 @@ async function readJson(file, prefix) { } } -function moonReleaseProducts(prefix) { - const value = JSON.parse(runMoon(["query", "projects"])); - if (!Array.isArray(value.projects)) { - fail(prefix, "moon query projects did not return a projects array"); - } - const products = new Map(); - for (const project of value.projects) { - const id = project?.id; - const release = project?.config?.project?.metadata?.release; - if (release === undefined) { - continue; - } - if (typeof id !== "string" || typeof release !== "object" || release === null) { - fail(prefix, "Moon release metadata returned an invalid product row"); - } - products.set(id, release); - } - return products; -} - -export function releaseMetadata(product, prefix) { - const release = moonReleaseProducts(prefix).get(product); - if (!release) { - fail(prefix, `Moon release metadata does not include ${product}`); - } - if (release.component !== product) { - fail(prefix, `Moon release metadata for ${product} must use matching component`); - } - if (typeof release.packagePath !== "string" || !release.packagePath) { - fail(prefix, `Moon release metadata for ${product} must declare packagePath`); - } - const artifactTargets = release.artifactTargets; - const expectedPreset = PRODUCT_PRESETS[product]; - if ( - typeof artifactTargets !== "object" || - artifactTargets === null || - artifactTargets.preset !== expectedPreset - ) { - fail(prefix, `Moon release metadata for ${product} must use artifactTargets preset ${expectedPreset}`); - } - return release; -} - export async function currentProductVersion(product, prefix) { const release = releaseMetadata(product, prefix); const packagePath = release.packagePath; @@ -160,50 +672,179 @@ export async function currentProductVersion(product, prefix) { fail(prefix, `${rel(file)} does not define a release version for ${product}`); } -export function artifactTargets(product, kind, prefix) { +export function expectedAssets(product, kind, version, prefix) { + const assets = artifactTargets(product, kind, prefix).map((target) => + target.asset.replaceAll("{version}", version), + ); + assets.push(`${product}-${version}-release-assets.sha256`); + return assets.sort(compareText); +} + +export function exactExtensionProducts(prefix = "release-artifact-targets.mjs") { + return Object.entries(graph(prefix).products) + .filter(([, config]) => config.kind === "exact-extension-artifact") + .map(([product]) => product) + .sort(compareText); +} + +function extensionSqlName(product, prefix) { + const value = graph(prefix).products[product]?.extension_sql_name; + if (typeof value !== "string" || !value) { + fail(prefix, `${product} release.toml must declare extension_sql_name`); + } + return value; +} + +function wasixExtensionTargetId(runtimeTarget) { + return runtimeTarget === "portable" ? "wasix-portable" : runtimeTarget; +} + +function defaultExtensionTargetRows(product, prefix) { + const sourceFile = `${releaseMetadata(product, prefix).packagePath}/release.toml`; + const rows = []; + for (const target of allArtifactTargets( + { product: "liboliphaunt-native", kind: "native-runtime", publishedOnly: true }, + prefix, + )) { + if (!target.extensionArtifacts) { + continue; + } + rows.push({ + target: target.target, + family: "native", + kind: target.target === "ios-xcframework" || target.target.startsWith("android-") + ? "native-static-registry" + : "native-dynamic", + status: "supported", + published: true, + _source_file: sourceFile, + }); + } + for (const target of allArtifactTargets( + { product: "liboliphaunt-wasix", kind: "wasix-runtime", publishedOnly: true }, + prefix, + )) { + rows.push({ + target: wasixExtensionTargetId(target.target), + family: "wasix", + kind: "wasix-runtime", + status: "supported", + published: true, + _source_file: sourceFile, + }); + } + if (rows.length === 0) { + fail(prefix, `${product} could not derive any exact-extension artifact targets`); + } + return rows; +} + +function readExtensionTargetRows(product, prefix) { const release = releaseMetadata(product, prefix); - const publishedTargets = release.artifactTargets.publishedTargets; - if ( - !Array.isArray(publishedTargets) || - !publishedTargets.every((target) => typeof target === "string" && target) - ) { - fail(prefix, `Moon release metadata for ${product} must declare publishedTargets`); - } - const targets = []; - for (const target of [...publishedTargets].sort(compareText)) { - const platform = DESKTOP_TARGETS[target]; - if (!platform) { - fail(prefix, `unknown ${product} artifact target ${target}`); + const relative = `${release.packagePath}/targets/artifacts.toml`; + const file = path.join(ROOT, relative); + if (!existsSync(file)) { + return defaultExtensionTargetRows(product, prefix); + } + const data = Bun.TOML.parse(readFileSync(file, "utf8")); + if (data.schema !== "oliphaunt-extension-artifact-targets-v1") { + fail(prefix, `${relative} must use schema = "oliphaunt-extension-artifact-targets-v1"`); + } + if (!Array.isArray(data.targets) || data.targets.length === 0) { + fail(prefix, `${relative} must define [[targets]] rows`); + } + const allowed = new Set(defaultExtensionTargetRows(product, prefix).map((row) => `${row.target}\0${row.family}\0${row.kind}`)); + for (const row of data.targets) { + row._source_file = relative; + if (!allowed.has(`${row.target}\0${row.family}\0${row.kind}`)) { + fail(prefix, `${relative} target row ${row.target}/${row.family}/${row.kind} is not backed by runtime artifact metadata`); } - if (product === "oliphaunt-broker") { - targets.push({ - id: `${product}.${target}`, - product, - kind, + } + return data.targets; +} + +function boolField(value, label, prefix) { + if (typeof value === "boolean") { + return value; + } + fail(prefix, `${label} must be true or false`); +} + +function nonEmptyString(value, label, prefix) { + if (typeof value === "string" && value.length > 0) { + return value; + } + fail(prefix, `${label} must be a non-empty string`); +} + +export function extensionArtifactTargets( + { + product = undefined, + family = undefined, + publishedOnly = false, + } = {}, + prefix = "release-artifact-targets.mjs", +) { + const products = product === undefined ? exactExtensionProducts(prefix) : [product]; + const parsed = []; + for (const productId of products) { + if (!exactExtensionProducts(prefix).includes(productId)) { + fail(prefix, `${productId} is not an exact-extension artifact product`); + } + const sqlName = extensionSqlName(productId, prefix); + const seen = new Set(); + for (const [index, row] of readExtensionTargetRows(productId, prefix).entries()) { + const source = row._source_file ?? releaseMetadata(productId, prefix).packagePath; + const target = nonEmptyString(row.target, `${source} targets[${index}].target`, prefix); + const targetFamily = nonEmptyString(row.family, `${source} targets[${index}].family`, prefix); + const kind = nonEmptyString(row.kind, `${source} targets[${index}].kind`, prefix); + const status = nonEmptyString(row.status, `${source} targets[${index}].status`, prefix); + const published = boolField(row.published, `${source} targets[${index}].published`, prefix); + if (!EXTENSION_FAMILIES.has(targetFamily)) { + fail(prefix, `${source} target ${target} has invalid family ${targetFamily}`); + } + if (!EXTENSION_KINDS.has(kind)) { + fail(prefix, `${source} target ${target} has invalid kind ${kind}`); + } + if (!EXTENSION_STATUSES.has(status)) { + fail(prefix, `${source} target ${target} has invalid status ${status}`); + } + if (targetFamily === "wasix" && kind !== "wasix-runtime") { + fail(prefix, `${source} target ${target} must use kind wasix-runtime for wasix family`); + } + if (targetFamily === "native" && kind === "wasix-runtime") { + fail(prefix, `${source} target ${target} cannot use wasix-runtime for native family`); + } + if (published && status !== "supported") { + fail(prefix, `${source} target ${target} cannot be published with status ${status}`); + } + const key = `${target}\0${targetFamily}\0${kind}`; + if (seen.has(key)) { + fail(prefix, `${source} has duplicate target row ${target}/${targetFamily}/${kind}`); + } + seen.add(key); + if (family !== undefined && targetFamily !== family) { + continue; + } + if (publishedOnly && !published) { + continue; + } + parsed.push({ + product: productId, + sqlName, + sql_name: sqlName, target, - asset: archiveAsset(product, target, platform.archive), - executableRelativePath: platform.brokerExecutable, - }); - } else if (product === "oliphaunt-node-direct") { - targets.push({ - id: `${product}.${target}`, - product, + family: targetFamily, kind, - target, - asset: archiveAsset(product, target, platform.archive), - libraryRelativePath: platform.nodeDirectLibrary, + published, + status, }); - } else { - fail(prefix, `unsupported product ${product}`); } } - return targets; + return parsed; } -export function expectedAssets(product, kind, version, prefix) { - const assets = artifactTargets(product, kind, prefix).map((target) => - target.asset.replaceAll("{version}", version), - ); - assets.push(`${product}-${version}-release-assets.sha256`); - return assets.sort(compareText); +export function publishedExtensionTargetIds({ family }, prefix = "release-artifact-targets.mjs") { + return [...new Set(extensionArtifactTargets({ family, publishedOnly: true }, prefix).map((target) => target.target))] + .sort(compareText); } From cc018881b27935aa4fc16eda00be63b91143c781 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 01:31:11 +0000 Subject: [PATCH 137/137] feat: split native tools into cargo facade --- Cargo.lock | 4 + Cargo.toml | 1 + .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 46 +++++----- .../examples-ci-release-validation.md | 6 +- docs/maintainers/release.md | 4 +- docs/maintainers/sdk-parity-policy.md | 2 +- examples/README.md | 14 +-- examples/tauri/src-tauri/Cargo.lock | 63 ++++--------- examples/tauri/src-tauri/Cargo.toml | 2 +- examples/tools/check-examples.sh | 2 +- release-please-config.json | 5 ++ .../native/crates/tools/Cargo.toml | 15 ++++ .../native/crates/tools/README.md | 8 ++ .../liboliphaunt/native/crates/tools/build.rs | 89 +++++++++++++++++++ .../native/crates/tools/src/lib.rs | 7 ++ src/runtimes/liboliphaunt/native/release.toml | 1 + .../rust/src/liboliphaunt/root/runtime.rs | 2 +- tools/policy/check-sdk-parity.sh | 2 +- tools/release/check_consumer_shape.py | 2 + tools/release/check_release_metadata.py | 1 + tools/release/local_registry_publish.py | 3 + .../package_liboliphaunt_cargo_artifacts.py | 76 ++++++++++++++++ tools/release/release.py | 61 +++++++++---- tools/release/sync-example-lockfiles.mjs | 1 + 24 files changed, 319 insertions(+), 98 deletions(-) create mode 100644 src/runtimes/liboliphaunt/native/crates/tools/Cargo.toml create mode 100644 src/runtimes/liboliphaunt/native/crates/tools/README.md create mode 100644 src/runtimes/liboliphaunt/native/crates/tools/build.rs create mode 100644 src/runtimes/liboliphaunt/native/crates/tools/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 42bbeb3c..349f1558 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2332,6 +2332,10 @@ dependencies = [ "tokio-postgres", ] +[[package]] +name = "oliphaunt-tools" +version = "0.1.0" + [[package]] name = "oliphaunt-wasix" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 7034eef4..7e91aa8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "src/bindings/wasix-rust/crates/oliphaunt-wasix", "src/sdks/rust/crates/oliphaunt-build", "src/sdks/rust", + "src/runtimes/liboliphaunt/native/crates/tools", "src/runtimes/broker", "src/runtimes/liboliphaunt/icu", "src/runtimes/liboliphaunt/wasix/crates/assets", diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 0d9f1a1c..143d3d9a 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -15,11 +15,11 @@ until the current-state gates here are checked with fresh local evidence. - [x] Rebuild or refresh local Cargo and npm registries from current release fixture/artifact generation paths, including native runtime crates, native - `oliphaunt-tools-*` crates, WASIX runtime/tools/AOT crates, broker crates, - extension crates, and JS packages. + `oliphaunt-tools` facade plus `oliphaunt-tools-*` payload crates, WASIX + runtime/tools/AOT crates, broker crates, extension crates, and JS packages. - [x] Verify native Tauri installs `liboliphaunt-native-linux-x64-gnu`, - `oliphaunt-tools-linux-x64-gnu`, and selected extension crates from - `registry = "oliphaunt-local"` with no path dependency fallback. + `oliphaunt-tools`, `oliphaunt-tools-linux-x64-gnu`, and selected extension + crates from `registry = "oliphaunt-local"` with no path dependency fallback. - [x] Verify native Electron installs `@oliphaunt/ts`, native runtime/tools npm packages, and extension npm packages from the local Verdaccio registry. - [x] Verify Tauri WASIX, Electron WASIX, and the nested WASIX SQLx Tauri @@ -180,9 +180,10 @@ until the current-state gates here are checked with fresh local evidence. `bash tools/policy/check-native-boundaries.sh`, and `bun tools/policy/check-wasix-release-dependency-invariants.mjs`. Local crate payload inspection found native root crates carrying only `initdb`, `pg_ctl`, - and `postgres`; native `oliphaunt-tools-*` carrying `pg_dump` and `psql`; - WASIX root carrying only `initdb` plus runtime/template payloads; and - `oliphaunt-wasix-tools` carrying `pg_dump.wasix.wasm` and `psql.wasix.wasm`. + and `postgres`; native `oliphaunt-tools` selecting `oliphaunt-tools-*` + payload crates carrying `pg_dump` and `psql`; WASIX root carrying only + `initdb` plus runtime/template payloads; and `oliphaunt-wasix-tools` + carrying `pg_dump.wasix.wasm` and `psql.wasix.wasm`. - 2026-06-26: Native root/tools npm descriptor checks now read `publishConfig.executableFiles` directly. Root package descriptors must list only `initdb`, `pg_ctl`, and `postgres`; split `@oliphaunt/tools-*` @@ -506,7 +507,7 @@ until the current-state gates here are checked with fresh local evidence. ## Priority 1: Example App Validation - [x] Inventory every example app, its package managers, local-registry dependencies, and runtime/tool/extension paths. -- [x] Ensure each native example uses `oliphaunt-tools-*` from the local registry when it exercises standalone tools. +- [x] Ensure each native example uses the `oliphaunt-tools` facade from the local registry when it exercises standalone tools. - [x] Ensure each WASIX example uses `oliphaunt-wasix-tools` from the local registry and does not rely on path-only tool assets. - [x] Add example-app smoke commands that model the desired developer experience and can run on Linux CI. - [x] Check frontend build/test flows for the Electron, Electron WASIX, Tauri, Tauri WASIX, and WASIX vanilla examples. @@ -557,12 +558,17 @@ until the current-state gates here are checked with fresh local evidence. `dump_sql` path and `psql` through `PsqlOptions::command("SELECT 1")`. Example policy now requires `preflight_tools()`, `dump_sql`, and `psql` calls in every WASIX example that validates the split tools package. -- Local-registry Cargo payload inspection confirmed `liboliphaunt-native-linux-x64-gnu-part-*` contains `initdb`, `pg_ctl`, and `postgres` only under `runtime/bin`, while `oliphaunt-tools-linux-x64-gnu-part-*` contains only `pg_dump` and `psql` there. +- Local-registry Cargo payload inspection confirmed + `liboliphaunt-native-linux-x64-gnu-part-*` contains `initdb`, `pg_ctl`, and + `postgres` only under `runtime/bin`, while the `oliphaunt-tools` facade + selects `oliphaunt-tools-linux-x64-gnu-part-*` payloads containing only + `pg_dump` and `psql` there. - The small liboliphaunt release fixture now includes all five native desktop PostgreSQL binaries so fixture Cargo packaging exercises the split: - `liboliphaunt-native-*` keeps `initdb`, `pg_ctl`, and `postgres`, while - `oliphaunt-tools-*` keeps `pg_dump` and `psql`. Consumer-shape checks enforce - the same generator contract. + `liboliphaunt-native-*` keeps `initdb`, `pg_ctl`, and `postgres`, while the + `oliphaunt-tools` facade selects `oliphaunt-tools-*` payloads that keep + `pg_dump` and `psql`. Consumer-shape checks enforce the same generator + contract. - Release dry-run validation now inspects the nested WASIX runtime archive for `postgres` and `initdb`, and rejects `pg_ctl`, `pg_dump`, or `psql` there. - Local registry publication was refreshed with explicit native runtime/tools, @@ -594,10 +600,10 @@ until the current-state gates here are checked with fresh local evidence. - `examples/tools/run-electron-driver-smoke.sh examples/electron` and `examples/tools/run-electron-driver-smoke.sh examples/electron-wasix` now provide repeatable Linux GUI smoke coverage using the packaged Electron binary, an IPC test-driver hook, and `xvfb-run` when present. - On 2026-06-26, all four GUI smoke commands passed against the refreshed local registries: native Electron, WASIX Electron, native Tauri, and WASIX Tauri. - Native Tauri compiled `oliphaunt-tools-linux-x64-gnu` plus split runtime and - extension crates from `oliphaunt-local`; WASIX Tauri exercised the split - WASIX runtime/tools/AOT and selected extension package graph through - WebDriver. + Native Tauri compiled the `oliphaunt-tools` facade plus split runtime, target + tools payload, and extension crates from `oliphaunt-local`; WASIX Tauri + exercised the split WASIX runtime/tools/AOT and selected extension package + graph through WebDriver. - On 2026-06-26, the nested WASIX SQLx Tauri profiler was switched to TCP startup so its headless local-registry run executes the split WASIX tools smoke (`preflight_tools`, `pg_dump --schema-only`, and noninteractive @@ -999,10 +1005,10 @@ until the current-state gates here are checked with fresh local evidence. proven instead of merely accepted by path. - On 2026-06-26, the split client-tool crate contract was rechecked against the implementation: native root/runtime artifacts keep `postgres`, `initdb`, and - `pg_ctl`, native `oliphaunt-tools-*` artifacts keep only `pg_dump` and - `psql`, WASIX root/runtime artifacts keep `postgres` plus `initdb`, and - `oliphaunt-wasix-tools` plus tools-AOT artifacts keep `pg_dump` and `psql` - with no WASIX `pg_ctl`. The focused shape checks passed: + `pg_ctl`, native `oliphaunt-tools` selects payload artifacts that keep only + `pg_dump` and `psql`, WASIX root/runtime artifacts keep `postgres` plus + `initdb`, and `oliphaunt-wasix-tools` plus tools-AOT artifacts keep + `pg_dump` and `psql` with no WASIX `pg_ctl`. The focused shape checks passed: `check_consumer_shape.py` for liboliphaunt native/WASIX/Rust, `check_artifact_targets.py`, `examples/tools/check-examples.sh`, and `cargo test -p oliphaunt-build --locked`. diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index 62f41607..90713b17 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -8,6 +8,7 @@ the release/tooling surface after the runtime tool crate split. - [x] Rebuild or stage current local registry artifacts from the active branch. - [x] Publish local Cargo crates into `target/local-registries/cargo`, including: - `liboliphaunt-native-linux-x64-gnu` + - `oliphaunt-tools` - `oliphaunt-tools-linux-x64-gnu` - `oliphaunt-broker-linux-x64-gnu` - selected native extension crates @@ -17,7 +18,7 @@ the release/tooling surface after the runtime tool crate split. - selected WASIX extension crates and extension-AOT crates - [x] Publish local npm packages to Verdaccio for root desktop examples. - [x] Update root examples so their manifests model the registry install path: - - native Tauri explicitly resolves the native tools artifact crate + - native Tauri resolves the native `oliphaunt-tools` facade, which selects the target tools payload crate - WASIX examples explicitly resolve the WASIX tools and tools-AOT artifact crates - product-local WASIX example no longer uses path dependencies - [x] Exercise tool paths in example code, not only in dependency manifests: @@ -90,7 +91,8 @@ the release/tooling surface after the runtime tool crate split. - The small liboliphaunt release fixture now models all five native desktop PostgreSQL binaries, so fixture packaging verifies that `liboliphaunt-native-*` part crates keep only `initdb`, `pg_ctl`, and - `postgres`, while `oliphaunt-tools-*` part crates keep `pg_dump` and `psql`. + `postgres`, while the `oliphaunt-tools` facade selects `oliphaunt-tools-*` + part crates that keep `pg_dump` and `psql`. Consumer-shape checks now enforce that generator contract. - The local Cargo registry was refreshed from the split artifacts. The native Tauri example regenerated its lockfile through `examples/tools/with-local-registries.sh`, diff --git a/docs/maintainers/release.md b/docs/maintainers/release.md index 3211c17d..e92ae410 100644 --- a/docs/maintainers/release.md +++ b/docs/maintainers/release.md @@ -125,8 +125,8 @@ plus mobile targets that apps consume as prebuilt artifacts. Downstream SDKs must consume published native artifacts through normal ecosystem mechanisms: -- Rust/Tauri resolves the native runtime and broker helper through Rust SDK - tooling and GitHub release assets. +- Rust/Tauri resolves the native runtime, `oliphaunt-tools` facade, and broker + helper through Rust SDK tooling and GitHub release assets. - Swift resolves Apple artifacts through SwiftPM-compatible release assets. - Kotlin/Android resolves Android ABI artifacts through the Android Gradle plugin and GitHub release assets. diff --git a/docs/maintainers/sdk-parity-policy.md b/docs/maintainers/sdk-parity-policy.md index b0334456..6fca411f 100644 --- a/docs/maintainers/sdk-parity-policy.md +++ b/docs/maintainers/sdk-parity-policy.md @@ -81,7 +81,7 @@ those overrides are not the consumer install path. | SDK | Runtime/library artifacts | Standalone tools | Extension artifacts | Explicit local override | | --- | --- | --- | --- | --- | -| Rust | Cargo-resolved `liboliphaunt-native-*` artifact crates staged by `oliphaunt-build` | split `oliphaunt-tools-*` Cargo artifact crates copied into the runtime cache | exact `oliphaunt-extension-*` Cargo artifact crates | `OLIPHAUNT_RESOURCES_DIR` | +| Rust | Cargo-resolved `liboliphaunt-native-*` artifact crates staged by `oliphaunt-build` | `oliphaunt-tools` Cargo facade selecting split `oliphaunt-tools-*` payload crates for the runtime cache | exact `oliphaunt-extension-*` Cargo artifact crates | `OLIPHAUNT_RESOURCES_DIR` | | WASIX Rust | Cargo-resolved `liboliphaunt-wasix-portable`, `oliphaunt-icu`, and target AOT artifact crates | optional `oliphaunt-wasix-tools` plus target tools-AOT artifact crates behind the `tools` feature | exact `oliphaunt-extension-*-wasix` and extension AOT Cargo artifact crates selected by feature | `OLIPHAUNT_WASM_GENERATED_ASSETS_DIR` | | TypeScript | npm optional platform packages such as `@oliphaunt/liboliphaunt-*` and `@oliphaunt/node-direct-*` | split `@oliphaunt/tools-*` npm packages | Node/Bun exact extension npm packages for package-managed installs; explicit prepared `runtimeDirectory` values are validated for selected extension files across Node/Bun/Deno | `libraryPath` and `runtimeDirectory` | | Swift | SwiftPM release assets and packaged runtime resources | not exposed in mobile native-direct mode | exact extension XCFramework artifacts selected by SQL extension name | `runtimeDirectory` or `resourceRoot` | diff --git a/examples/README.md b/examples/README.md index 5d93fb84..e4bc95c8 100644 --- a/examples/README.md +++ b/examples/README.md @@ -10,10 +10,11 @@ These examples keep the same todo schema across desktop shells: Each app opts into `hstore`, `pg_trgm`, and `unaccent`, then uses `hstore` tags plus trigram/accent-insensitive search for the todo list. Native examples load `postgres`, `initdb`, and `pg_ctl` from `liboliphaunt-native-*`, while -`pg_dump` and `psql` come from `oliphaunt-tools-*`. WASIX examples load -`postgres` and `initdb` from the runtime crates. WASIX examples enable the -`oliphaunt-wasix` `tools` feature, which resolves `pg_dump`/`psql` from -`oliphaunt-wasix-tools`; WASIX intentionally has no `pg_ctl`. +`pg_dump` and `psql` come through the `oliphaunt-tools` facade selecting +`oliphaunt-tools-*` payload crates. WASIX examples load `postgres` and `initdb` +from the runtime crates. WASIX examples enable the `oliphaunt-wasix` `tools` +feature, which resolves `pg_dump`/`psql` from `oliphaunt-wasix-tools`; WASIX +intentionally has no `pg_ctl`. Local registry artifacts for Linux x64 from CI run `28049923289` can be staged with: @@ -39,8 +40,9 @@ python3 tools/release/local_registry_publish.py publish \ --artifact-root target/local-registry-artifacts/oliphaunt-extension-package-artifacts ``` -The native packaging step emits both `liboliphaunt-native-linux-x64-gnu` and -`oliphaunt-tools-linux-x64-gnu`. The WASIX packaging step emits +The native packaging step emits `liboliphaunt-native-linux-x64-gnu`, the +`oliphaunt-tools` facade crate, and `oliphaunt-tools-linux-x64-gnu`. The WASIX +packaging step emits `liboliphaunt-wasix-portable`, `oliphaunt-wasix-tools`, `liboliphaunt-wasix-aot-*`, and `oliphaunt-wasix-tools-aot-*`. diff --git a/examples/tauri/src-tauri/Cargo.lock b/examples/tauri/src-tauri/Cargo.lock index 62978a19..44eaf6e5 100644 --- a/examples/tauri/src-tauri/Cargo.lock +++ b/examples/tauri/src-tauri/Cargo.lock @@ -1714,15 +1714,9 @@ dependencies = [ name = "liboliphaunt-native-linux-x64-gnu" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "dbbed43b4d8c1a57433def7020f33c01a2b10eba72edfad7b77c80be516e8eb8" +checksum = "339fb30e364733e12d691126243e8cf6e17472cf7f0625e69ba0b2d7ed296e4e" dependencies = [ "liboliphaunt-native-linux-x64-gnu-part-000", - "liboliphaunt-native-linux-x64-gnu-part-001", - "liboliphaunt-native-linux-x64-gnu-part-002", - "liboliphaunt-native-linux-x64-gnu-part-003", - "liboliphaunt-native-linux-x64-gnu-part-004", - "liboliphaunt-native-linux-x64-gnu-part-005", - "liboliphaunt-native-linux-x64-gnu-part-006", "sha2", ] @@ -1730,43 +1724,7 @@ dependencies = [ name = "liboliphaunt-native-linux-x64-gnu-part-000" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "5610cfaffb481874bd2d56d10fce3ed07581d3b312619d0c664aacfe87d7b095" - -[[package]] -name = "liboliphaunt-native-linux-x64-gnu-part-001" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "627a1e5101e32dd4ad382d4c8939d558562eff92136aab0baed3c9bf5a4ee910" - -[[package]] -name = "liboliphaunt-native-linux-x64-gnu-part-002" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "de88e6326ad8b8ae559de1f827ea7adf56e2a3c29099b5b99daed7d53bf45746" - -[[package]] -name = "liboliphaunt-native-linux-x64-gnu-part-003" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "85bf22215694ecbf17e8a8b2328b431ca27cf4848fa2b337751a5b3e92488f0a" - -[[package]] -name = "liboliphaunt-native-linux-x64-gnu-part-004" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "fe14dd7b52188e80b9afdc53af2eed678ec5c577393b9e8b947a8d4a37a90b7b" - -[[package]] -name = "liboliphaunt-native-linux-x64-gnu-part-005" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "87b3c9cc20a00f3285582b9a6b265287f304b5a4368dd86e9f329607b783a5e1" - -[[package]] -name = "liboliphaunt-native-linux-x64-gnu-part-006" -version = "0.1.0" -source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "a3fa2b24de388519f09f5f502b992b61ea80be2179a4b3d9bcc42eee223045ba" +checksum = "a19bbcad796b3aaee8a3ba3c0f4f46d7a148aa5e7958ca57498a4837f0c06d4a" [[package]] name = "libredox" @@ -2144,7 +2102,7 @@ dependencies = [ name = "oliphaunt" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "c959c19f99a25ba04dc9a92f0dd042a82269507999ba972754f2b4862dbf23bf" +checksum = "bf38854611fdfe97264113f7746b4a7cd61e7fb8b2346e4436e5bca1fa4ba8da" dependencies = [ "crossbeam-channel", "flate2", @@ -2153,7 +2111,7 @@ dependencies = [ "libloading 0.8.9", "liboliphaunt-native-linux-x64-gnu", "oliphaunt-broker-linux-x64-gnu", - "oliphaunt-tools-linux-x64-gnu", + "oliphaunt-tools", "serde", "sha2", "tar", @@ -2191,7 +2149,7 @@ dependencies = [ "oliphaunt-extension-hstore-linux-x64-gnu", "oliphaunt-extension-pg-trgm-linux-x64-gnu", "oliphaunt-extension-unaccent-linux-x64-gnu", - "oliphaunt-tools-linux-x64-gnu", + "oliphaunt-tools", "serde", "tauri", "tauri-build", @@ -2226,6 +2184,15 @@ dependencies = [ "sha2", ] +[[package]] +name = "oliphaunt-tools" +version = "0.1.0" +source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" +checksum = "d03f050c7e2307a0b41a082369ab69f2da478d65f5cfd26ec30bf56816333c82" +dependencies = [ + "oliphaunt-tools-linux-x64-gnu", +] + [[package]] name = "oliphaunt-tools-linux-x64-gnu" version = "0.1.0" @@ -2240,7 +2207,7 @@ dependencies = [ name = "oliphaunt-tools-linux-x64-gnu-part-000" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "c069918c5c037a145fc0b0453f7f90ea06a26556344b3b096c3ab09f82864c03" +checksum = "834e7c11f46fb5b5f87cefca8106ce533eba734ace5818e21d011be92fbacdaf" [[package]] name = "once_cell" diff --git a/examples/tauri/src-tauri/Cargo.toml b/examples/tauri/src-tauri/Cargo.toml index 82d75d0b..a4e118fc 100644 --- a/examples/tauri/src-tauri/Cargo.toml +++ b/examples/tauri/src-tauri/Cargo.toml @@ -23,6 +23,7 @@ tauri-build = { version = "2", features = [] } [dependencies] anyhow = "1" oliphaunt = { version = "=0.1.0", registry = "oliphaunt-local" } +oliphaunt-tools = { version = "=0.1.0", registry = "oliphaunt-local" } serde = { version = "1", features = ["derive"] } tauri = { version = "2", features = [] } thiserror = "2" @@ -31,7 +32,6 @@ tokio = { version = "1", features = ["sync"] } [target.'cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))'.dependencies] liboliphaunt-native-linux-x64-gnu = { version = "=0.1.0", registry = "oliphaunt-local" } oliphaunt-broker-linux-x64-gnu = { version = "=0.1.0", registry = "oliphaunt-local" } -oliphaunt-tools-linux-x64-gnu = { version = "=0.1.0", registry = "oliphaunt-local" } oliphaunt-extension-hstore-linux-x64-gnu = { version = "=0.1.0", registry = "oliphaunt-local" } oliphaunt-extension-pg-trgm-linux-x64-gnu = { version = "=0.1.0", registry = "oliphaunt-local" } oliphaunt-extension-unaccent-linux-x64-gnu = { version = "=0.1.0", registry = "oliphaunt-local" } diff --git a/examples/tools/check-examples.sh b/examples/tools/check-examples.sh index dbb0fb88..8e8727f6 100755 --- a/examples/tools/check-examples.sh +++ b/examples/tools/check-examples.sh @@ -113,7 +113,7 @@ require_text "examples/electron/package.json" '"@oliphaunt/extension-unaccent": require_text "examples/electron/package.json" '"pg": "\^8\.16\.3"' reject_file "examples/electron/src/oliphaunt-kysely.ts" require_text "examples/tauri/src-tauri/Cargo.toml" 'registry = "oliphaunt-local"' -require_text "examples/tauri/src-tauri/Cargo.toml" 'oliphaunt-tools-linux-x64-gnu' +require_text "examples/tauri/src-tauri/Cargo.toml" 'oliphaunt-tools =' require_text "examples/tauri/src-tauri/Cargo.toml" 'oliphaunt-extension-hstore-linux-x64-gnu' require_text "examples/tauri/src-tauri/Cargo.toml" 'oliphaunt-extension-pg-trgm-linux-x64-gnu' require_text "examples/tauri/src-tauri/Cargo.toml" 'oliphaunt-extension-unaccent-linux-x64-gnu' diff --git a/release-please-config.json b/release-please-config.json index b7d7e1ba..de4af269 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -58,6 +58,11 @@ "path": "tools-packages/win32-x64-msvc/package.json", "jsonpath": "$.version" }, + { + "type": "toml", + "path": "crates/tools/Cargo.toml", + "jsonpath": "$.package.version" + }, { "type": "json", "path": "icu-npm/package.json", diff --git a/src/runtimes/liboliphaunt/native/crates/tools/Cargo.toml b/src/runtimes/liboliphaunt/native/crates/tools/Cargo.toml new file mode 100644 index 00000000..2e6a9349 --- /dev/null +++ b/src/runtimes/liboliphaunt/native/crates/tools/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "oliphaunt-tools" +version = "0.1.0" +edition = "2024" +rust-version = "1.93" +description = "Target-selecting Cargo facade for Oliphaunt native pg_dump and psql artifacts." +readme = "README.md" +repository.workspace = true +homepage.workspace = true +license = "MIT AND Apache-2.0 AND PostgreSQL" +links = "oliphaunt_artifact_oliphaunt_tools_relay" +build = "build.rs" + +[lib] +path = "src/lib.rs" diff --git a/src/runtimes/liboliphaunt/native/crates/tools/README.md b/src/runtimes/liboliphaunt/native/crates/tools/README.md new file mode 100644 index 00000000..a3a5ebf3 --- /dev/null +++ b/src/runtimes/liboliphaunt/native/crates/tools/README.md @@ -0,0 +1,8 @@ +# oliphaunt-tools + +Cargo facade for target-specific Oliphaunt native PostgreSQL client tool +artifacts. + +Applications normally receive this crate through `oliphaunt`. It selects the +matching `oliphaunt-tools-*` artifact crate for the Cargo target and relays the +resolved `pg_dump` and `psql` payload manifest to `oliphaunt-build`. diff --git a/src/runtimes/liboliphaunt/native/crates/tools/build.rs b/src/runtimes/liboliphaunt/native/crates/tools/build.rs new file mode 100644 index 00000000..68b70641 --- /dev/null +++ b/src/runtimes/liboliphaunt/native/crates/tools/build.rs @@ -0,0 +1,89 @@ +use std::collections::BTreeMap; +use std::env; + +const ARTIFACT_ENV_PREFIX: &str = "DEP_OLIPHAUNT_ARTIFACT_"; +const ARTIFACT_ENV_SUFFIX: &str = "_MANIFEST"; +const RELAY_ENV_PREFIX: &str = "DEP_OLIPHAUNT_ARTIFACT_OLIPHAUNT_TOOLS_RELAY_"; + +fn main() { + match relay_manifest_instructions(env::vars()) { + Ok(instructions) => { + for instruction in instructions { + println!("{instruction}"); + } + } + Err(error) => { + println!("cargo::error={error}"); + panic!("oliphaunt-tools artifact relay failed: {error}"); + } + } +} + +fn relay_manifest_instructions(vars: I) -> Result, String> +where + I: IntoIterator, +{ + let mut manifests = BTreeMap::new(); + let mut instructions = Vec::new(); + for (key, value) in vars { + let Some(metadata_key) = relay_metadata_key(&key) else { + continue; + }; + if value.is_empty() { + continue; + } + if let Some(existing) = manifests.insert(metadata_key.clone(), value.clone()) + && existing != value + { + return Err(format!( + "conflicting Cargo artifact manifests for metadata key {metadata_key}: {existing} and {value}" + )); + } + instructions.push(format!("cargo::rerun-if-changed={value}")); + } + for (metadata_key, manifest) in manifests { + instructions.push(format!("cargo::metadata={metadata_key}={manifest}")); + } + Ok(instructions) +} + +fn relay_metadata_key(env_key: &str) -> Option { + if env_key.starts_with(RELAY_ENV_PREFIX) { + return None; + } + let stem = env_key + .strip_prefix(ARTIFACT_ENV_PREFIX)? + .strip_suffix(ARTIFACT_ENV_SUFFIX)?; + if stem.is_empty() { + return None; + } + Some(format!("{}_manifest", stem.to_ascii_lowercase())) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn re_emits_target_tool_manifest() { + let instructions = relay_manifest_instructions([( + "DEP_OLIPHAUNT_ARTIFACT_OLIPHAUNT_TOOLS_LINUX_X64_GNU_MANIFEST".to_owned(), + "/tmp/tools.toml".to_owned(), + )]) + .unwrap(); + assert!(instructions.contains(&"cargo::rerun-if-changed=/tmp/tools.toml".to_owned())); + assert!(instructions.contains( + &"cargo::metadata=oliphaunt_tools_linux_x64_gnu_manifest=/tmp/tools.toml".to_owned() + )); + } + + #[test] + fn ignores_own_downstream_metadata() { + let instructions = relay_manifest_instructions([( + "DEP_OLIPHAUNT_ARTIFACT_OLIPHAUNT_TOOLS_RELAY_MANIFEST".to_owned(), + "/tmp/tools.toml".to_owned(), + )]) + .unwrap(); + assert!(instructions.is_empty()); + } +} diff --git a/src/runtimes/liboliphaunt/native/crates/tools/src/lib.rs b/src/runtimes/liboliphaunt/native/crates/tools/src/lib.rs new file mode 100644 index 00000000..8d33ddbc --- /dev/null +++ b/src/runtimes/liboliphaunt/native/crates/tools/src/lib.rs @@ -0,0 +1,7 @@ +#![deny(unsafe_code)] + +/// Product id for the native PostgreSQL client tools artifact family. +pub const PRODUCT: &str = "oliphaunt-tools"; + +/// Artifact kind relayed by this facade crate. +pub const KIND: &str = "native-tools"; diff --git a/src/runtimes/liboliphaunt/native/release.toml b/src/runtimes/liboliphaunt/native/release.toml index 8d0edc76..8239dc96 100644 --- a/src/runtimes/liboliphaunt/native/release.toml +++ b/src/runtimes/liboliphaunt/native/release.toml @@ -7,6 +7,7 @@ registry_packages = [ "crates:liboliphaunt-native-linux-x64-gnu", "crates:liboliphaunt-native-macos-arm64", "crates:liboliphaunt-native-windows-x64-msvc", + "crates:oliphaunt-tools", "crates:oliphaunt-tools-linux-arm64-gnu", "crates:oliphaunt-tools-linux-x64-gnu", "crates:oliphaunt-tools-macos-arm64", diff --git a/src/sdks/rust/src/liboliphaunt/root/runtime.rs b/src/sdks/rust/src/liboliphaunt/root/runtime.rs index 49cf3cc4..f1b08e8a 100644 --- a/src/sdks/rust/src/liboliphaunt/root/runtime.rs +++ b/src/sdks/rust/src/liboliphaunt/root/runtime.rs @@ -30,7 +30,7 @@ pub(super) fn materialize_runtime( let install_dir = locate_native_install_dir()?; let tools_dir = locate_native_tools_dir(&install_dir).ok_or_else(|| { Error::Engine( - "could not locate native PostgreSQL client tools pg_dump and psql; add the target oliphaunt-tools artifact crate or set OLIPHAUNT_TOOLS_DIR" + "could not locate native PostgreSQL client tools pg_dump and psql; add the oliphaunt-tools Cargo facade or set OLIPHAUNT_TOOLS_DIR" .to_owned(), ) })?; diff --git a/tools/policy/check-sdk-parity.sh b/tools/policy/check-sdk-parity.sh index a59fb5f8..2a633d2b 100755 --- a/tools/policy/check-sdk-parity.sh +++ b/tools/policy/check-sdk-parity.sh @@ -378,7 +378,7 @@ require_text docs/maintainers/sdk-parity-policy.md "## Artifact Resolution" \ "SDK parity docs must include the artifact-resolution contract" require_text docs/maintainers/sdk-parity-policy.md "Explicit local override" \ "SDK parity docs must include explicit local override paths in the artifact-resolution matrix" -require_text docs/maintainers/sdk-parity-policy.md "split \`oliphaunt-tools-*\` Cargo artifact crates copied into the runtime cache" \ +require_text docs/maintainers/sdk-parity-policy.md "\`oliphaunt-tools\` Cargo facade selecting split \`oliphaunt-tools-*\` payload crates for the runtime cache" \ "SDK parity docs must describe Rust split tools Cargo artifact resolution" require_text docs/maintainers/sdk-parity-policy.md "\`OLIPHAUNT_RESOURCES_DIR\`" \ "SDK parity docs must document Rust's explicit local runtime-resource override" diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 3896f6bb..21099f87 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -309,6 +309,7 @@ def liboliphaunt_native_expected_registry_packages() -> set[str]: "npm:@oliphaunt/icu", "maven:dev.oliphaunt.runtime:oliphaunt-icu", "maven:dev.oliphaunt.runtime:liboliphaunt-runtime-resources", + "crates:oliphaunt-tools", *{f"crates:liboliphaunt-native-{target.target}" for target in runtime_targets}, *{f"crates:oliphaunt-tools-{target.target}" for target in tools_targets}, *npm_registry_packages("liboliphaunt-native", "native-runtime", "typescript-native-direct"), @@ -489,6 +490,7 @@ def check_liboliphaunt(findings: list[Finding]) -> None: and "missing oliphaunt-tools native release asset" in native_packager and "extract_archive(tools_archive, tools_root)" in native_packager and "validate_tools_target_pair" in native_packager + and "write_tools_facade_crate" in native_packager and "package_base=TOOLS_PRODUCT" in native_packager and 'artifact_product=TOOLS_PRODUCT' in native_packager and 'tool_set="runtime"' in native_packager diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index ce944be3..50e6c602 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1507,6 +1507,7 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None or "missing oliphaunt-tools native release asset" not in native_packager_source or "extract_archive(tools_archive, tools_root)" not in native_packager_source or "validate_tools_target_pair" not in native_packager_source + or "write_tools_facade_crate" not in native_packager_source or 'tool_set="runtime"' not in native_packager_source or 'tool_set="tools"' not in native_packager_source or "package_base=TOOLS_PRODUCT" not in native_packager_source diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index 34422e3b..d83e3da0 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -1581,11 +1581,14 @@ def native_runtime_artifact_manifests(source_root: Path, *, include_parts: bool return [] manifests = [ *source_root.glob("liboliphaunt-native-*/Cargo.toml"), + source_root / "oliphaunt-tools" / "Cargo.toml", *source_root.glob("oliphaunt-tools-*/Cargo.toml"), ] result: list[Path] = [] seen: set[Path] = set() for manifest in sorted(manifests): + if not manifest.is_file(): + continue if manifest in seen: continue seen.add(manifest) diff --git a/tools/release/package_liboliphaunt_cargo_artifacts.py b/tools/release/package_liboliphaunt_cargo_artifacts.py index 4c32d390..2da8b284 100644 --- a/tools/release/package_liboliphaunt_cargo_artifacts.py +++ b/tools/release/package_liboliphaunt_cargo_artifacts.py @@ -7,6 +7,7 @@ import hashlib import json import os +import re import shutil import subprocess import sys @@ -25,6 +26,7 @@ KIND = "native-runtime" TOOLS_PRODUCT = "oliphaunt-tools" TOOLS_KIND = "native-tools" +TOOLS_FACADE_TEMPLATE = ROOT / "src/runtimes/liboliphaunt/native/crates/tools" SURFACE = "rust-native-direct" CRATES_IO_MAX_BYTES = 10 * 1024 * 1024 DEFAULT_PART_BYTES = 7 * 1024 * 1024 @@ -660,6 +662,71 @@ def validate_tools_target_pair( fail(f"{tools_target.id} must use Cargo target triple {runtime_target.triple}") +def rust_artifact_cargo_target_cfg(target: artifact_targets.ArtifactTarget) -> str: + if target.target == "linux-arm64-gnu": + return 'all(target_os = "linux", target_arch = "aarch64", target_env = "gnu")' + if target.target == "linux-x64-gnu": + return 'all(target_os = "linux", target_arch = "x86_64", target_env = "gnu")' + if target.target == "macos-arm64": + return 'all(target_os = "macos", target_arch = "aarch64")' + if target.target == "windows-x64-msvc": + return 'all(target_os = "windows", target_arch = "x86_64", target_env = "msvc")' + fail(f"unsupported Cargo target cfg for {target.id}") + + +def write_tools_facade_crate( + source_root: Path, + *, + version: str, + tools_targets: list[artifact_targets.ArtifactTarget], +) -> GeneratedPackage: + crate_dir = source_root / TOOLS_PRODUCT + if crate_dir.exists(): + fail(f"duplicate generated {TOOLS_PRODUCT} source crate: {rel(crate_dir)}") + shutil.copytree( + TOOLS_FACADE_TEMPLATE, + crate_dir, + ignore=shutil.ignore_patterns("target"), + ) + cargo_toml = crate_dir / "Cargo.toml" + text = cargo_toml.read_text(encoding="utf-8") + text = text.replace( + "repository.workspace = true", + 'repository = "https://github.com/f0rr0/oliphaunt"', + ).replace( + "homepage.workspace = true", + 'homepage = "https://oliphaunt.dev"', + ) + text, count = re.subn(r'(?m)^version = "[^"]+"$', f'version = "{version}"', text, count=1) + if count != 1: + fail(f"{rel(cargo_toml)} must declare exactly one package version") + dependency_blocks = [] + for target in sorted(tools_targets, key=lambda item: item.target): + package = cargo_package_name(target.target, package_base=TOOLS_PRODUCT) + dependency_blocks.append( + "\n".join( + [ + "", + f"[target.'cfg({rust_artifact_cargo_target_cfg(target)})'.dependencies]", + f'{package} = {{ version = "={version}", path = "../{package}" }}', + ] + ) + ) + if "\n[workspace]" not in text: + text = text.rstrip() + "\n\n[workspace]\n" + text = text.rstrip() + "\n" + "\n".join(dependency_blocks) + "\n" + cargo_toml.write_text(text, encoding="utf-8") + return GeneratedPackage( + name=TOOLS_PRODUCT, + manifest_path=cargo_toml, + crate_path=None, + target="portable", + product=TOOLS_PRODUCT, + kind=TOOLS_KIND, + role="facade", + ) + + def package_payload( payload_root: Path, source_root: Path, @@ -885,10 +952,12 @@ def main(argv: list[str]) -> int: targets = [target for target in targets if target.target in selected] packages: list[GeneratedPackage] = [] + selected_tools_targets: list[artifact_targets.ArtifactTarget] = [] for target in targets: tools_target = tools_targets.get(target.target) if tools_target is None: fail(f"missing oliphaunt-tools Cargo artifact target for {target.target}") + selected_tools_targets.append(tools_target) packages.extend( package_target( target, @@ -901,6 +970,13 @@ def main(argv: list[str]) -> int: part_bytes=args.part_bytes, ) ) + packages.append( + write_tools_facade_crate( + source_root, + version=args.version, + tools_targets=selected_tools_targets, + ) + ) write_packages_manifest(packages, output_dir) print("generated liboliphaunt native Cargo artifact crates:") for package in packages: diff --git a/tools/release/release.py b/tools/release/release.py index eee1185a..62f207f9 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -744,13 +744,10 @@ def render_oliphaunt_release_cargo_toml(source: str, native_version: str, broker published_only=True, ): crate = package_liboliphaunt_cargo_artifacts.cargo_package_name(target.target) - tools_crate = package_liboliphaunt_cargo_artifacts.cargo_package_name( - target.target, - package_base=package_liboliphaunt_cargo_artifacts.TOOLS_PRODUCT, - ) + tools_facade = package_liboliphaunt_cargo_artifacts.TOOLS_PRODUCT cfg = rust_artifact_cargo_target_cfg(target) target_dependencies.setdefault(cfg, []).append(f'{crate} = {{ version = "={native_version}" }}') - target_dependencies.setdefault(cfg, []).append(f'{tools_crate} = {{ version = "={native_version}" }}') + target_dependencies.setdefault(cfg, []).append(f'{tools_facade} = {{ version = "={native_version}" }}') for target in artifact_targets.artifact_targets( product="oliphaunt-broker", kind="broker-helper", @@ -781,13 +778,18 @@ def validate_generated_oliphaunt_release_artifact_coverage(manifest_path: Path) + ", ".join(missing_broker) ) + native_version = current_product_version("liboliphaunt-native") native_targets = artifact_targets.artifact_targets( product="liboliphaunt-native", kind="native-runtime", surface="rust-native-direct", published_only=True, ) - native_crates = cargo_registry_packages("liboliphaunt-native") + native_runtime_crates = { + package_liboliphaunt_cargo_artifacts.cargo_package_name(target.target) + for target in native_targets + } + native_crates = set(cargo_registry_packages("liboliphaunt-native")) if not native_crates: target_names = ", ".join(target.target for target in native_targets) fail( @@ -797,12 +799,27 @@ def validate_generated_oliphaunt_release_artifact_coverage(manifest_path: Path) "artifact packages. Split/size native runtime artifacts into crates.io-sized " "packages before publishing oliphaunt-rust." ) - missing_native = [crate for crate in native_crates if f"{crate} = " not in manifest] + tools_facade = package_liboliphaunt_cargo_artifacts.TOOLS_PRODUCT + missing_native = sorted( + crate for crate in native_runtime_crates if f'{crate} = {{ version = "={native_version}" }}' not in manifest + ) if missing_native: fail( "generated oliphaunt release source is missing native runtime Cargo artifact dependencies: " + ", ".join(missing_native) ) + if f'{tools_facade} = {{ version = "={native_version}" }}' not in manifest: + fail(f"generated oliphaunt release source is missing native tools facade dependency {tools_facade}") + direct_tool_deps = sorted( + crate + for crate in native_crates + if crate.startswith(f"{tools_facade}-") and f"{crate} = " in manifest + ) + if direct_tool_deps: + fail( + "generated oliphaunt release source must depend on oliphaunt-tools, not target tools crates: " + + ", ".join(direct_tool_deps) + ) def render_oliphaunt_wasix_release_cargo_toml(source: str, runtime_version: str) -> str: @@ -897,12 +914,9 @@ def prepare_oliphaunt_release_source(version: str) -> Path: crate = package_liboliphaunt_cargo_artifacts.cargo_package_name(target.target) if f'{crate} = {{ version = "={native_version}" }}' not in rendered: fail(f"generated oliphaunt release source is missing native runtime artifact dependency {crate}") - tools_crate = package_liboliphaunt_cargo_artifacts.cargo_package_name( - target.target, - package_base=package_liboliphaunt_cargo_artifacts.TOOLS_PRODUCT, - ) - if f'{tools_crate} = {{ version = "={native_version}" }}' not in rendered: - fail(f"generated oliphaunt release source is missing native tools artifact dependency {tools_crate}") + tools_facade = package_liboliphaunt_cargo_artifacts.TOOLS_PRODUCT + if f'{tools_facade} = {{ version = "={native_version}" }}' not in rendered: + fail(f"generated oliphaunt release source is missing native tools facade dependency {tools_facade}") for target in artifact_targets.artifact_targets( product="oliphaunt-broker", kind="broker-helper", @@ -2836,14 +2850,17 @@ def liboliphaunt_cargo_artifact_crates(version: str) -> list[tuple[str, Path | N ) for target in native_targets } + expected_facades = {package_liboliphaunt_cargo_artifacts.TOOLS_PRODUCT} + expected_registry_crates = expected_aggregators | expected_facades configured_crates = set(cratesio_product_crates("liboliphaunt-native")) - if configured_crates != expected_aggregators: + if configured_crates != expected_registry_crates: fail( "liboliphaunt-native crates.io packages must match native Rust runtime/tool artifact targets: " - f"expected={sorted(expected_aggregators)}, configured={sorted(configured_crates)}" + f"expected={sorted(expected_registry_crates)}, configured={sorted(configured_crates)}" ) seen_aggregators: set[str] = set() + seen_facades: set[str] = set() expected_part_crates: set[Path] = set() for item in packages_data: if not isinstance(item, dict): @@ -2868,6 +2885,12 @@ def liboliphaunt_cargo_artifact_crates(version: str) -> list[tuple[str, Path | N if crate_path is not None: fail(f"liboliphaunt native artifact aggregator {name} must publish from source after part crates") seen_aggregators.add(name) + elif role == "facade": + if name not in expected_facades: + fail(f"unexpected liboliphaunt native tools facade crate {name}") + if crate_path is not None: + fail(f"liboliphaunt native tools facade {name} must publish from source after target tool crates") + seen_facades.add(name) else: fail(f"unsupported liboliphaunt generated Cargo artifact role {role!r}") packages.append((name, crate_path, source_manifest, role)) @@ -2876,6 +2899,11 @@ def liboliphaunt_cargo_artifact_crates(version: str) -> list[tuple[str, Path | N "generated liboliphaunt native artifact aggregators do not match configured crates: " f"expected={sorted(expected_aggregators)}, generated={sorted(seen_aggregators)}" ) + if seen_facades != expected_facades: + fail( + "generated liboliphaunt native tools facades do not match configured crates: " + f"expected={sorted(expected_facades)}, generated={sorted(seen_facades)}" + ) unexpected = sorted( path.name for path in output_dir.glob("*.crate") @@ -2978,6 +3006,9 @@ def publish_liboliphaunt_cargo_artifacts(head_ref: str) -> None: for crate, _crate_path, manifest_path, role in packages: if role == "aggregator": cargo_publish_manifest(crate, version, manifest_path) + for crate, _crate_path, manifest_path, role in packages: + if role == "facade": + cargo_publish_manifest(crate, version, manifest_path) verify_generated_cratesio_packages_published( "liboliphaunt-native", [crate for crate, _crate_path, _manifest_path, _role in packages], diff --git a/tools/release/sync-example-lockfiles.mjs b/tools/release/sync-example-lockfiles.mjs index 00318983..ad9af254 100755 --- a/tools/release/sync-example-lockfiles.mjs +++ b/tools/release/sync-example-lockfiles.mjs @@ -145,6 +145,7 @@ function nativeTauriPackages(versions) { packageSpec('oliphaunt', versions.oliphaunt), packageSpec('oliphaunt-build', versions.oliphauntBuild), packageSpec('liboliphaunt-native-linux-x64-gnu', versions.nativeRuntime), + packageSpec('oliphaunt-tools', versions.nativeRuntime), packageSpec('oliphaunt-tools-linux-x64-gnu', versions.nativeRuntime), packageSpec('oliphaunt-broker-linux-x64-gnu', versions.brokerLinuxX64), ...exampleExtensions.map((extension) =>