From 5cfcee12172d571c787b6d34af8f8e36183fc186 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Wed, 24 Jun 2026 11:17:30 +0000 Subject: [PATCH 001/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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/308] 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) => From f4f946c4b4ed64c1bdd78a731dd1892bc8364b50 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 01:49:57 +0000 Subject: [PATCH 138/308] chore: port ci planner to bun --- .github/workflows/ci.yml | 2 +- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 52 +- docs/internal/IMPLEMENTATION_CHECKLIST.md | 31 +- tools/graph/ci_plan.mjs | 822 ++++++++++++++++++ tools/graph/ci_plan.py | 665 -------------- tools/graph/graph.py | 27 +- tools/policy/check-release-policy.py | 189 +++- tools/policy/check-repo-structure.sh | 28 +- tools/policy/check-tooling-stack.sh | 4 +- tools/policy/python-entrypoints.allowlist | 1 - tools/release/check_artifact_targets.py | 63 +- 11 files changed, 1141 insertions(+), 743 deletions(-) create mode 100644 tools/graph/ci_plan.mjs delete mode 100644 tools/graph/ci_plan.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ccc55d0..9cd88e1b 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 tools/graph/ci_plan.py + run: tools/dev/bun.sh tools/graph/ci_plan.mjs - 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 143d3d9a..fde56531 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -5,7 +5,7 @@ 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 +## Active Continuation Queue: 2026-06-27 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 @@ -78,15 +78,55 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Rechecked the root/tool crate split requested for PostgreSQL + client tools. Native root runtime packages/crates are limited by + `tools/release/native-runtime-payload-policy.json` to `initdb`, `pg_ctl`, and + `postgres`, while split `oliphaunt-tools` packages/crates carry only + `pg_dump` and `psql`. WASIX root crates carry `postgres` and `initdb`, reject + `pg_ctl`, `pg_dump`, and `psql` in the root archive, and publish + `pg_dump.wasix.wasm` plus `psql.wasix.wasm` through `oliphaunt-wasix-tools` + and tools-AOT crates. Fresh checks passed: `python3 + tools/release/check_consumer_shape.py --products-json + '["liboliphaunt-native","liboliphaunt-wasix","oliphaunt-rust","oliphaunt-js"]'`, + `python3 tools/release/check_artifact_targets.py`, `tools/dev/bun.sh + tools/policy/check-wasix-release-dependency-invariants.mjs`, `cargo check -p + oliphaunt-tools --locked`, `cargo test -p oliphaunt-tools --locked`, `cargo + check -p oliphaunt-wasix-tools --locked`, `cargo check -p oliphaunt-wasix + --no-default-features --features tools --locked`, and `bash + examples/tools/check-examples.sh`. +- 2026-06-27: Continued the tooling cleanup by porting the shared CI affected + planner from `tools/graph/ci_plan.py` to `tools/graph/ci_plan.mjs`. The Builds + workflow now invokes the Bun planner directly, `tools/graph/graph.py` and + release policy checks query its JSON subcommands, and stale Python inventory + references were removed. Fresh checks passed: workflow-dispatch planner + smoke with `tools/dev/bun.sh tools/graph/ci_plan.mjs`, `tools/graph/graph.py + check`, `python3 tools/policy/check-release-policy.py`, and `bash + tools/policy/check-repo-structure.sh`. +- 2026-06-27: Added and pushed the native Rust `oliphaunt-tools` Cargo facade + crate so consumer manifests can depend on the facade while Cargo selects the + target `oliphaunt-tools-*` payload crate. The Rust SDK release renderer now + emits `oliphaunt-tools` instead of direct target tools dependencies, native + liboliphaunt Cargo publishing orders part crates, target aggregators, then + facade crates, and local-registry/example checks expect the facade plus + payload crate shape. Fresh checks passed: `cargo check -p oliphaunt-tools + --locked`, `cargo test -p oliphaunt-tools --locked`, `cargo package -p + oliphaunt-tools --locked --allow-dirty --no-verify`, `tools/release/release.py + check`, `python3 tools/release/check_release_metadata.py`, `python3 + tools/release/check_consumer_shape.py`, `python3 + tools/release/check_artifact_targets.py`, `bash tools/policy/check-sdk-parity.sh`, + `examples/tools/with-local-registries.sh cargo metadata --manifest-path + examples/tauri/src-tauri/Cargo.toml --locked --format-version 1`, and `bash + examples/tools/check-examples.sh` with the stale generated registry index + temporarily hidden from checksum comparison. - 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 + `tools/release/release-artifact-targets.mjs`; `tools/graph/ci_plan.mjs` 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 + `GITHUB_EVENT_NAME=workflow_dispatch tools/dev/bun.sh tools/graph/ci_plan.mjs`, 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 @@ -707,8 +747,8 @@ until the current-state gates here are checked with fresh local evidence. 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. + the workflow now invokes `tools/dev/bun.sh tools/graph/ci_plan.mjs` directly, keeping + the shared planner as the single Bun 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 @@ -820,7 +860,7 @@ until the current-state gates here are checked with fresh local evidence. 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, + the direct Bun helper smoke, pull-request-mode `ci_plan.mjs` 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. diff --git a/docs/internal/IMPLEMENTATION_CHECKLIST.md b/docs/internal/IMPLEMENTATION_CHECKLIST.md index 403c851b..2013a3c6 100644 --- a/docs/internal/IMPLEMENTATION_CHECKLIST.md +++ b/docs/internal/IMPLEMENTATION_CHECKLIST.md @@ -46,7 +46,7 @@ or CI/build output proves the contract. `tools/graph/graph.py check` passes and reports Moon projects/release products. - [x] Stable CI job names are derived from Moon task `ci-*` tags. Evidence: - `tools/graph/ci_plan.py` and `tools/policy/check-moon-product-graph.mjs`. + `tools/graph/ci_plan.mjs` and `tools/policy/check-moon-product-graph.mjs`. - [x] Runtime target fan-out is metadata-driven, not hardcoded in mobile jobs. Evidence: focused mobile planner output narrows native runtime and native extension matrices by surface, and `tools/policy/check-release-policy.py` @@ -111,7 +111,7 @@ or CI/build output proves the contract. release-wide `extension-packages` path may stage all exact-extension products. - [x] Builds workflow has a builder-only aggregate. Evidence: - `tools/graph/ci_plan.py` emits `builder_jobs`, and the `Builds` GitHub job + `tools/graph/ci_plan.mjs` emits `builder_jobs`, and the `Builds` GitHub job fails if any selected runtime, helper runtime, SDK package, exact-extension artifact/package, or mobile app builder fails. Local planner probe confirms a full run selects runtime, WASIX, helper, SDK, extension, and mobile app @@ -167,7 +167,7 @@ or CI/build output proves the contract. the platform app artifact path. They do not build WASIX extension artifacts and do not start emulator/simulator E2E jobs in the `Builds` workflow. - [x] Mobile-focused extension artifact builders are target-scoped. Evidence: - direct `tools/graph/ci_plan.py` probes show Android mobile builds select + direct `tools/graph/ci_plan.mjs` probes show Android mobile builds select native extension artifacts for `android-arm64-v8a` and `android-x86_64` only, iOS mobile builds select `ios-xcframework` only, and standalone extension-package builds still select every published native @@ -266,7 +266,7 @@ or CI/build output proves the contract. the release artifact gate because it depends on the staged mobile app artifacts that `Builds` validates. - [x] Full non-PR Builds runs are deliverable builders by default. Evidence: - `tools/graph/ci_plan.py::plan_for_full_run()` starts from `BUILDER_JOBS` + `tools/graph/ci_plan.mjs::planForFullRun()` starts from `BUILDER_JOBS` plus the WASIX AOT target planner dependency, and `tools/policy/check-release-policy.py` rejects full-run plans that select non-builder side lanes such as `repo`, `release-intent`, docs, regressions, @@ -541,9 +541,10 @@ Run before claiming this architecture complete: - [x] `bash -n tools/release/build-sdk-ci-artifacts.sh src/sdks/swift/tools/check-sdk.sh` - [x] `python3 -m py_compile tools/release/release.py - tools/release/build-extension-ci-artifacts.py tools/graph/ci_plan.py + tools/release/build-extension-ci-artifacts.py tools/release/check_artifact_targets.py tools/release/check_release_metadata.py` +- [x] `tools/dev/bun.sh tools/graph/ci_plan.mjs --help` - [x] `python3 tools/graph/graph.py check` - [x] `node tools/policy/check-moon-product-graph.mjs` - [x] `python3 tools/release/check_artifact_targets.py` @@ -570,14 +571,14 @@ Run before claiming this architecture complete: verifies local package shape only; publishable SDK artifact envelopes use explicit `package-artifacts` builder tasks, and runtime/extension/mobile artifacts stay in target-scoped builder jobs. -- [x] `python3 tools/graph/ci_plan.py` for a full run now selects only +- [x] `tools/dev/bun.sh tools/graph/ci_plan.mjs` for a full run now selects only `affected` plus 21 artifact-producing builder jobs. WASIX AOT target fan-out is emitted by the affected plan as `liboliphaunt_wasix_aot_runtime_matrix`; there is no separate AOT planner job in the Builds workflow. - [x] `GITHUB_EVENT_NAME=workflow_dispatch NATIVE_TARGET=all WASM_TARGET=linux-x64-gnu MOBILE_TARGET=all - python3 tools/graph/ci_plan.py` now selects only + tools/dev/bun.sh tools/graph/ci_plan.mjs` 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,24 +613,24 @@ 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 tools/graph/ci_plan.py` + WASM_TARGET=all MOBILE_TARGET=ios tools/dev/bun.sh tools/graph/ci_plan.mjs` - [x] `GITHUB_EVENT_NAME=workflow_dispatch NATIVE_TARGET=all - WASM_TARGET=all MOBILE_TARGET=android python3 tools/graph/ci_plan.py` -- [x] `tools/graph/ci_plan.py` direct probe for + WASM_TARGET=all MOBILE_TARGET=android tools/dev/bun.sh tools/graph/ci_plan.mjs` +- [x] `tools/graph/ci_plan.mjs` direct probe for `{"extension-artifacts-native:build-target"}` selects `extension-artifacts-native` without `liboliphaunt-native`, proving extension artifact-only work does not create a native-runtime waterfall. -- [x] `tools/graph/ci_plan.py` direct probes for +- [x] `tools/graph/ci_plan.mjs` direct probes for `oliphaunt-react-native:mobile-build-android` and `oliphaunt-react-native:mobile-build-ios` select only Android or iOS native extension artifacts respectively. -- [x] `tools/graph/ci_plan.py` direct probe for +- [x] `tools/graph/ci_plan.mjs` direct probe for `oliphaunt-react-native:package-artifacts` selects `react-native-sdk-package`, `mobile-build-android`, `mobile-build-ios`, `kotlin-sdk-package`, `swift-sdk-package`, Android/iOS native runtime builders, and `mobile-extension-packages`; native target selection is exactly `android-arm64-v8a`, `android-x86_64`, and `ios-xcframework`. -- [x] `tools/graph/ci_plan.py` direct probe for a single +- [x] `tools/graph/ci_plan.mjs` direct probe for a single `oliphaunt-extension-postgis` change with aggregate artifact/package tasks selects only `oliphaunt-extension-postgis`, emits 6 native rows, and emits 1 WASIX row. @@ -670,10 +671,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 tools/graph/ci_plan.py` + WASM_TARGET=all MOBILE_TARGET=all tools/dev/bun.sh tools/graph/ci_plan.mjs` - [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 tools/graph/ci_plan.py` + WASM_TARGET=all MOBILE_TARGET=android tools/dev/bun.sh tools/graph/ci_plan.mjs` 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/graph/ci_plan.mjs b/tools/graph/ci_plan.mjs new file mode 100644 index 00000000..ed1a0159 --- /dev/null +++ b/tools/graph/ci_plan.mjs @@ -0,0 +1,822 @@ +#!/usr/bin/env bun +// Map Moon affected tasks onto stable GitHub Actions jobs. +// +// Moon is the only project/task graph. Stable GitHub job names are selected +// from Moon task tags named `ci-`. GitHub Actions still owns platform +// matrix fan-out because runner OS, native target triples, and simulator/device +// targets are CI execution details, not source projects. +import { execFileSync } from "node:child_process"; +import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs"; +import { homedir } from "node:os"; +import path from "node:path"; + +import { + brokerRuntimeMatrix, + extensionArtifactsNativeMatrix, + extensionArtifactsWasixMatrix, + liboliphauntNativeAndroidRuntimeMatrix, + liboliphauntNativeDesktopRuntimeMatrix, + liboliphauntNativeIosRuntimeMatrix, + liboliphauntNativeRuntimeTargetsForSurface, + liboliphauntWasixAotRuntimeMatrix, + nodeDirectRuntimeMatrix, + reactNativeAndroidMobileAppMatrix, +} from "../release/artifact_target_matrix.mjs"; +import { compareText, exactExtensionProducts } from "../release/release-artifact-targets.mjs"; + +const ROOT = path.resolve(import.meta.dir, "../.."); +const PREFIX = "ci_plan.mjs"; + +export const BASE_JOBS = new Set(["affected"]); +export const ALWAYS_JOBS = new Set(BASE_JOBS); +export const BUILDER_JOBS = new Set([ + "broker-runtime", + "extension-artifacts-native", + "extension-artifacts-wasix", + "extension-packages", + "js-sdk-package", + "kotlin-sdk-package", + "liboliphaunt-native-android", + "liboliphaunt-native-desktop", + "liboliphaunt-native-ios", + "liboliphaunt-native-release-assets", + "liboliphaunt-wasix-aot", + "liboliphaunt-wasix-release-assets", + "liboliphaunt-wasix-runtime", + "mobile-build-android", + "mobile-build-ios", + "mobile-extension-packages", + "node-direct", + "react-native-sdk-package", + "rust-sdk-package", + "swift-sdk-package", + "wasix-rust-package", +]); +const NATIVE_RUNTIME_JOBS = new Set([ + "liboliphaunt-native-android", + "liboliphaunt-native-desktop", + "liboliphaunt-native-ios", +]); +const NATIVE_RUNTIME_TASKS = new Set([ + "liboliphaunt-native:release-runtime", + "liboliphaunt-native:release-runtime-desktop", + "liboliphaunt-native:release-runtime-mobile-target", +]); +export const WASM_RUNTIME_JOBS = new Set([ + "liboliphaunt-wasix-runtime", + "liboliphaunt-wasix-aot", + "liboliphaunt-wasix-release-assets", +]); +const AGGREGATE_ARTIFACT_JOBS = new Set(["liboliphaunt-native-release-assets"]); +const WASM_RUNTIME_PORTABLE_TASK = "liboliphaunt-wasix:runtime-portable"; +const WASM_RUNTIME_AOT_TASK = "liboliphaunt-wasix:runtime-aot"; +const MOBILE_JOB_SURFACES = { + "mobile-build-android": "react-native-android", + "mobile-build-ios": "react-native-ios", +}; +const ANDROID_MOBILE_JOBS = new Set(["mobile-build-android"]); +const IOS_MOBILE_JOBS = new Set(["mobile-build-ios"]); +const EXTENSION_ARTIFACT_CONSUMER_JOBS = new Set(["extension-packages", "mobile-extension-packages"]); +const WASIX_EXTENSION_ARTIFACT_PORTABLE_CONSUMER_JOBS = new Set([ + "extension-packages", + "extension-artifacts-wasix", +]); +const MOBILE_SMOKE_EXTENSION_PRODUCTS = new Set(["oliphaunt-extension-vector"]); + +function fail(message) { + console.error(`${PREFIX}: ${message}`); + process.exit(2); +} + +function moonBin() { + if (process.env.MOON_BIN) { + return process.env.MOON_BIN; + } + for (const candidate of [ + path.join(homedir(), ".proto/shims/moon"), + path.join(homedir(), ".proto/bin/moon"), + ]) { + if (existsSync(candidate)) { + return candidate; + } + } + return "moon"; +} + +function commandJson(command, args) { + const output = execFileSync(command, args, { + cwd: ROOT, + env: process.env, + encoding: "utf8", + maxBuffer: 100 * 1024 * 1024, + }); + return JSON.parse(output); +} + +function moon(args) { + return commandJson(moonBin(), args); +} + +function affectedProjectsAndTasks() { + const summary = commandJson("tools/dev/bun.sh", ["tools/graph/affected.mjs", "summary"]); + return { + directProjects: new Set(stringList(summary.directProjects ?? [])), + projects: new Set(stringList(summary.projects ?? [])), + directTasks: new Set(stringList(summary.directTasks ?? [])), + }; +} + +function stringList(value) { + if (!Array.isArray(value)) { + fail("expected a JSON string list"); + } + return value.map((item) => String(item)).sort(compareText); +} + +function setUnion(...sets) { + const result = new Set(); + for (const set of sets) { + for (const item of set) { + result.add(item); + } + } + return result; +} + +function intersects(left, right) { + for (const item of left) { + if (right.has(item)) { + return true; + } + } + return false; +} + +function difference(left, right) { + return new Set([...left].filter((item) => !right.has(item))); +} + +function sorted(set) { + return [...set].sort(compareText); +} + +function names(value) { + if (value !== null && !Array.isArray(value) && typeof value === "object") { + return Object.keys(value).sort(compareText); + } + 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 sorted(result); + } + return []; +} + +export function moonCiJobTargets() { + const queried = moon(["query", "tasks"]); + const tasksByProject = queried.tasks; + if (tasksByProject === null || Array.isArray(tasksByProject) || typeof tasksByProject !== "object") { + fail("moon query tasks did not return a tasks object"); + } + + const jobs = new Map(); + for (const [projectId, projectTasks] of Object.entries(tasksByProject)) { + if (projectTasks === null || Array.isArray(projectTasks) || typeof projectTasks !== "object") { + continue; + } + for (const [taskId, task] of Object.entries(projectTasks)) { + if (task === null || Array.isArray(task) || typeof task !== "object") { + continue; + } + const target = String(task.target || `${projectId}:${taskId}`); + const tags = Array.isArray(task.tags) ? task.tags : []; + for (const tag of tags) { + if (typeof tag === "string" && tag.startsWith("ci-")) { + const job = tag.slice("ci-".length); + if (!jobs.has(job)) { + jobs.set(job, new Set()); + } + jobs.get(job).add(target); + } + } + } + } + return Object.fromEntries( + [...jobs.entries()] + .sort(([left], [right]) => compareText(left, right)) + .map(([job, targets]) => [job, sorted(targets)]), + ); +} + +export const CI_JOB_TARGETS = moonCiJobTargets(); +export const ALL_BUILDER_JOBS = difference( + setUnion(BUILDER_JOBS, WASM_RUNTIME_JOBS, AGGREGATE_ARTIFACT_JOBS), + ALWAYS_JOBS, +); +export const COVERAGE_JOB_PRODUCTS = Object.fromEntries( + Object.entries(CI_JOB_TARGETS) + .filter(([, targets]) => targets.some((target) => target.endsWith(":coverage"))) + .map(([job, targets]) => [job, targets[0].split(":", 1)[0]]) + .sort(([left], [right]) => compareText(left, right)), +); +export const CI_JOBS_CONFIG = { + always_jobs: sorted(ALWAYS_JOBS), + ci_job_targets: CI_JOB_TARGETS, + coverage_job_products: COVERAGE_JOB_PRODUCTS, + wasm_runtime_jobs: sorted(WASM_RUNTIME_JOBS), +}; + +export function jobTargetsForJobs(jobs) { + return Object.fromEntries( + sorted(jobs) + .filter((job) => CI_JOB_TARGETS[job] !== undefined) + .map((job) => [job, CI_JOB_TARGETS[job]]), + ); +} + +function emptyMatrix() { + return { include: [] }; +} + +export function jobsForTargets(targets, { allowedJobs = undefined } = {}) { + const jobs = new Set(); + for (const [job, jobTargets] of Object.entries(CI_JOB_TARGETS)) { + if (allowedJobs !== undefined && !allowedJobs.has(job)) { + continue; + } + if (intersects(targets, new Set(jobTargets))) { + jobs.add(job); + } + } + return jobs; +} + +export function addImpliedJobs(jobs, tasks) { + if ( + intersects( + jobs, + new Set(["liboliphaunt-wasix-runtime", "liboliphaunt-wasix-aot", "liboliphaunt-wasix-release-assets"]), + ) || + intersects(new Set([WASM_RUNTIME_PORTABLE_TASK, WASM_RUNTIME_AOT_TASK]), tasks) + ) { + for (const job of WASM_RUNTIME_JOBS) { + jobs.add(job); + } + } + + if (intersects(jobs, new Set(Object.keys(MOBILE_JOB_SURFACES)))) { + jobs.add("mobile-extension-packages"); + jobs.add("react-native-sdk-package"); + } + + if (intersects(jobs, ANDROID_MOBILE_JOBS)) { + jobs.add("liboliphaunt-native-android"); + jobs.add("kotlin-sdk-package"); + } + + if (intersects(jobs, IOS_MOBILE_JOBS)) { + jobs.add("liboliphaunt-native-ios"); + jobs.add("swift-sdk-package"); + } + + if (jobs.has("swift-sdk-package")) { + jobs.add("liboliphaunt-native-ios"); + } + + if (jobs.has("liboliphaunt-native-release-assets")) { + for (const job of NATIVE_RUNTIME_JOBS) { + jobs.add(job); + } + } + + if (intersects(jobs, new Set(["extension-artifacts-native", "extension-artifacts-wasix"]))) { + jobs.add("extension-packages"); + } + + if (intersects(jobs, EXTENSION_ARTIFACT_CONSUMER_JOBS)) { + jobs.add("extension-artifacts-native"); + } + + if (intersects(jobs, WASIX_EXTENSION_ARTIFACT_PORTABLE_CONSUMER_JOBS)) { + jobs.add("extension-artifacts-wasix"); + jobs.add("liboliphaunt-wasix-runtime"); + jobs.add("liboliphaunt-wasix-aot"); + } +} + +export function planJobsForAffected(directProjects, tasks) { + const jobs = new Set(ALWAYS_JOBS); + for (const job of jobsForTargets(tasks, { allowedJobs: ALL_BUILDER_JOBS })) { + jobs.add(job); + } + if (intersects(directProjects, new Set(exactExtensionProducts()))) { + jobs.add("extension-artifacts-native"); + jobs.add("extension-artifacts-wasix"); + jobs.add("extension-packages"); + } + if (jobs.has("react-native-sdk-package")) { + for (const job of ANDROID_MOBILE_JOBS) { + jobs.add(job); + } + for (const job of IOS_MOBILE_JOBS) { + jobs.add(job); + } + } + if (directProjects.has("ci-workflows")) { + for (const job of ALL_BUILDER_JOBS) { + jobs.add(job); + } + } + addImpliedJobs(jobs, tasks); + if (intersects(tasks, NATIVE_RUNTIME_TASKS)) { + jobs.add("liboliphaunt-native-release-assets"); + for (const job of NATIVE_RUNTIME_JOBS) { + jobs.add(job); + } + } + return jobs; +} + +export function nativeTargetSubsetForJobs(jobs, tasks) { + if (!intersects(jobs, NATIVE_RUNTIME_JOBS)) { + return null; + } + if (jobs.has("liboliphaunt-native-release-assets")) { + return null; + } + if (intersects(tasks, NATIVE_RUNTIME_TASKS)) { + return null; + } + + const targets = mobileNativeTargetsForJobs(jobs); + if (jobs.has("swift-sdk-package")) { + targets.add("ios-xcframework"); + } + if (jobs.has("kotlin-sdk-package")) { + for (const target of liboliphauntNativeRuntimeTargetsForSurface("maven")) { + targets.add(target); + } + } + return targets.size > 0 ? targets : null; +} + +export function mobileNativeTargetsForJobs(jobs) { + const targets = new Set(); + for (const [job, surface] of Object.entries(MOBILE_JOB_SURFACES)) { + if (jobs.has(job)) { + for (const target of liboliphauntNativeRuntimeTargetsForSurface(surface)) { + targets.add(target); + } + } + } + return targets; +} + +export function mobileExtensionPackageNativeTargets(jobs, selectedTargets) { + if (!jobs.has("mobile-extension-packages")) { + return []; + } + if (selectedTargets !== null && selectedTargets !== undefined) { + return sorted(selectedTargets); + } + return sorted(mobileNativeTargetsForJobs(jobs)); +} + +function focusedMobileNativeTargets(mobileTarget, nativeTarget, focusedMobileJobs) { + const targets = mobileNativeTargetsForJobs(focusedMobileJobs); + if (nativeTarget === "all") { + return targets; + } + if (mobileTarget === "both") { + throw new Error("focused mobile_target=both requires native_target=all"); + } + if (!targets.has(nativeTarget)) { + throw new Error( + `native_target=${nativeTarget} is not valid for mobile_target=${mobileTarget}; expected one of: all, ${sorted(targets).join(", ")}`, + ); + } + return new Set([nativeTarget]); +} + +export function planForPullRequest() { + const base = process.env.MOON_BASE; + const head = process.env.MOON_HEAD; + if (!base || !head) { + throw new Error("MOON_BASE and MOON_HEAD are required for pull_request CI planning"); + } + + const { directProjects, projects, directTasks } = affectedProjectsAndTasks(); + const jobs = planJobsForAffected(directProjects, directTasks); + const selectedNativeTargets = nativeTargetSubsetForJobs(jobs, directTasks); + const reason = + `direct affected projects: ${sorted(directProjects).join(", ") || "(none)"}; ` + + `downstream affected projects: ${sorted(projects).join(", ") || "(none)"}; ` + + `direct affected tasks: ${sorted(directTasks).join(", ") || "(none)"}`; + return { jobs, projects, tasks: directTasks, reason, selectedTargets: selectedNativeTargets }; +} + +export function selectedExtensionProductsForPlan(directProjects, tasks, jobs) { + const extensionJobs = new Set([ + "extension-artifacts-native", + "extension-artifacts-wasix", + "extension-packages", + ...Object.keys(MOBILE_JOB_SURFACES), + ]); + if (!intersects(jobs, extensionJobs)) { + return null; + } + + const exactProducts = new Set(exactExtensionProducts()); + const selected = new Set([...directProjects].filter((project) => exactProducts.has(project))); + for (const target of tasks) { + const project = target.split(":", 1)[0]; + if (exactProducts.has(project)) { + selected.add(project); + } + } + const broadExtensionInputs = new Set([ + "extension-artifacts-native", + "extension-artifacts-wasix", + "extension-contrib-postgres18", + "extension-model", + "extension-packages", + "extensions", + "liboliphaunt-native", + "liboliphaunt-wasix", + "postgres18", + "source-inputs", + "third-party-native", + "third-party-shared", + "third-party-wasix", + ]); + if (intersects(directProjects, broadExtensionInputs)) { + return exactProducts; + } + if (tasks.has("extension-packages:assemble-release") && selected.size === 0) { + return exactProducts; + } + if (jobs.has("extension-packages") && selected.size === 0) { + return exactProducts; + } + if (intersects(jobs, new Set(Object.keys(MOBILE_JOB_SURFACES)))) { + for (const product of MOBILE_SMOKE_EXTENSION_PRODUCTS) { + selected.add(product); + } + } + if (intersects(jobs, new Set(["extension-artifacts-native", "extension-artifacts-wasix"])) && selected.size === 0) { + return exactProducts; + } + if (tasks.has("extension-packages:assemble-mobile") && selected.size === 0) { + return exactProducts; + } + return selected.size > 0 ? selected : null; +} + +export function planForFullRun({ + wasmTarget = "all", + nativeTarget = "all", + mobileTarget = "all", +} = {}) { + if (mobileTarget !== "all") { + const mobileJobsByTarget = { + android: new Set(["mobile-build-android"]), + ios: new Set(["mobile-build-ios"]), + both: new Set(["mobile-build-android", "mobile-build-ios"]), + }; + const focusedMobileJobs = mobileJobsByTarget[mobileTarget]; + if (focusedMobileJobs === undefined) { + throw new Error(`unknown mobile target ${mobileTarget}; expected one of: all, android, ios, both`); + } + const focusedJobs = setUnion(BASE_JOBS, focusedMobileJobs); + addImpliedJobs(focusedJobs, new Set()); + const focusedNativeTargets = focusedMobileNativeTargets(mobileTarget, nativeTarget, focusedMobileJobs); + return { + jobs: focusedJobs, + projects: new Set(["liboliphaunt-native", "oliphaunt-react-native"]), + tasks: targetsForJobs(focusedMobileJobs), + reason: `manual focused mobile CI run for ${mobileTarget}`, + selectedTargets: focusedNativeTargets, + }; + } + + if (nativeTarget !== "all") { + let focusedJobs; + let focusedProjects; + if (nativeTarget.startsWith("android-") || nativeTarget === "ios-xcframework") { + focusedJobs = setUnion( + BASE_JOBS, + new Set([nativeTarget.startsWith("android-") ? "liboliphaunt-native-android" : "liboliphaunt-native-ios"]), + ); + focusedProjects = new Set(["liboliphaunt-native"]); + } else { + focusedJobs = setUnion(BASE_JOBS, new Set(["liboliphaunt-native-desktop", "broker-runtime", "node-direct"])); + focusedProjects = new Set(["liboliphaunt-native", "oliphaunt-broker", "oliphaunt-node-direct"]); + } + addImpliedJobs(focusedJobs, new Set()); + return { + jobs: focusedJobs, + projects: focusedProjects, + tasks: targetsForJobs(focusedJobs), + reason: `manual focused native runtime CI run for ${nativeTarget}`, + selectedTargets: null, + }; + } + + if (wasmTarget !== "all") { + const focusedJobs = setUnion(BASE_JOBS, new Set(["liboliphaunt-wasix-runtime", "liboliphaunt-wasix-aot"])); + return { + jobs: focusedJobs, + projects: new Set(["liboliphaunt-wasix"]), + tasks: targetsForJobs(focusedJobs), + reason: `manual focused WASIX runtime CI run for ${wasmTarget}`, + selectedTargets: null, + }; + } + + const jobs = setUnion(BASE_JOBS, BUILDER_JOBS, WASM_RUNTIME_JOBS); + addImpliedJobs(jobs, targetsForJobs(jobs)); + return { + jobs, + projects: new Set(), + tasks: targetsForJobs(jobs), + reason: "non-PR full CI/runtime run", + selectedTargets: null, + }; +} + +function targetsForJobs(jobs) { + const targets = new Set(); + for (const job of jobs) { + for (const target of CI_JOB_TARGETS[job] ?? []) { + targets.add(target); + } + } + return targets; +} + +function renderPlan({ jobs, projects, tasks, reason, selectedTargets }) { + const selectedExtensionProducts = selectedExtensionProductsForPlan(new Set(), tasks, jobs); + return renderPlanWithSelection({ jobs, projects, tasks, reason, selectedTargets, selectedExtensionProducts }); +} + +function renderPlanWithSelection({ jobs, projects, tasks, reason, selectedTargets, selectedExtensionProducts }) { + return { + jobs: sorted(jobs), + builder_jobs: sorted(new Set([...jobs].filter((job) => BUILDER_JOBS.has(job)))), + job_targets: jobTargetsForJobs(jobs), + projects: sorted(projects), + tasks: sorted(tasks), + liboliphaunt_native_desktop_runtime_matrix: jobs.has("liboliphaunt-native-desktop") + ? liboliphauntNativeDesktopRuntimeMatrix(process.env.NATIVE_TARGET || "all", selectedTargets ?? undefined) + : emptyMatrix(), + liboliphaunt_native_android_runtime_matrix: jobs.has("liboliphaunt-native-android") + ? liboliphauntNativeAndroidRuntimeMatrix(process.env.NATIVE_TARGET || "all", selectedTargets ?? undefined) + : emptyMatrix(), + liboliphaunt_native_ios_runtime_matrix: jobs.has("liboliphaunt-native-ios") + ? liboliphauntNativeIosRuntimeMatrix(process.env.NATIVE_TARGET || "all", selectedTargets ?? undefined) + : emptyMatrix(), + extension_artifacts_native_matrix: jobs.has("extension-artifacts-native") + ? extensionArtifactsNativeMatrix( + process.env.NATIVE_TARGET || "all", + jobs.has("extension-packages") ? undefined : selectedTargets ?? undefined, + selectedExtensionProducts ?? undefined, + ) + : emptyMatrix(), + extension_artifacts_wasix_matrix: jobs.has("extension-artifacts-wasix") + ? extensionArtifactsWasixMatrix("all", selectedExtensionProducts ?? undefined) + : emptyMatrix(), + liboliphaunt_wasix_aot_runtime_matrix: jobs.has("liboliphaunt-wasix-aot") + ? liboliphauntWasixAotRuntimeMatrix(process.env.WASM_TARGET || "all") + : emptyMatrix(), + extension_package_products: sorted(selectedExtensionProducts ?? new Set()), + extension_package_products_csv: sorted(selectedExtensionProducts ?? new Set()).join(","), + mobile_extension_package_native_targets: mobileExtensionPackageNativeTargets(jobs, selectedTargets), + mobile_extension_package_native_targets_csv: mobileExtensionPackageNativeTargets(jobs, selectedTargets).join(","), + react_native_android_mobile_app_matrix: jobs.has("mobile-build-android") + ? reactNativeAndroidMobileAppMatrix(process.env.NATIVE_TARGET || "all", selectedTargets ?? undefined) + : emptyMatrix(), + broker_runtime_matrix: jobs.has("broker-runtime") + ? brokerRuntimeMatrix(process.env.NATIVE_TARGET || "all") + : emptyMatrix(), + node_direct_runtime_matrix: jobs.has("node-direct") + ? nodeDirectRuntimeMatrix(process.env.NATIVE_TARGET || "all") + : emptyMatrix(), + reason, + }; +} + +function sortedValue(value) { + if (Array.isArray(value)) { + return value.map(sortedValue); + } + if (value instanceof Set) { + return sorted(value); + } + if (value !== null && typeof value === "object") { + return Object.fromEntries( + Object.keys(value) + .sort(compareText) + .map((key) => [key, sortedValue(value[key])]), + ); + } + return value; +} + +function output(name, value) { + const rendered = typeof value === "string" ? value : JSON.stringify(sortedValue(value)); + const outputPath = process.env.GITHUB_OUTPUT; + if (outputPath) { + appendFileSync(outputPath, `${name}=${rendered}\n`, "utf8"); + } + console.log(`${name}=${rendered}`); +} + +function writePlanArtifact(plan) { + const file = path.join(ROOT, "target/graph/ci-plan.json"); + mkdirSync(path.dirname(file), { recursive: true }); + writeFileSync(file, `${JSON.stringify(sortedValue(plan), null, 2)}\n`, "utf8"); +} + +export function emitGithubOutputs() { + let planned; + try { + if (process.env.GITHUB_EVENT_NAME === "pull_request") { + const pullRequestPlan = planForPullRequest(); + let directProjects = new Set(); + try { + directProjects = affectedProjectsAndTasks().directProjects; + } catch { + directProjects = new Set(); + } + const selectedExtensionProducts = selectedExtensionProductsForPlan( + directProjects, + pullRequestPlan.tasks, + pullRequestPlan.jobs, + ); + planned = renderPlanWithSelection({ ...pullRequestPlan, selectedExtensionProducts }); + } else { + planned = renderPlan( + planForFullRun({ + wasmTarget: process.env.WASM_TARGET || "all", + nativeTarget: process.env.NATIVE_TARGET || "all", + mobileTarget: process.env.MOBILE_TARGET || "all", + }), + ); + } + } catch (error) { + console.error(`affected planning failed: ${error.message}`); + return 2; + } + writePlanArtifact(planned); + for (const [name, value] of Object.entries(planned)) { + output(name, value); + } + return 0; +} + +function parseJsonFlag(argv, name, { defaultValue = undefined } = {}) { + 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 JSON.parse(argv[index + 1]); + } + if (value.startsWith(`${flag}=`)) { + return JSON.parse(value.slice(flag.length + 1)); + } + } + return defaultValue; +} + +function stringFlag(argv, name, defaultValue = "all") { + 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); + } + } + return defaultValue; +} + +function setFlag(argv, name) { + const value = parseJsonFlag(argv, name, { defaultValue: [] }); + return new Set(stringList(value)); +} + +function nullableSetFlag(argv, name) { + const value = parseJsonFlag(argv, name, { defaultValue: null }); + if (value === null) { + return null; + } + return new Set(stringList(value)); +} + +function printJson(value) { + console.log(JSON.stringify(sortedValue(value), null, 2)); +} + +function printPlanForFullRun(argv) { + const plan = planForFullRun({ + wasmTarget: stringFlag(argv, "wasm-target"), + nativeTarget: stringFlag(argv, "native-target"), + mobileTarget: stringFlag(argv, "mobile-target"), + }); + printJson({ + jobs: sorted(plan.jobs), + projects: sorted(plan.projects), + tasks: sorted(plan.tasks), + reason: plan.reason, + selectedTargets: plan.selectedTargets === null ? null : sorted(plan.selectedTargets), + }); +} + +function printMatrix(argv, matrix) { + const nativeTarget = stringFlag(argv, "native-target"); + const wasmTarget = stringFlag(argv, "wasm-target"); + const selectedTargets = nullableSetFlag(argv, "selected-targets-json"); + const selectedProducts = nullableSetFlag(argv, "selected-products-json"); + if (matrix === "extension-artifacts-native") { + printJson(extensionArtifactsNativeMatrix(nativeTarget, selectedTargets ?? undefined, selectedProducts ?? undefined)); + } else if (matrix === "extension-artifacts-wasix") { + printJson(extensionArtifactsWasixMatrix(wasmTarget, selectedProducts ?? undefined)); + } else { + fail(`unsupported matrix query ${matrix}`); + } +} + +function usage() { + return `usage: tools/graph/ci_plan.mjs [command] + +Default command emits GitHub Actions outputs and target/graph/ci-plan.json. + +Commands: + config + jobs-for-affected --direct-projects-json JSON --tasks-json JSON + native-target-subset --jobs-json JSON --tasks-json JSON + selected-extension-products --direct-projects-json JSON --tasks-json JSON --jobs-json JSON + plan-full [--wasm-target TARGET] [--native-target TARGET] [--mobile-target TARGET] + mobile-extension-package-native-targets --jobs-json JSON --selected-targets-json JSON|null + matrix extension-artifacts-native|extension-artifacts-wasix [selection flags] +`; +} + +function main(argv) { + const [command, ...rest] = argv; + if (command === undefined) { + process.exit(emitGithubOutputs()); + } + if (command === "--help" || command === "-h") { + console.log(usage()); + } else if (command === "config") { + printJson({ + baseJobs: sorted(BASE_JOBS), + builderJobs: sorted(BUILDER_JOBS), + ciJobTargets: CI_JOB_TARGETS, + ciJobsConfig: CI_JOBS_CONFIG, + }); + } else if (command === "jobs-for-affected") { + printJson(sorted(planJobsForAffected(setFlag(rest, "direct-projects-json"), setFlag(rest, "tasks-json")))); + } else if (command === "native-target-subset") { + const targets = nativeTargetSubsetForJobs(setFlag(rest, "jobs-json"), setFlag(rest, "tasks-json")); + printJson(targets === null ? null : sorted(targets)); + } else if (command === "selected-extension-products") { + const selected = selectedExtensionProductsForPlan( + setFlag(rest, "direct-projects-json"), + setFlag(rest, "tasks-json"), + setFlag(rest, "jobs-json"), + ); + printJson(selected === null ? null : sorted(selected)); + } else if (command === "plan-full") { + printPlanForFullRun(rest); + } else if (command === "mobile-extension-package-native-targets") { + printJson(mobileExtensionPackageNativeTargets(setFlag(rest, "jobs-json"), nullableSetFlag(rest, "selected-targets-json"))); + } else if (command === "matrix") { + const [matrix, ...matrixRest] = rest; + printMatrix(matrixRest, matrix); + } else { + fail(`unknown command ${command}`); + } +} + +if (import.meta.main) { + main(Bun.argv.slice(2)); +} diff --git a/tools/graph/ci_plan.py b/tools/graph/ci_plan.py deleted file mode 100644 index 9a65ed55..00000000 --- a/tools/graph/ci_plan.py +++ /dev/null @@ -1,665 +0,0 @@ -#!/usr/bin/env python3 -"""Map Moon affected tasks onto stable GitHub Actions jobs. - -Moon is the only project/task graph. Stable GitHub job names are selected from -Moon task tags named ``ci-``. GitHub Actions still owns platform matrix -fan-out because runner OS, native target triples, and simulator/device targets -are CI execution details, not source projects. -""" - -from __future__ import annotations - -import json -import os -import subprocess -import sys -from pathlib import Path - - -ROOT = Path(__file__).resolve().parents[2] - - -BASE_JOBS = {"affected"} -ALWAYS_JOBS = set(BASE_JOBS) -BUILDER_JOBS = { - "broker-runtime", - "extension-artifacts-native", - "extension-artifacts-wasix", - "extension-packages", - "js-sdk-package", - "kotlin-sdk-package", - "liboliphaunt-native-android", - "liboliphaunt-native-desktop", - "liboliphaunt-native-ios", - "liboliphaunt-native-release-assets", - "liboliphaunt-wasix-aot", - "liboliphaunt-wasix-release-assets", - "liboliphaunt-wasix-runtime", - "mobile-build-android", - "mobile-build-ios", - "mobile-extension-packages", - "node-direct", - "react-native-sdk-package", - "rust-sdk-package", - "swift-sdk-package", - "wasix-rust-package", -} -NATIVE_RUNTIME_JOBS = { - "liboliphaunt-native-android", - "liboliphaunt-native-desktop", - "liboliphaunt-native-ios", -} -NATIVE_RUNTIME_TASKS = { - "liboliphaunt-native:release-runtime", - "liboliphaunt-native:release-runtime-desktop", - "liboliphaunt-native:release-runtime-mobile-target", -} -WASM_RUNTIME_JOBS = { - "liboliphaunt-wasix-runtime", - "liboliphaunt-wasix-aot", - "liboliphaunt-wasix-release-assets", -} -AGGREGATE_ARTIFACT_JOBS = {"liboliphaunt-native-release-assets"} -WASM_RUNTIME_PORTABLE_TASK = "liboliphaunt-wasix:runtime-portable" -WASM_RUNTIME_AOT_TASK = "liboliphaunt-wasix:runtime-aot" -MOBILE_JOB_SURFACES = { - "mobile-build-android": "react-native-android", - "mobile-build-ios": "react-native-ios", -} -ANDROID_MOBILE_JOBS = {"mobile-build-android"} -IOS_MOBILE_JOBS = {"mobile-build-ios"} -EXTENSION_ARTIFACT_CONSUMER_JOBS = { - "extension-packages", - "mobile-extension-packages", -} -WASIX_EXTENSION_ARTIFACT_PORTABLE_CONSUMER_JOBS = { - "extension-packages", - "extension-artifacts-wasix", -} -MOBILE_SMOKE_EXTENSION_PRODUCTS = {"oliphaunt-extension-vector"} - - -def moon_bin() -> str: - if configured := os.environ.get("MOON_BIN"): - return configured - for candidate in ( - Path.home() / ".proto" / "shims" / "moon", - Path.home() / ".proto" / "bin" / "moon", - ): - if candidate.exists(): - return str(candidate) - return "moon" - - -def moon(args: list[str]) -> dict[str, object]: - output = subprocess.check_output([moon_bin(), *args], cwd=ROOT, text=True) - 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"], - 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") - if not isinstance(tasks_by_project, dict): - raise RuntimeError("moon query tasks did not return a tasks object") - - jobs: dict[str, set[str]] = {} - for project_id, project_tasks in tasks_by_project.items(): - if not isinstance(project_tasks, dict): - continue - for task_id, task in project_tasks.items(): - if not isinstance(task, dict): - continue - target = task.get("target") or f"{project_id}:{task_id}" - tags = task.get("tags", []) - if not isinstance(tags, list): - continue - for tag in tags: - if isinstance(tag, str) and tag.startswith("ci-"): - job = tag.removeprefix("ci-") - jobs.setdefault(job, set()).add(str(target)) - return {job: sorted(targets) for job, targets in sorted(jobs.items())} - - -CI_JOB_TARGETS: dict[str, list[str]] = moon_ci_job_targets() -ALL_BUILDER_JOBS = (set(BUILDER_JOBS) | WASM_RUNTIME_JOBS | AGGREGATE_ARTIFACT_JOBS) - ALWAYS_JOBS -COVERAGE_JOB_PRODUCTS = { - job: targets[0].split(":", 1)[0] - for job, targets in CI_JOB_TARGETS.items() - if any(target.endswith(":coverage") for target in targets) -} -CI_JOBS_CONFIG = { - "always_jobs": sorted(ALWAYS_JOBS), - "ci_job_targets": CI_JOB_TARGETS, - "coverage_job_products": COVERAGE_JOB_PRODUCTS, - "wasm_runtime_jobs": sorted(WASM_RUNTIME_JOBS), -} - - -def job_targets_for_jobs(jobs: set[str]) -> dict[str, list[str]]: - return { - job: CI_JOB_TARGETS[job] - for job in sorted(jobs) - if job in CI_JOB_TARGETS - } - - -def empty_matrix() -> dict[str, list[dict[str, str]]]: - return {"include": []} - - -def jobs_for_targets(targets: set[str], *, allowed_jobs: set[str] | None = None) -> set[str]: - jobs: set[str] = set() - target_set = set(targets) - for job, job_targets in CI_JOB_TARGETS.items(): - if allowed_jobs is not None and job not in allowed_jobs: - continue - if target_set & set(job_targets): - jobs.add(job) - return jobs - - -def add_implied_jobs(jobs: set[str], tasks: set[str]) -> None: - if jobs & { - "liboliphaunt-wasix-runtime", - "liboliphaunt-wasix-aot", - "liboliphaunt-wasix-release-assets", - } or {WASM_RUNTIME_PORTABLE_TASK, WASM_RUNTIME_AOT_TASK} & tasks: - jobs.update(WASM_RUNTIME_JOBS) - - if jobs & set(MOBILE_JOB_SURFACES): - jobs.add("mobile-extension-packages") - jobs.add("react-native-sdk-package") - - if jobs & ANDROID_MOBILE_JOBS: - jobs.add("liboliphaunt-native-android") - jobs.add("kotlin-sdk-package") - - if jobs & IOS_MOBILE_JOBS: - jobs.add("liboliphaunt-native-ios") - jobs.add("swift-sdk-package") - - if "swift-sdk-package" in jobs: - jobs.add("liboliphaunt-native-ios") - - if "liboliphaunt-native-release-assets" in jobs: - jobs.update(NATIVE_RUNTIME_JOBS) - - if jobs & {"extension-artifacts-native", "extension-artifacts-wasix"}: - jobs.add("extension-packages") - - if jobs & EXTENSION_ARTIFACT_CONSUMER_JOBS: - jobs.add("extension-artifacts-native") - - 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( - direct_projects: set[str], - tasks: set[str], -) -> set[str]: - jobs = set(ALWAYS_JOBS) - jobs.update(jobs_for_targets(tasks, allowed_jobs=ALL_BUILDER_JOBS)) - 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) - jobs.update(IOS_MOBILE_JOBS) - if "ci-workflows" in direct_projects: - jobs.update(ALL_BUILDER_JOBS) - add_implied_jobs(jobs, tasks) - if tasks & NATIVE_RUNTIME_TASKS: - jobs.add("liboliphaunt-native-release-assets") - jobs.update(NATIVE_RUNTIME_JOBS) - return jobs - - -def native_target_subset_for_jobs(jobs: set[str], tasks: set[str]) -> set[str] | None: - if not (jobs & NATIVE_RUNTIME_JOBS): - return None - if "liboliphaunt-native-release-assets" in jobs: - return None - if tasks & NATIVE_RUNTIME_TASKS: - return None - - targets = mobile_native_targets_for_jobs(jobs) - if "swift-sdk-package" in jobs: - targets.add("ios-xcframework") - if "kotlin-sdk-package" in jobs: - targets.update(liboliphaunt_native_runtime_targets_for_surface("maven")) - return targets or None - - -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(liboliphaunt_native_runtime_targets_for_surface(surface)) - return targets - - -def mobile_extension_package_native_targets(jobs: set[str], selected_targets: set[str] | None) -> list[str]: - if "mobile-extension-packages" not in jobs: - return [] - if selected_targets is not None: - return sorted(selected_targets) - return sorted(mobile_native_targets_for_jobs(jobs)) - - -def focused_mobile_native_targets( - mobile_target: str, - native_target: str, - focused_mobile_jobs: set[str], -) -> set[str]: - targets = mobile_native_targets_for_jobs(focused_mobile_jobs) - if native_target == "all": - return targets - if mobile_target == "both": - raise RuntimeError("focused mobile_target=both requires native_target=all") - if native_target not in targets: - valid_targets = ", ".join(sorted(targets)) - raise RuntimeError( - f"native_target={native_target} is not valid for mobile_target={mobile_target}; " - f"expected one of: all, {valid_targets}" - ) - return {native_target} - - -def plan_for_pull_request() -> tuple[set[str], set[str], set[str], str, set[str] | None]: - base = os.environ.get("MOON_BASE") - head = os.environ.get("MOON_HEAD") - if not base or not head: - raise RuntimeError("MOON_BASE and MOON_HEAD are required for pull_request CI planning") - - direct_projects, projects, direct_tasks = affected_projects_and_tasks() - jobs = plan_jobs_for_affected(direct_projects, direct_tasks) - selected_native_targets = native_target_subset_for_jobs(jobs, direct_tasks) - reason = ( - f"direct affected projects: {', '.join(sorted(direct_projects)) or '(none)'}; " - f"downstream affected projects: {', '.join(sorted(projects)) or '(none)'}; " - f"direct affected tasks: {', '.join(sorted(direct_tasks)) or '(none)'}" - ) - return jobs, projects, direct_tasks, reason, selected_native_targets - - -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", - native_target=native_target, - selected_targets=selected_targets, - ) - - -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", - native_target=native_target, - selected_targets=selected_targets, - ) - - -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", - native_target=native_target, - selected_targets=selected_targets, - ) - - -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", - native_target=native_target, - selected_targets=selected_targets, - ) - - -def broker_runtime_matrix(native_target: str = "all") -> dict[str, list[dict[str, str]]]: - 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]]]: - 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", - 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", wasm_target=wasm_target) - - -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]]]: - 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]: - targets: set[str] = set() - for job in jobs: - targets.update(CI_JOB_TARGETS.get(job, [])) - return targets - - -def selected_extension_products_for_plan( - direct_projects: set[str], - tasks: set[str], - jobs: set[str], -) -> set[str] | None: - if not ( - jobs - & ( - {"extension-artifacts-native", "extension-artifacts-wasix", "extension-packages"} - | set(MOBILE_JOB_SURFACES) - ) - ): - return None - - exact_products = set(exact_extension_products()) - selected = (direct_projects & exact_products) | { - target.split(":", 1)[0] - for target in tasks - if target.split(":", 1)[0] in exact_products - } - broad_extension_inputs = { - "extension-artifacts-native", - "extension-artifacts-wasix", - "extension-contrib-postgres18", - "extension-model", - "extension-packages", - "extensions", - "liboliphaunt-native", - "liboliphaunt-wasix", - "postgres18", - "source-inputs", - "third-party-native", - "third-party-shared", - "third-party-wasix", - } - if direct_projects & broad_extension_inputs: - return exact_products - if "extension-packages:assemble-release" in tasks and not selected: - return exact_products - if "extension-packages" in jobs and not selected: - return exact_products - if jobs & set(MOBILE_JOB_SURFACES): - selected.update(MOBILE_SMOKE_EXTENSION_PRODUCTS) - if jobs & {"extension-artifacts-native", "extension-artifacts-wasix"} and not selected: - return exact_products - if "extension-packages:assemble-mobile" in tasks and not selected: - return exact_products - if not selected: - return None - return selected - - -def plan_for_full_run( - wasm_target: str = "all", - native_target: str = "all", - mobile_target: str = "all", -) -> tuple[set[str], set[str], set[str], str, set[str] | None]: - if mobile_target != "all": - mobile_jobs_by_target = { - "android": {"mobile-build-android"}, - "ios": {"mobile-build-ios"}, - "both": {"mobile-build-android", "mobile-build-ios"}, - } - focused_mobile_jobs = mobile_jobs_by_target.get(mobile_target) - if focused_mobile_jobs is None: - raise RuntimeError(f"unknown mobile target {mobile_target}; expected one of: all, android, ios, both") - focused_jobs = set(BASE_JOBS) | focused_mobile_jobs - add_implied_jobs(focused_jobs, set()) - focused_native_targets = focused_mobile_native_targets(mobile_target, native_target, focused_mobile_jobs) - return ( - focused_jobs, - {"liboliphaunt-native", "oliphaunt-react-native"}, - targets_for_jobs(focused_mobile_jobs), - f"manual focused mobile CI run for {mobile_target}", - focused_native_targets, - ) - - if native_target != "all": - if native_target.startswith("android-") or native_target == "ios-xcframework": - focused_jobs = set(BASE_JOBS) | { - "liboliphaunt-native-android" if native_target.startswith("android-") else "liboliphaunt-native-ios" - } - focused_projects = {"liboliphaunt-native"} - else: - focused_jobs = set(BASE_JOBS) | {"liboliphaunt-native-desktop", "broker-runtime", "node-direct"} - focused_projects = {"liboliphaunt-native", "oliphaunt-broker", "oliphaunt-node-direct"} - add_implied_jobs(focused_jobs, set()) - return ( - focused_jobs, - focused_projects, - targets_for_jobs(focused_jobs), - f"manual focused native runtime CI run for {native_target}", - None, - ) - - if wasm_target != "all": - focused_jobs = set(BASE_JOBS) | { - "liboliphaunt-wasix-runtime", - "liboliphaunt-wasix-aot", - } - return ( - focused_jobs, - {"liboliphaunt-wasix"}, - targets_for_jobs(focused_jobs), - f"manual focused WASIX runtime CI run for {wasm_target}", - None, - ) - - jobs = set(BASE_JOBS) | BUILDER_JOBS | WASM_RUNTIME_JOBS - add_implied_jobs(jobs, targets_for_jobs(jobs)) - return jobs, set(), targets_for_jobs(jobs), "non-PR full CI/runtime run", None - - -def output(name: str, value: object) -> None: - if isinstance(value, str): - rendered = value - else: - rendered = json.dumps(value, sort_keys=True, separators=(",", ":")) - path = os.environ.get("GITHUB_OUTPUT") - if path: - with Path(path).open("a", encoding="utf-8") as handle: - print(f"{name}={rendered}", file=handle) - print(f"{name}={rendered}") - - -def write_plan_artifact(plan: dict[str, object]) -> None: - path = ROOT / "target" / "graph" / "ci-plan.json" - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(f"{json.dumps(plan, indent=2, sort_keys=True)}\n", encoding="utf-8") - - -def emit_github_outputs() -> int: - try: - if os.environ.get("GITHUB_EVENT_NAME") == "pull_request": - jobs, projects, tasks, reason, selected_native_targets = plan_for_pull_request() - else: - jobs, projects, tasks, reason, selected_native_targets = plan_for_full_run( - os.environ.get("WASM_TARGET", "all"), - os.environ.get("NATIVE_TARGET", "all"), - os.environ.get("MOBILE_TARGET", "all"), - ) - except Exception as error: - print(f"affected planning failed: {error}", file=sys.stderr) - return 2 - direct_projects: set[str] = set() - if os.environ.get("GITHUB_EVENT_NAME") == "pull_request": - try: - direct_projects, _, _ = affected_projects_and_tasks() - except Exception: - direct_projects = set() - selected_extension_products = selected_extension_products_for_plan(direct_projects, tasks, jobs) - - plan = { - "jobs": sorted(jobs), - "builder_jobs": sorted(jobs & BUILDER_JOBS), - "job_targets": job_targets_for_jobs(jobs), - "projects": sorted(projects), - "tasks": sorted(tasks), - "liboliphaunt_native_desktop_runtime_matrix": ( - liboliphaunt_native_desktop_runtime_matrix( - os.environ.get("NATIVE_TARGET", "all"), - selected_native_targets, - ) - if "liboliphaunt-native-desktop" in jobs - else empty_matrix() - ), - "liboliphaunt_native_android_runtime_matrix": ( - liboliphaunt_native_android_runtime_matrix( - os.environ.get("NATIVE_TARGET", "all"), - selected_native_targets, - ) - if "liboliphaunt-native-android" in jobs - else empty_matrix() - ), - "liboliphaunt_native_ios_runtime_matrix": ( - liboliphaunt_native_ios_runtime_matrix( - os.environ.get("NATIVE_TARGET", "all"), - selected_native_targets, - ) - if "liboliphaunt-native-ios" in jobs - else empty_matrix() - ), - "extension_artifacts_native_matrix": ( - extension_artifacts_native_matrix( - os.environ.get("NATIVE_TARGET", "all"), - selected_native_targets if "extension-packages" not in jobs else None, - selected_extension_products, - ) - if "extension-artifacts-native" in jobs - else empty_matrix() - ), - "extension_artifacts_wasix_matrix": ( - extension_artifacts_wasix_matrix("all", selected_extension_products) - if "extension-artifacts-wasix" in jobs - else empty_matrix() - ), - "liboliphaunt_wasix_aot_runtime_matrix": ( - liboliphaunt_wasix_aot_runtime_matrix(os.environ.get("WASM_TARGET", "all")) - if "liboliphaunt-wasix-aot" in jobs - else empty_matrix() - ), - "extension_package_products": sorted(selected_extension_products or []), - "extension_package_products_csv": ",".join(sorted(selected_extension_products or [])), - "mobile_extension_package_native_targets": mobile_extension_package_native_targets(jobs, selected_native_targets), - "mobile_extension_package_native_targets_csv": ",".join( - mobile_extension_package_native_targets(jobs, selected_native_targets) - ), - "react_native_android_mobile_app_matrix": ( - react_native_android_mobile_app_matrix( - os.environ.get("NATIVE_TARGET", "all"), - selected_native_targets, - ) - if "mobile-build-android" in jobs - else empty_matrix() - ), - "broker_runtime_matrix": ( - broker_runtime_matrix(os.environ.get("NATIVE_TARGET", "all")) - if "broker-runtime" in jobs - else empty_matrix() - ), - "node_direct_runtime_matrix": ( - node_direct_runtime_matrix(os.environ.get("NATIVE_TARGET", "all")) - if "node-direct" in jobs - else empty_matrix() - ), - "reason": reason, - } - write_plan_artifact(plan) - for name, value in plan.items(): - output(name, value) - return 0 - - -if __name__ == "__main__": - raise SystemExit(emit_github_outputs()) diff --git a/tools/graph/graph.py b/tools/graph/graph.py index 21c6b3ca..23dc580d 100755 --- a/tools/graph/graph.py +++ b/tools/graph/graph.py @@ -38,11 +38,6 @@ "target", } -sys.path.insert(0, str(ROOT / "tools" / "release")) -sys.path.insert(0, str(ROOT / "tools" / "graph")) -from ci_plan import CI_JOB_TARGETS, CI_JOBS_CONFIG, plan_jobs_for_affected # noqa: E402 - - def fail(message: str) -> NoReturn: raise SystemExit(f"graph.py: {message}") @@ -77,6 +72,28 @@ def bun_json(args: list[str]) -> Any: return json.loads(output) +def ci_plan_query(command: str, *args: str) -> Any: + return bun_json(["tools/graph/ci_plan.mjs", command, *args]) + + +CI_PLAN_CONFIG = ci_plan_query("config") +CI_JOB_TARGETS = CI_PLAN_CONFIG["ciJobTargets"] +CI_JOBS_CONFIG = CI_PLAN_CONFIG["ciJobsConfig"] + + +def plan_jobs_for_affected(direct_projects: set[str], tasks: set[str]) -> set[str]: + jobs = ci_plan_query( + "jobs-for-affected", + "--direct-projects-json", + json.dumps(sorted(direct_projects), separators=(",", ":")), + "--tasks-json", + json.dumps(sorted(tasks), separators=(",", ":")), + ) + if not isinstance(jobs, list) or not all(isinstance(job, str) for job in jobs): + fail("CI planner jobs-for-affected query did not return a string list") + return set(jobs) + + def release_graph() -> dict[str, Any]: value = bun_json(["tools/release/release_graph_query.mjs", "graph"]) if not isinstance(value, dict): diff --git a/tools/policy/check-release-policy.py b/tools/policy/check-release-policy.py index ab084372..4259ce87 100644 --- a/tools/policy/check-release-policy.py +++ b/tools/policy/check-release-policy.py @@ -12,9 +12,7 @@ ROOT = pathlib.Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "tools/release")) -sys.path.insert(0, str(ROOT / "tools/graph")) -import ci_plan # noqa: E402 import artifact_targets # noqa: E402 import product_metadata # noqa: E402 @@ -38,6 +36,182 @@ def fail(message: str) -> None: raise SystemExit(message) +def bun_json(args: list[str]) -> object: + try: + output = subprocess.check_output( + ["tools/dev/bun.sh", *args], + cwd=ROOT, + stderr=subprocess.STDOUT, + text=True, + ) + except subprocess.CalledProcessError as error: + raise RuntimeError(error.output.strip()) from error + return json.loads(output) + + +def string_set(value: object, label: str) -> set[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + fail(f"{label} must be a JSON string list") + return set(value) + + +def optional_string_set(value: object, label: str) -> set[str] | None: + if value is None: + return None + return string_set(value, label) + + +def json_flag(value: set[str] | None) -> str: + if value is None: + return "null" + return json.dumps(sorted(value), separators=(",", ":")) + + +class CiPlanClient: + def __init__(self) -> None: + config = bun_json(["tools/graph/ci_plan.mjs", "config"]) + if not isinstance(config, dict): + fail("CI planner config query must return an object") + self.BASE_JOBS = string_set(config.get("baseJobs"), "baseJobs") + self.BUILDER_JOBS = string_set(config.get("builderJobs"), "builderJobs") + targets = config.get("ciJobTargets") + if not isinstance(targets, dict): + fail("ciJobTargets must be an object") + self.CI_JOB_TARGETS = { + str(job): sorted(string_set(job_targets, f"ciJobTargets.{job}")) + for job, job_targets in targets.items() + } + + def query(self, *args: str) -> object: + return bun_json(["tools/graph/ci_plan.mjs", *args]) + + def plan_jobs_for_affected(self, direct_projects: set[str], tasks: set[str]) -> set[str]: + return string_set( + self.query( + "jobs-for-affected", + "--direct-projects-json", + json_flag(direct_projects), + "--tasks-json", + json_flag(tasks), + ), + "jobs-for-affected", + ) + + def native_target_subset_for_jobs(self, jobs: set[str], tasks: set[str]) -> set[str] | None: + return optional_string_set( + self.query( + "native-target-subset", + "--jobs-json", + json_flag(jobs), + "--tasks-json", + json_flag(tasks), + ), + "native-target-subset", + ) + + def selected_extension_products_for_plan( + self, + direct_projects: set[str], + tasks: set[str], + jobs: set[str], + ) -> set[str] | None: + return optional_string_set( + self.query( + "selected-extension-products", + "--direct-projects-json", + json_flag(direct_projects), + "--tasks-json", + json_flag(tasks), + "--jobs-json", + json_flag(jobs), + ), + "selected-extension-products", + ) + + def plan_for_full_run( + self, + *, + wasm_target: str = "all", + native_target: str = "all", + mobile_target: str = "all", + ) -> tuple[set[str], set[str], set[str], str, set[str] | None]: + value = self.query( + "plan-full", + "--wasm-target", + wasm_target, + "--native-target", + native_target, + "--mobile-target", + mobile_target, + ) + if not isinstance(value, dict): + fail("plan-full must return an object") + reason = value.get("reason") + if not isinstance(reason, str): + fail("plan-full reason must be a string") + return ( + string_set(value.get("jobs"), "plan-full.jobs"), + string_set(value.get("projects"), "plan-full.projects"), + string_set(value.get("tasks"), "plan-full.tasks"), + reason, + optional_string_set(value.get("selectedTargets"), "plan-full.selectedTargets"), + ) + + def mobile_extension_package_native_targets( + self, + jobs: set[str], + selected_targets: set[str] | None, + ) -> list[str]: + value = self.query( + "mobile-extension-package-native-targets", + "--jobs-json", + json_flag(jobs), + "--selected-targets-json", + json_flag(selected_targets), + ) + return sorted(string_set(value, "mobile-extension-package-native-targets")) + + def extension_artifacts_native_matrix( + self, + native_target: str, + selected_targets: set[str] | None, + selected_products: set[str] | None = None, + ) -> dict: + value = self.query( + "matrix", + "extension-artifacts-native", + "--native-target", + native_target, + "--selected-targets-json", + json_flag(selected_targets), + "--selected-products-json", + json_flag(selected_products), + ) + if not isinstance(value, dict): + fail("extension-artifacts-native matrix must be an object") + return value + + def extension_artifacts_wasix_matrix( + self, + wasm_target: str, + selected_products: set[str] | None = None, + ) -> dict: + value = self.query( + "matrix", + "extension-artifacts-wasix", + "--wasm-target", + wasm_target, + "--selected-products-json", + json_flag(selected_products), + ) + if not isinstance(value, dict): + fail("extension-artifacts-wasix matrix must be an object") + return value + + +ci_plan = CiPlanClient() + + def read_text(path: str) -> str: return (ROOT / path).read_text(encoding="utf-8") @@ -68,11 +242,6 @@ 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): @@ -398,10 +567,10 @@ def check_ci_policy() -> None: for forbidden in ("targets=(", "tools/graph/jobs.toml", "tools/release/release-inputs.toml"): if forbidden in ci: fail(f"CI workflow must not contain {forbidden}") - assert_contains("tools/graph/ci_plan.py", "moon([\"query\", \"tasks\"])", "CI planner must read Moon task tags") - assert_contains("tools/graph/ci_plan.py", "ci-", "CI planner must document ci-* task tags") + assert_contains("tools/graph/ci_plan.mjs", "moon([\"query\", \"tasks\"])", "CI planner must read Moon task tags") + assert_contains("tools/graph/ci_plan.mjs", "ci-", "CI planner must document ci-* task tags") assert_contains( - "tools/graph/ci_plan.py", + "tools/graph/ci_plan.mjs", "extension_package_products_csv", "CI planner must emit selected exact-extension products for artifact package builders", ) diff --git a/tools/policy/check-repo-structure.sh b/tools/policy/check-repo-structure.sh index eac18445..1fb39f9b 100755 --- a/tools/policy/check-repo-structure.sh +++ b/tools/policy/check-repo-structure.sh @@ -510,7 +510,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 tools/graph/ci_plan.py' +require_text .github/workflows/ci.yml 'tools/dev/bun.sh tools/graph/ci_plan.mjs' 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' @@ -543,24 +543,24 @@ 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.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' +require_text tools/graph/ci_plan.mjs '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' -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/ci_plan.mjs 'moonCiJobTargets' +require_text tools/graph/ci_plan.mjs 'ci-' +require_text tools/graph/ci_plan.mjs 'jobTargetsForJobs' +reject_text tools/graph/ci_plan.mjs 'import plan as 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' -reject_text tools/graph/ci_plan.py 'PROJECT_JOBS = {' -reject_text tools/graph/ci_plan.py 'CI_JOB_TARGETS: dict[str, list[str]] = {' -reject_text tools/graph/ci_plan.py 'MOBILE_ANDROID_PATTERNS = [' -reject_text tools/graph/ci_plan.py 'RN_IOS_PLATFORM_PATTERNS = [' +require_text tools/graph/ci_plan.mjs 'WASM_RUNTIME_PORTABLE_TASK' +require_text tools/graph/ci_plan.mjs 'WASM_RUNTIME_JOBS' +reject_text tools/graph/ci_plan.mjs 'PROJECT_JOBS = {' +reject_text tools/graph/ci_plan.mjs 'CI_JOB_TARGETS: dict[str, list[str]] = {' +reject_text tools/graph/ci_plan.mjs 'MOBILE_ANDROID_PATTERNS = [' +reject_text tools/graph/ci_plan.mjs 'RN_IOS_PLATFORM_PATTERNS = [' require_text src/runtimes/liboliphaunt/wasix/moon.yml 'runtime-portable:' -reject_text tools/graph/ci_plan.py 'PRODUCER_PROJECTS' -reject_text tools/graph/ci_plan.py 'PRODUCER_TASKS' +reject_text tools/graph/ci_plan.mjs 'PRODUCER_PROJECTS' +reject_text tools/graph/ci_plan.mjs 'PRODUCER_TASKS' reject_text .github/workflows/ci.yml 'producer_required' reject_text .github/workflows/ci.yml 'asset-plan' reject_text .github/workflows/ci.yml 'plan-wasix-assets.py' diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index c4bd1d33..bd0fe927 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -225,7 +225,7 @@ grep -Fq 'moon(["query", "affected", "--upstream", "none", "--downstream", "none fail "affected runner must get direct affected projects from Moon" 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 'tools/graph/affected.mjs' tools/graph/ci_plan.py || +grep -Fq 'tools/graph/affected.mjs' tools/graph/ci_plan.mjs || 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" @@ -387,7 +387,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 tools/graph/ci_plan.py' .github/workflows/ci.yml || +grep -Fq 'tools/dev/bun.sh tools/graph/ci_plan.mjs' .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" diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index 8a3735b6..d9e3248d 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/graph/ci_plan.py tools/graph/graph.py tools/policy/check-release-policy.py tools/release/artifact_targets.py diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py index 55cdcc7b..441e5ac4 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -16,9 +16,6 @@ ROOT = Path(__file__).resolve().parents[2] -sys.path.insert(0, str(ROOT / "tools" / "graph")) - -import ci_plan # noqa: E402 def fail(message: str) -> NoReturn: @@ -53,6 +50,24 @@ def artifact_target_matrix(matrix: str) -> dict[str, list[dict[str, str]]]: return value +def ci_plan_full_run(*, wasm_target: str = "all", native_target: str = "all", mobile_target: str = "all") -> dict: + value = bun_json( + [ + "tools/graph/ci_plan.mjs", + "plan-full", + "--wasm-target", + wasm_target, + "--native-target", + native_target, + "--mobile-target", + mobile_target, + ] + ) + if not isinstance(value, dict): + fail("CI planner full-run query did not return an object") + return value + + def ts_template(asset: str) -> str: return asset.replace("{version}", "${version}") @@ -494,25 +509,25 @@ def validate_ci_release_artifacts() -> None: f"/target/sdk-artifacts/{project_id}/**/*", f"{project_id} package task must declare staged SDK package artifacts as Moon outputs", ) - focused_wasix_jobs, *_ = ci_plan.plan_for_full_run(wasm_target="linux-x64-gnu") + focused_wasix_jobs = set(ci_plan_full_run(wasm_target="linux-x64-gnu").get("jobs", [])) if focused_wasix_jobs != {"affected", "liboliphaunt-wasix-runtime", "liboliphaunt-wasix-aot"}: fail( "focused WASIX target runs must build only the portable runtime and requested AOT producer, " f"got {sorted(focused_wasix_jobs)}" ) require_text( - "tools/graph/ci_plan.py", - '"extension_artifacts_wasix_matrix": (', + "tools/graph/ci_plan.mjs", + "extension_artifacts_wasix_matrix:", "CI planner must model WASIX exact-extension artifact matrix output", ) require_text( - "tools/graph/ci_plan.py", - 'if "extension-artifacts-wasix" in jobs', + "tools/graph/ci_plan.mjs", + 'jobs.has("extension-artifacts-wasix")', "CI planner must emit WASIX exact-extension rows only when the WASIX extension builder is selected", ) require_text( - "tools/graph/ci_plan.py", - 'extension_artifacts_wasix_matrix("all", selected_extension_products)', + "tools/graph/ci_plan.mjs", + 'extensionArtifactsWasixMatrix("all", selectedExtensionProducts', "WASIX extension artifacts are portable and must use the portable selector, not the AOT target selector", ) wasix_release_needs = ( @@ -558,12 +573,12 @@ def validate_ci_release_artifacts() -> None: if "swift-sdk-package:\n name: Builds / swift-sdk\n needs:\n - affected\n - liboliphaunt-native-ios" not in ci: fail("Swift SDK package artifacts must depend on the iOS native target builder that produces the Apple release asset") require_text( - "tools/graph/ci_plan.py", - 'if "swift-sdk-package" in jobs:', + "tools/graph/ci_plan.mjs", + 'jobs.has("swift-sdk-package")', "CI affected planner must make Swift SDK package builds imply liboliphaunt target asset producers", ) require_text( - "tools/graph/ci_plan.py", + "tools/graph/ci_plan.mjs", 'targets.add("ios-xcframework")', "CI affected planner must narrow Swift SDK liboliphaunt target builds to the Apple SwiftPM target when possible", ) @@ -633,12 +648,12 @@ def validate_ci_release_artifacts() -> None: "staged exact-extension artifact checks must reject placeholder files that are not readable release archives", ) require_text( - "tools/graph/ci_plan.py", + "tools/graph/ci_plan.mjs", 'jobs.add("mobile-extension-packages")', "affected planner must select target-scoped exact-extension packages whenever mobile jobs are selected", ) reject_text( - "tools/graph/ci_plan.py", + "tools/graph/ci_plan.mjs", 'if "extension-artifacts-native" in jobs:\n jobs.add("liboliphaunt-native")', "affected planner must not create a coarse native-runtime waterfall for exact-extension artifact builds", ) @@ -1088,7 +1103,7 @@ def validate_ci_release_artifacts() -> None: def validate_target_matrices() -> None: ci = read_text(".github/workflows/ci.yml") release = read_text(".github/workflows/release.yml") - planner = read_text("tools/graph/ci_plan.py") + planner = read_text("tools/graph/ci_plan.mjs") for output_name in ( "liboliphaunt_native_desktop_runtime_matrix", "liboliphaunt_native_android_runtime_matrix", @@ -1096,15 +1111,15 @@ def validate_target_matrices() -> None: ): if output_name not in ci or f"fromJson(needs.affected.outputs.{output_name})" not in ci: fail(f"CI {output_name} matrix must come from affected planner output") - for helper in ( - "liboliphaunt_native_desktop_runtime_matrix", - "liboliphaunt_native_android_runtime_matrix", - "liboliphaunt_native_ios_runtime_matrix", + for output_name, helper in ( + ("liboliphaunt_native_desktop_runtime_matrix", "liboliphauntNativeDesktopRuntimeMatrix"), + ("liboliphaunt_native_android_runtime_matrix", "liboliphauntNativeAndroidRuntimeMatrix"), + ("liboliphaunt_native_ios_runtime_matrix", "liboliphauntNativeIosRuntimeMatrix"), ): require_text( - "tools/graph/ci_plan.py", - "tools/release/artifact_target_matrix.mjs", - f"CI affected planner must derive {helper} from release metadata artifact targets", + "tools/graph/ci_plan.mjs", + helper, + f"CI affected planner must derive {output_name} from release metadata artifact targets", ) if "broker_runtime_matrix" not in ci or "fromJson(needs.affected.outputs.broker_runtime_matrix)" not in ci: fail("CI broker matrix must come from affected planner output") @@ -1159,7 +1174,7 @@ 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 "tools/release/artifact_target_matrix.mjs" not in planner: + if "../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") From d55eaf95cfa5adafb99a495270a55bed07d366a4 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 02:02:15 +0000 Subject: [PATCH 139/308] chore: port graph tool to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 31 +- docs/internal/IMPLEMENTATION_CHECKLIST.md | 4 +- tools/graph/graph.mjs | 880 ++++++++++++++++++ tools/graph/graph.py | 744 --------------- tools/graph/moon.yml | 6 +- tools/policy/check-repo-structure.sh | 9 +- tools/policy/python-entrypoints.allowlist | 1 - 7 files changed, 911 insertions(+), 764 deletions(-) create mode 100755 tools/graph/graph.mjs delete mode 100755 tools/graph/graph.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index fde56531..62f2b268 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -96,12 +96,23 @@ until the current-state gates here are checked with fresh local evidence. examples/tools/check-examples.sh`. - 2026-06-27: Continued the tooling cleanup by porting the shared CI affected planner from `tools/graph/ci_plan.py` to `tools/graph/ci_plan.mjs`. The Builds - workflow now invokes the Bun planner directly, `tools/graph/graph.py` and + workflow now invokes the Bun planner directly, `tools/graph/graph.mjs` and release policy checks query its JSON subcommands, and stale Python inventory references were removed. Fresh checks passed: workflow-dispatch planner - smoke with `tools/dev/bun.sh tools/graph/ci_plan.mjs`, `tools/graph/graph.py - check`, `python3 tools/policy/check-release-policy.py`, and `bash + smoke with `tools/dev/bun.sh tools/graph/ci_plan.mjs`, `tools/dev/bun.sh + tools/graph/graph.mjs check`, `python3 tools/policy/check-release-policy.py`, and `bash tools/policy/check-repo-structure.sh`. +- 2026-06-27: Ported the local graph metadata generator/checker from + `tools/graph/graph.py` to `tools/graph/graph.mjs`. The `graph-tools` Moon + project now runs as JavaScript through `tools/dev/bun.sh`, repo structure + policy requires the Bun entrypoint, and the intentional Python entrypoint + inventory is down to 16 tracked files. Fresh checks passed: + `tools/dev/bun.sh tools/graph/graph.mjs check`, `$HOME/.proto/bin/moon run + graph-tools:check`, `bash tools/policy/check-repo-structure.sh`, `bash + tools/policy/check-tooling-stack.sh`, `tools/dev/bun.sh + tools/policy/check-python-entrypoints.mjs`, `python3 + tools/release/check_artifact_targets.py`, `python3 + tools/policy/check-release-policy.py`, and `git diff --cached --check`. - 2026-06-27: Added and pushed the native Rust `oliphaunt-tools` Cargo facade crate so consumer manifests can depend on the facade while Cargo selects the target `oliphaunt-tools-*` payload crate. The Rust SDK release renderer now @@ -128,8 +139,8 @@ until the current-state gates here are checked with fresh local evidence. every former matrix name, focused selected-extension matrix smoke, `GITHUB_EVENT_NAME=workflow_dispatch tools/dev/bun.sh tools/graph/ci_plan.mjs`, 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 + `python3 tools/release/check_artifact_targets.py`, `tools/dev/bun.sh + tools/graph/graph.mjs 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 @@ -858,12 +869,12 @@ until the current-state gates here are checked with fresh local evidence. `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.mjs` smoke, - `graph.py check`, `check-tooling-stack.sh`, `check-repo-structure.sh`, + affected project/task selection, and the graph checker now runs as + `tools/graph/graph.mjs`. On 2026-06-26, validation passed with the direct Bun + helper smoke, pull-request-mode `ci_plan.mjs` smoke, graph checks, + `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. + Python inventory contained 32 tracked files at that point. - 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/docs/internal/IMPLEMENTATION_CHECKLIST.md b/docs/internal/IMPLEMENTATION_CHECKLIST.md index 2013a3c6..58c33a78 100644 --- a/docs/internal/IMPLEMENTATION_CHECKLIST.md +++ b/docs/internal/IMPLEMENTATION_CHECKLIST.md @@ -43,7 +43,7 @@ or CI/build output proves the contract. ## Moon Graph - [x] Moon is the only task and affectedness graph. Evidence: - `tools/graph/graph.py check` passes and reports Moon projects/release + `tools/dev/bun.sh tools/graph/graph.mjs check` passes and reports Moon projects/release products. - [x] Stable CI job names are derived from Moon task `ci-*` tags. Evidence: `tools/graph/ci_plan.mjs` and `tools/policy/check-moon-product-graph.mjs`. @@ -545,7 +545,7 @@ Run before claiming this architecture complete: tools/release/check_artifact_targets.py tools/release/check_release_metadata.py` - [x] `tools/dev/bun.sh tools/graph/ci_plan.mjs --help` -- [x] `python3 tools/graph/graph.py check` +- [x] `tools/dev/bun.sh tools/graph/graph.mjs check` - [x] `node tools/policy/check-moon-product-graph.mjs` - [x] `python3 tools/release/check_artifact_targets.py` - [x] `python3 tools/policy/check-release-policy.py` diff --git a/tools/graph/graph.mjs b/tools/graph/graph.mjs new file mode 100755 index 00000000..7a9b294d --- /dev/null +++ b/tools/graph/graph.mjs @@ -0,0 +1,880 @@ +#!/usr/bin/env bun +import { execFileSync } from "node:child_process"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { homedir } from "node:os"; +import path from "node:path"; + +const TOOL = "graph.mjs"; +const ROOT = path.resolve(import.meta.dir, "../.."); +const GRAPH_ROOT = path.join(ROOT, "target/graph"); +const COVERAGE_BASELINE_PATH = path.join(ROOT, "coverage/baseline.toml"); +const SYNTHETIC_ROOT = path.join(ROOT, "tools/graph/synthetic"); + +const GENERATED_PATH_PARTS = new Set([ + ".build", + ".cxx", + ".expo", + ".gradle", + ".kotlin", + ".moon", + ".next", + ".source", + "DerivedData", + "Pods", + "__pycache__", + "dist", + "lib", + "node_modules", + "out", + "target", +]); + +function fail(message) { + console.error(`${TOOL}: ${message}`); + process.exit(1); +} + +function posix(value) { + return String(value).split(path.sep).join("/"); +} + +function rel(file) { + const resolved = path.resolve(String(file)); + const relative = path.relative(ROOT, resolved); + if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) { + return posix(resolved); + } + return posix(relative); +} + +function compareText(left, right) { + return left < right ? -1 : left > right ? 1 : 0; +} + +function sorted(items) { + return [...items].sort(compareText); +} + +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 jsonText(value) { + return `${JSON.stringify(sortedValue(value), null, 2)}\n`; +} + +function moonBin() { + if (process.env.MOON_BIN) { + return process.env.MOON_BIN; + } + const protoMoon = path.join(homedir(), ".proto/bin/moon"); + return existsSync(protoMoon) ? protoMoon : "moon"; +} + +function readToml(file) { + if (!existsSync(file)) { + fail(`missing TOML input: ${rel(file)}`); + } + const value = Bun.TOML.parse(readFileSync(file, "utf8")); + if (value === null || Array.isArray(value) || typeof value !== "object") { + fail(`${rel(file)} must contain a TOML table`); + } + return value; +} + +function commandJson(command, args, { input = undefined } = {}) { + const output = execFileSync(command, args, { + cwd: ROOT, + env: process.env, + encoding: "utf8", + input, + maxBuffer: 100 * 1024 * 1024, + }); + return JSON.parse(output); +} + +function runMoon(args, { input = undefined } = {}) { + const value = commandJson(moonBin(), args, { input }); + if (value === null || Array.isArray(value) || typeof value !== "object") { + fail("moon query did not return a JSON object"); + } + return value; +} + +function bunJson(args) { + return commandJson("tools/dev/bun.sh", args); +} + +function ciPlanQuery(command, ...args) { + return bunJson(["tools/graph/ci_plan.mjs", command, ...args]); +} + +const CI_PLAN_CONFIG = ciPlanQuery("config"); +const CI_JOB_TARGETS = CI_PLAN_CONFIG.ciJobTargets; +const CI_JOBS_CONFIG = CI_PLAN_CONFIG.ciJobsConfig; + +function planJobsForAffected(directProjects, tasks) { + const jobs = ciPlanQuery( + "jobs-for-affected", + "--direct-projects-json", + JSON.stringify(sorted(directProjects)), + "--tasks-json", + JSON.stringify(sorted(tasks)), + ); + if (!Array.isArray(jobs) || !jobs.every((job) => typeof job === "string")) { + fail("CI planner jobs-for-affected query did not return a string list"); + } + return new Set(jobs); +} + +function releaseGraph() { + const value = bunJson(["tools/release/release_graph_query.mjs", "graph"]); + if (value === null || Array.isArray(value) || typeof value !== "object") { + fail("release graph query did not return an object"); + } + return value; +} + +function releaseProductProjects() { + const value = bunJson(["tools/release/release_graph_query.mjs", "product-projects"]); + if ( + value === null || + Array.isArray(value) || + typeof value !== "object" || + !Object.entries(value).every(([key, item]) => typeof key === "string" && typeof item === "string") + ) { + fail("release graph product-project query did not return a string map"); + } + return value; +} + +function releaseOrder(products) { + const value = bunJson([ + "tools/release/release_graph_query.mjs", + "release-order", + "--products-json", + JSON.stringify(products), + ]); + if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) { + fail("release graph order query did not return a string list"); + } + return value; +} + +function releasePlanForPaths(paths) { + const args = ["tools/release/release_graph_query.mjs", "plan"]; + for (const item of paths) { + args.push("--changed-file", item); + } + const value = bunJson(args); + if (value === null || Array.isArray(value) || typeof value !== "object") { + fail("release graph plan query did not return an object"); + } + return value; +} + +function releasePlansForSinglePaths(paths) { + const value = bunJson([ + "tools/release/release_graph_query.mjs", + "plans-for-paths", + "--paths-json", + JSON.stringify(paths), + ]); + if ( + value === null || + Array.isArray(value) || + typeof value !== "object" || + !Object.entries(value).every(([key, item]) => typeof key === "string" && item !== null && typeof item === "object" && !Array.isArray(item)) + ) { + fail("release graph plans-for-paths query did not return a plan map"); + } + return value; +} + +function affectedNames(value) { + if (value !== null && typeof value === "object" && !Array.isArray(value)) { + return new Set(Object.keys(value).map(String)); + } + 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) { + result.add(String(identifier)); + } + } + } + return result; + } + return new Set(); +} + +function moonProjects() { + const projects = runMoon(["query", "projects"]).projects; + if (!Array.isArray(projects)) { + fail("moon query projects did not return a projects array"); + } + return projects; +} + +function moonTasks() { + const tasks = runMoon(["query", "tasks"]).tasks; + if (tasks === null || Array.isArray(tasks) || typeof tasks !== "object") { + fail("moon query tasks did not return a tasks object"); + } + return tasks; +} + +function objectKeys(value) { + return value !== null && typeof value === "object" && !Array.isArray(value) ? Object.keys(value) : []; +} + +function normalizeProject(project) { + const config = project.config !== null && typeof project.config === "object" ? project.config : {}; + const rawDeps = project.dependencies ?? config.dependsOn ?? []; + if (!Array.isArray(rawDeps)) { + fail(`Moon project ${project.id} has non-list dependsOn`); + } + const deps = {}; + for (const dependency of rawDeps) { + if (typeof dependency === "string") { + deps[dependency] = "production"; + } else if (dependency !== null && typeof dependency === "object" && typeof dependency.id === "string") { + deps[dependency.id] = String(dependency.scope ?? "production"); + } else { + fail(`Moon project ${project.id} has unsupported dependency entry ${JSON.stringify(dependency)}`); + } + } + return { + id: project.id, + source: project.source ?? config.source ?? "", + language: project.language ?? config.language, + layer: project.layer ?? config.layer, + stack: project.stack ?? config.stack, + tags: sorted(config.tags ?? []), + dependsOn: sorted(Object.keys(deps)), + dependencyScopes: Object.fromEntries(Object.entries(deps).sort(([left], [right]) => compareText(left, right))), + project: config.project !== null && typeof config.project === "object" ? config.project : {}, + tasks: sorted(Object.keys(project.tasks ?? {})), + }; +} + +function normalizeTask(task) { + const inputs = new Set([ + ...objectKeys(task.inputFiles), + ...objectKeys(task.inputGlobs), + ]); + for (const item of task.inputs ?? []) { + if (item !== null && typeof item === "object" && (item.file || item.glob)) { + inputs.add(item.file ?? item.glob); + } + } + + const outputs = new Set([ + ...objectKeys(task.outputFiles), + ...objectKeys(task.outputGlobs), + ]); + for (const item of task.outputs ?? []) { + if (typeof item === "string") { + outputs.add(item); + } else if (item !== null && typeof item === "object" && (item.file || item.glob)) { + outputs.add(item.file ?? item.glob); + } + } + + const deps = (task.deps ?? []) + .map((dep) => + dep !== null && typeof dep === "object" + ? { target: dep.target, cacheStrategy: dep.cacheStrategy ?? null } + : { target: dep, cacheStrategy: null }, + ) + .sort((left, right) => compareText(left.target ?? "", right.target ?? "") || compareText(left.cacheStrategy ?? "", right.cacheStrategy ?? "")); + + return { + command: [task.command ?? "", ...(task.args ?? [])].join(" ").trim(), + deps, + tags: sorted(task.tags ?? []), + inputs: sorted(inputs), + outputs: sorted(outputs), + cache: task.options?.cache, + runInCI: task.options?.runInCI ?? true, + }; +} + +function releaseProducts(releaseMetadata) { + const products = releaseMetadata.products; + if (products === null || Array.isArray(products) || typeof products !== "object") { + fail("release metadata must define [products.] tables"); + } + return products; +} + +function dependentsByProject(projects) { + const dependents = Object.fromEntries(Object.keys(projects).map((project) => [project, new Set()])); + for (const [project, config] of Object.entries(projects)) { + for (const dependency of config.dependsOn) { + if (!dependents[dependency]) { + dependents[dependency] = new Set(); + } + dependents[dependency].add(project); + } + } + return Object.fromEntries( + Object.keys(dependents) + .sort(compareText) + .map((project) => [project, sorted(dependents[project])]), + ); +} + +function downstreamClosure(project, dependents) { + const seen = new Set([project]); + const queue = [project]; + while (queue.length > 0) { + const current = queue.shift(); + for (const dependent of dependents[current] ?? []) { + if (!seen.has(dependent)) { + seen.add(dependent); + queue.push(dependent); + } + } + } + return sorted(seen); +} + +function ownerProjectForPath(projects, filePath) { + if (isGeneratedLocalState(filePath)) { + return null; + } + const matches = Object.values(projects).filter( + (project) => + project.source === "." || + filePath === project.source || + filePath.startsWith(`${project.source}/`), + ); + matches.sort((left, right) => right.source.length - left.source.length); + return matches[0]?.id ?? null; +} + +function isGeneratedLocalState(filePath) { + if (filePath.startsWith("target/")) { + return true; + } + return filePath.split("/").some((part) => GENERATED_PATH_PARTS.has(part)); +} + +function coverageExpectations(coverageBaseline, tasks) { + const products = coverageBaseline.products; + if (products === null || Array.isArray(products) || typeof products !== "object") { + fail("coverage baseline must define [products.] tables"); + } + const expectations = {}; + for (const [product, config] of Object.entries(products).sort(([left], [right]) => compareText(left, right))) { + const productTasks = tasks[product] ?? {}; + expectations[product] = { + tool: config.tool, + lineThreshold: config.line_threshold, + measuredLineCoverage: config.measured_line_coverage, + summary: config.summary, + reports: config.reports ?? [], + includeGlobs: config.source_globs ?? config.include_globs ?? [], + excludeGlobs: config.exclude_globs ?? [], + moonCoverageTask: Object.hasOwn(productTasks, "coverage"), + }; + } + return expectations; +} + +function ciMatrix(tasks) { + const jobs = {}; + const missing = {}; + for (const [job, targets] of Object.entries(CI_JOB_TARGETS)) { + const missingTargets = []; + for (const target of targets) { + const [project, taskId] = target.split(":", 2); + if (!Object.hasOwn(tasks[project] ?? {}, taskId)) { + missingTargets.push(target); + } + } + jobs[job] = { + targets, + allTargetsExist: missingTargets.length === 0, + }; + if (missingTargets.length > 0) { + missing[job] = missingTargets; + } + } + return { + metadata: { + alwaysJobs: sorted(CI_JOBS_CONFIG.always_jobs), + coverageJobProducts: Object.fromEntries(Object.entries(CI_JOBS_CONFIG.coverage_job_products).sort(([left], [right]) => compareText(left, right))), + wasmRuntimeJobs: sorted(CI_JOBS_CONFIG.wasm_runtime_jobs), + source: "Moon task tags ci-", + }, + jobs, + requiredJobs: sorted(Object.keys(CI_JOB_TARGETS)), + missingTargets: missing, + }; +} + +function buildGraph() { + const releaseMetadata = releaseGraph(); + const coverageBaseline = readToml(COVERAGE_BASELINE_PATH); + const projects = Object.fromEntries(moonProjects().map((project) => [project.id, normalizeProject(project)])); + const tasksRaw = moonTasks(); + const tasks = Object.fromEntries( + Object.entries(tasksRaw) + .sort(([left], [right]) => compareText(left, right)) + .map(([project, projectTasks]) => [ + project, + Object.fromEntries( + Object.entries(projectTasks) + .sort(([left], [right]) => compareText(left, right)) + .map(([taskId, task]) => [taskId, normalizeTask(task)]), + ), + ]), + ); + const products = releaseProducts(releaseMetadata); + const productIds = Object.keys(products); + const productProjects = releaseProductProjects(); + const dependents = dependentsByProject(projects); + return { + moonProjects: projects, + moonTasks: tasks, + moonDependents: dependents, + releaseProducts: Object.fromEntries( + Object.entries(products).map(([product, config]) => [ + product, + { + owner: config.owner, + kind: config.kind, + moonProject: productProjects[product], + tagPrefix: config.tag_prefix, + publishTargets: config.publish_targets ?? [], + releaseArtifacts: config.release_artifacts ?? [], + moonProjectExists: Object.hasOwn(projects, productProjects[product]), + }, + ]), + ), + releaseOrder: releaseOrder(productIds), + coverageExpectations: coverageExpectations(coverageBaseline, tasksRaw), + ciMatrix: ciMatrix(tasksRaw), + productIds, + policy: releaseMetadata.policy ?? {}, + }; +} + +function normalizeExplainPaths(paths) { + const normalized = new Set(); + for (const item of paths) { + let value = String(item).trim().replaceAll("\\", "/"); + if (value.startsWith("./")) { + value = value.slice(2); + } + if (value) { + normalized.add(value); + } + } + return sorted(normalized); +} + +function explainPaths(paths, graph) { + const projects = graph.moonProjects; + const dependents = graph.moonDependents; + const normalizedPaths = normalizeExplainPaths(paths); + const releaseImpact = releasePlanForPaths(normalizedPaths); + return { + paths: normalizedPaths.map((filePath) => { + const owner = ownerProjectForPath(projects, filePath); + return { + path: filePath, + ownerProject: owner, + moonAffectedProjects: owner ? downstreamClosure(owner, dependents) : [], + coverageProducts: coverageProductsForPath(filePath, graph), + }; + }), + releasePlan: releaseImpact, + }; +} + +function coverageProductsForPath(filePath, graph) { + if (isGeneratedLocalState(filePath)) { + return []; + } + const products = []; + for (const [product, config] of Object.entries(graph.coverageExpectations)) { + const includes = config.includeGlobs ?? []; + const excludes = config.excludeGlobs ?? []; + if (productMatches(filePath, includes) && !productMatches(filePath, excludes)) { + products.push(product); + } + } + return sorted(products); +} + +function escapeRegex(char) { + return /[\\^$.*+?()[\]{}|]/.test(char) ? `\\${char}` : char; +} + +function globPatternToRegex(pattern) { + return new RegExp(`^${[...pattern].map((char) => (char === "*" ? ".*" : escapeRegex(char))).join("")}$`); +} + +function productMatches(filePath, patterns) { + const includes = patterns.filter((pattern) => !pattern.startsWith("!")); + const excludes = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1)); + return includes.some((pattern) => globPatternToRegex(pattern).test(filePath)) && + !excludes.some((pattern) => globPatternToRegex(pattern).test(filePath)); +} + +function writeJson(file, value) { + mkdirSync(path.dirname(file), { recursive: true }); + writeFileSync(file, jsonText(value), "utf8"); +} + +function writeGraph(graph) { + mkdirSync(GRAPH_ROOT, { recursive: true }); + writeJson(path.join(GRAPH_ROOT, "products.json"), { + moonProjects: graph.moonProjects, + moonDependents: graph.moonDependents, + releaseProducts: graph.releaseProducts, + releaseOrder: graph.releaseOrder, + productIds: graph.productIds, + }); + writeJson(path.join(GRAPH_ROOT, "tasks.json"), graph.moonTasks); + writeJson(path.join(GRAPH_ROOT, "ci-matrix.json"), graph.ciMatrix); + writeJson(path.join(GRAPH_ROOT, "coverage-expectations.json"), graph.coverageExpectations); + writeJson(path.join(GRAPH_ROOT, "explain.json"), { + usage: "tools/graph/graph.mjs explain --path ", + syntheticCases: Object.fromEntries( + ["affected", "release", "coverage"].map((contract) => [ + contract, + syntheticContractCases(contract).cases ?? {}, + ]), + ), + }); +} + +function syntheticContractCases(contract) { + const file = path.join(SYNTHETIC_ROOT, `${contract}.toml`); + if (!existsSync(file)) { + fail(`missing synthetic graph fixture: ${rel(file)}`); + } + return readToml(file); +} + +function assertEqualList(label, actual, expected) { + const left = sorted(actual ?? []); + const right = sorted(expected ?? []); + if (JSON.stringify(left) !== JSON.stringify(right)) { + fail(`${label}: expected ${JSON.stringify(right)}, got ${JSON.stringify(left)}`); + } +} + +function assertDocsEvidencePathsDoNotSelectBuilderJobs() { + const forbiddenJobs = new Set([ + "extension-artifacts-native", + "extension-artifacts-wasix", + "extension-packages", + "liboliphaunt-wasix-aot", + "liboliphaunt-wasix-release-assets", + "liboliphaunt-wasix-runtime", + "mobile-build-android", + "mobile-build-ios", + "mobile-extension-packages", + ]); + const paths = [ + "src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json", + "src/extensions/generated/docs/extension-evidence.json", + "src/extensions/generated/docs/extensions.json", + ]; + for (const filePath of paths) { + const affected = runMoon(["query", "affected", "--upstream", "none", "--downstream", "none"], { + input: `${filePath}\n`, + }); + const jobs = planJobsForAffected( + affectedNames(affected.projects), + affectedNames(affected.tasks), + ); + const unexpected = sorted([...jobs].filter((job) => forbiddenJobs.has(job))); + if (unexpected.length > 0) { + fail(`${filePath} must not select CI builder jobs, got ${JSON.stringify(unexpected)}`); + } + } +} + +function taskConfig(graph, project, taskId) { + const value = graph.moonTasks?.[project]?.[taskId]; + if (!value) { + fail(`missing Moon task ${project}:${taskId}`); + } + return value; +} + +function assertTaskTags(graph, project, taskId, expected) { + const actual = taskConfig(graph, project, taskId).tags ?? []; + const missing = expected.filter((tag) => !actual.includes(tag)); + if (missing.length > 0) { + fail(`${project}:${taskId} tags: missing ${JSON.stringify(sorted(missing))}, got ${JSON.stringify(sorted(actual))}`); + } +} + +function assertDepCacheStrategy(graph, project, taskId, target, expected) { + const deps = taskConfig(graph, project, taskId).deps ?? []; + for (const dep of deps) { + if (dep.target === target) { + if (dep.cacheStrategy !== expected) { + fail(`${project}:${taskId} dependency ${target}: expected cacheStrategy=${expected}, got ${dep.cacheStrategy}`); + } + return; + } + } + fail(`${project}:${taskId} is missing dependency ${target}`); +} + +function checkGraph(graph) { + const projects = graph.moonProjects; + const releaseProductsConfig = releaseProducts(releaseGraph()); + const productProjects = releaseProductProjects(); + for (const [product, config] of Object.entries(releaseProductsConfig)) { + const projectId = productProjects[product]; + const project = projects[projectId]; + if (!project) { + fail(`release product ${product} does not have an owning Moon project`); + } + if (!(project.tags ?? []).includes("release-product")) { + fail(`release product ${product} Moon project ${projectId} must be tagged release-product`); + } + let release = project.project?.metadata?.release; + if (release === null || Array.isArray(release) || typeof release !== "object") { + release = project.project?.release; + } + if (release === null || Array.isArray(release) || typeof release !== "object") { + fail(`release product ${product} Moon project ${projectId} must declare project.release metadata`); + } + if (release.component !== product) { + fail(`release product ${product} Moon metadata component mismatch: ${release.component}`); + } + if (release.packagePath !== config.path) { + fail(`release product ${product} Moon metadata packagePath mismatch: ${release.packagePath}`); + } + } + + const missingCiTargets = graph.ciMatrix.missingTargets; + if (Object.keys(missingCiTargets).length > 0) { + fail(`CI matrix references missing Moon targets: ${JSON.stringify(missingCiTargets)}`); + } + + assertDocsEvidencePathsDoNotSelectBuilderJobs(); + + for (const [project, projectTasks] of Object.entries(graph.moonTasks)) { + for (const [taskId, config] of Object.entries(projectTasks)) { + if (!config.tags || config.tags.length === 0) { + fail(`${project}:${taskId} must declare Moon task tags`); + } + } + } + + for (const project of Object.keys(graph.moonProjects)) { + for (const taskId of ["check", "test"]) { + if (Object.hasOwn(graph.moonTasks[project] ?? {}, taskId)) { + let expectedTags; + if (taskId === "check") { + expectedTags = ["quality", "static"]; + } else if (project === "liboliphaunt-native") { + expectedTags = ["quality", "runtime"]; + } else { + expectedTags = ["quality", "unit"]; + } + assertTaskTags(graph, project, taskId, expectedTags); + } + } + } + + for (const project of [ + "oliphaunt-rust", + "oliphaunt-swift", + "oliphaunt-kotlin", + "oliphaunt-react-native", + "oliphaunt-js", + "oliphaunt-wasix-rust", + ]) { + assertTaskTags(graph, project, "coverage", ["coverage", "quality"]); + assertTaskTags(graph, project, "bench-run", ["bench", "measured"]); + } + + for (const target of [ + "oliphaunt-rust:coverage", + "oliphaunt-swift:coverage", + "oliphaunt-kotlin:coverage", + "oliphaunt-js:coverage", + "oliphaunt-react-native:coverage", + "oliphaunt-wasix-rust:coverage", + ]) { + assertDepCacheStrategy(graph, "repo", "coverage", target, "outputs"); + } + assertDepCacheStrategy(graph, "docs", "smoke", "docs:build", "outputs"); + assertDepCacheStrategy(graph, "docs", "release-check", "docs:build", "outputs"); + + for (const [product, config] of Object.entries(graph.coverageExpectations)) { + if (!config.moonCoverageTask) { + fail(`coverage baseline product ${product} has no Moon coverage task`); + } + if (config.lineThreshold === undefined || config.measuredLineCoverage === undefined) { + fail(`coverage baseline product ${product} is missing measured threshold data`); + } + } + + const affectedCases = syntheticContractCases("affected").cases; + if (affectedCases === null || Array.isArray(affectedCases) || typeof affectedCases !== "object") { + fail("tools/graph/synthetic/affected.toml must define [cases.] tables"); + } + for (const [caseId, graphCase] of Object.entries(affectedCases)) { + const filePath = graphCase.path; + if (typeof filePath !== "string") { + fail(`synthetic affected case ${caseId} is missing path`); + } + const explanation = explainPaths([filePath], graph); + assertEqualList(`${caseId} Moon affected projects`, explanation.paths[0].moonAffectedProjects, graphCase.moon_projects ?? []); + } + + const releaseCases = syntheticContractCases("release").cases; + if (releaseCases === null || Array.isArray(releaseCases) || typeof releaseCases !== "object") { + fail("tools/graph/synthetic/release.toml must define [cases.] tables"); + } + for (const [caseId, graphCase] of Object.entries(releaseCases)) { + if (typeof graphCase.path !== "string") { + fail(`synthetic release case ${caseId} is missing path`); + } + } + const releaseCasePaths = Object.values(releaseCases).map((graphCase) => graphCase.path).filter((item) => typeof item === "string"); + const releaseCasePlans = releasePlansForSinglePaths(releaseCasePaths); + for (const [caseId, graphCase] of Object.entries(releaseCases)) { + const filePath = graphCase.path; + const releaseImpact = releaseCasePlans[filePath]; + assertEqualList(`${caseId} direct release products`, releaseImpact.directProducts, graphCase.direct_products ?? []); + assertEqualList(`${caseId} release products`, releaseImpact.releaseProducts, graphCase.release_products ?? []); + if (Object.hasOwn(graphCase, "docs_only") && releaseImpact.docsOnly !== graphCase.docs_only) { + fail(`${caseId} docsOnly: expected ${graphCase.docs_only}, got ${releaseImpact.docsOnly}`); + } + } + + const coverageCases = syntheticContractCases("coverage").cases; + if (coverageCases === null || Array.isArray(coverageCases) || typeof coverageCases !== "object") { + fail("tools/graph/synthetic/coverage.toml must define [cases.] tables"); + } + for (const [caseId, graphCase] of Object.entries(coverageCases)) { + const filePath = graphCase.path; + if (typeof filePath !== "string") { + fail(`synthetic coverage case ${caseId} is missing path`); + } + const explanation = explainPaths([filePath], graph); + assertEqualList(`${caseId} coverage products`, explanation.paths[0].coverageProducts, graphCase.coverage_products ?? []); + } + + for (const [project, taskId, expectedCache, expectedOutput] of [ + ["graph-tools", "cache-witness", false, null], + ["graph-tools", "cache-witness-fixture", true, "/target/graph/cache-witness/output.txt"], + ]) { + const config = taskConfig(graph, project, taskId); + if (config.cache !== expectedCache) { + fail(`${project}:${taskId} cache: expected ${expectedCache}, got ${config.cache}`); + } + if (expectedOutput !== null && !(config.outputs ?? []).includes(expectedOutput)) { + fail(`${project}:${taskId} must declare output ${expectedOutput}`); + } + } +} + +function printExplanation(explanation, format) { + if (format === "json") { + console.log(JSON.stringify(sortedValue(explanation), null, 2)); + return; + } + for (const item of explanation.paths) { + console.log(item.path); + console.log(` owner project: ${item.ownerProject ?? "(none)"}`); + console.log(` Moon affected: ${item.moonAffectedProjects.join(", ") || "(none)"}`); + console.log(` coverage: ${item.coverageProducts.join(", ") || "(none)"}`); + } + const plan = explanation.releasePlan; + console.log(`Release direct products: ${plan.directProducts.join(", ") || "(none)"}`); + console.log(`Release products: ${plan.releaseProducts.join(", ") || "(none)"}`); +} + +function parseArgs(argv) { + const [command, ...rest] = argv; + if (!["generate", "check", "explain"].includes(command)) { + fail("usage: tools/graph/graph.mjs generate|check|explain [--path ] [--format text|json]"); + } + if (command !== "explain") { + if (rest.length > 0) { + fail(`${command} does not accept arguments: ${rest.join(" ")}`); + } + return { command }; + } + + const paths = []; + let format = "text"; + for (let index = 0; index < rest.length; index += 1) { + const value = rest[index]; + if (value === "--path") { + if (index + 1 >= rest.length) { + fail("--path requires a value"); + } + paths.push(rest[index + 1]); + index += 1; + } else if (value.startsWith("--path=")) { + paths.push(value.slice("--path=".length)); + } else if (value === "--format") { + if (index + 1 >= rest.length) { + fail("--format requires a value"); + } + format = rest[index + 1]; + index += 1; + } else if (value.startsWith("--format=")) { + format = value.slice("--format=".length); + } else { + fail(`unknown argument ${value}`); + } + } + if (paths.length === 0) { + fail("explain requires at least one --path"); + } + if (!["text", "json"].includes(format)) { + fail("--format must be text or json"); + } + return { command, paths, format }; +} + +function main(argv) { + const args = parseArgs(argv); + const graph = buildGraph(); + if (args.command === "generate") { + writeGraph(graph); + console.log(`generated graph data in ${rel(GRAPH_ROOT)}`); + } else if (args.command === "check") { + writeGraph(graph); + checkGraph(graph); + console.log(`graph checks passed (${Object.keys(graph.moonProjects).length} Moon projects, ${graph.productIds.length} release products)`); + } else if (args.command === "explain") { + writeGraph(graph); + printExplanation(explainPaths(args.paths, graph), args.format); + } +} + +if (import.meta.main) { + main(process.argv.slice(2)); +} diff --git a/tools/graph/graph.py b/tools/graph/graph.py deleted file mode 100755 index 23dc580d..00000000 --- a/tools/graph/graph.py +++ /dev/null @@ -1,744 +0,0 @@ -#!/usr/bin/env python3 -"""Generate and explain Oliphaunt product/task/release metadata data.""" - -from __future__ import annotations - -import argparse -import json -import os -import re -import subprocess -import sys -import tomllib -from collections import deque -from pathlib import Path -from typing import Any, NoReturn - - -ROOT = Path(__file__).resolve().parents[2] -GRAPH_ROOT = ROOT / "target" / "graph" -COVERAGE_BASELINE_PATH = ROOT / "coverage" / "baseline.toml" -SYNTHETIC_ROOT = ROOT / "tools" / "graph" / "synthetic" -GENERATED_PATH_PARTS = { - ".build", - ".cxx", - ".expo", - ".gradle", - ".kotlin", - ".moon", - ".next", - ".source", - "DerivedData", - "Pods", - "__pycache__", - "dist", - "lib", - "node_modules", - "out", - "target", -} - -def fail(message: str) -> NoReturn: - raise SystemExit(f"graph.py: {message}") - - -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 rel(path: Path) -> str: - return path.relative_to(ROOT).as_posix() - - -def read_toml(path: Path) -> dict[str, Any]: - if not path.is_file(): - fail(f"missing TOML input: {rel(path)}") - with path.open("rb") as handle: - return tomllib.load(handle) - - -def run_moon(args: list[str], *, stdin: str | None = None) -> dict[str, Any]: - command = [moon_bin(), *args] - env = dict(os.environ) - output = subprocess.check_output(command, cwd=ROOT, env=env, text=True, input=stdin) - 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 ci_plan_query(command: str, *args: str) -> Any: - return bun_json(["tools/graph/ci_plan.mjs", command, *args]) - - -CI_PLAN_CONFIG = ci_plan_query("config") -CI_JOB_TARGETS = CI_PLAN_CONFIG["ciJobTargets"] -CI_JOBS_CONFIG = CI_PLAN_CONFIG["ciJobsConfig"] - - -def plan_jobs_for_affected(direct_projects: set[str], tasks: set[str]) -> set[str]: - jobs = ci_plan_query( - "jobs-for-affected", - "--direct-projects-json", - json.dumps(sorted(direct_projects), separators=(",", ":")), - "--tasks-json", - json.dumps(sorted(tasks), separators=(",", ":")), - ) - if not isinstance(jobs, list) or not all(isinstance(job, str) for job in jobs): - fail("CI planner jobs-for-affected query did not return a string list") - return set(jobs) - - -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} - 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") - if not isinstance(projects, list): - fail("moon query projects did not return a projects array") - return projects - - -def moon_tasks() -> dict[str, Any]: - data = run_moon(["query", "tasks"]) - tasks = data.get("tasks") - if not isinstance(tasks, dict): - fail("moon query tasks did not return a tasks object") - return tasks - - -def normalize_project(project: dict[str, Any]) -> dict[str, Any]: - config = project.get("config") if isinstance(project.get("config"), dict) else {} - raw_deps = project.get("dependencies") or config.get("dependsOn") or [] - if not isinstance(raw_deps, list): - fail(f"Moon project {project.get('id')} has non-list dependsOn") - deps: dict[str, str] = {} - for dependency in raw_deps: - if isinstance(dependency, str): - deps[dependency] = "production" - elif isinstance(dependency, dict) and isinstance(dependency.get("id"), str): - deps[dependency["id"]] = str(dependency.get("scope") or "production") - else: - fail(f"Moon project {project.get('id')} has unsupported dependency entry {dependency!r}") - return { - "id": project["id"], - "source": project.get("source") or config.get("source") or "", - "language": project.get("language") or config.get("language"), - "layer": project.get("layer") or config.get("layer"), - "stack": project.get("stack") or config.get("stack"), - "tags": sorted(config.get("tags") or []), - "dependsOn": sorted(deps), - "dependencyScopes": dict(sorted(deps.items())), - "project": config.get("project") if isinstance(config.get("project"), dict) else {}, - "tasks": sorted((project.get("tasks") or {}).keys()), - } - - -def normalize_task(task: dict[str, Any]) -> dict[str, Any]: - inputs = sorted( - { - *task.get("inputFiles", {}).keys(), - *task.get("inputGlobs", {}).keys(), - *[ - item.get("file") or item.get("glob") - for item in task.get("inputs", []) - if isinstance(item, dict) and (item.get("file") or item.get("glob")) - ], - } - ) - outputs = sorted( - { - *task.get("outputFiles", {}).keys(), - *task.get("outputGlobs", {}).keys(), - *[ - item.get("file") or item.get("glob") or item - for item in task.get("outputs", []) - if isinstance(item, (dict, str)) - ], - } - ) - deps = sorted( - ( - { - "target": dep.get("target"), - "cacheStrategy": dep.get("cacheStrategy"), - } - if isinstance(dep, dict) - else {"target": dep, "cacheStrategy": None} - for dep in task.get("deps", []) - ), - key=lambda dep: (dep.get("target") or "", dep.get("cacheStrategy") or ""), - ) - command = " ".join([task.get("command") or "", *(task.get("args") or [])]).strip() - return { - "command": command, - "deps": deps, - "tags": sorted(task.get("tags") or []), - "inputs": inputs, - "outputs": outputs, - "cache": (task.get("options") or {}).get("cache"), - "runInCI": (task.get("options") or {}).get("runInCI", True), - } - - -def release_products(release_metadata: dict[str, Any]) -> dict[str, dict[str, Any]]: - products = release_metadata.get("products") - if not isinstance(products, dict): - fail("release metadata must define [products.] tables") - return products - - -def dependents_by_project(projects: dict[str, dict[str, Any]]) -> dict[str, list[str]]: - dependents: dict[str, set[str]] = {project: set() for project in projects} - for project, config in projects.items(): - for dependency in config["dependsOn"]: - dependents.setdefault(dependency, set()).add(project) - return {project: sorted(values) for project, values in sorted(dependents.items())} - - -def downstream_closure(project: str, dependents: dict[str, list[str]]) -> list[str]: - seen = {project} - queue: deque[str] = deque([project]) - while queue: - current = queue.popleft() - for dependent in dependents.get(current, []): - if dependent not in seen: - seen.add(dependent) - queue.append(dependent) - return sorted(seen) - - -def owner_project_for_path(projects: dict[str, dict[str, Any]], path: str) -> str | None: - 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 is_generated_local_state(path: str) -> bool: - if path.startswith("target/"): - return True - return any(part in GENERATED_PATH_PARTS for part in Path(path).parts) - - -def coverage_expectations( - coverage_baseline: dict[str, Any], - tasks: dict[str, Any], -) -> dict[str, Any]: - products = coverage_baseline.get("products") - if not isinstance(products, dict): - fail("coverage baseline must define [products.] tables") - expectations: dict[str, Any] = {} - for product, config in sorted(products.items()): - product_tasks = tasks.get(product, {}) - expectations[product] = { - "tool": config.get("tool"), - "lineThreshold": config.get("line_threshold"), - "measuredLineCoverage": config.get("measured_line_coverage"), - "summary": config.get("summary"), - "reports": config.get("reports", []), - "includeGlobs": config.get("source_globs", config.get("include_globs", [])), - "excludeGlobs": config.get("exclude_globs", []), - "moonCoverageTask": "coverage" in product_tasks, - } - return expectations - - -def ci_matrix(tasks: dict[str, Any]) -> dict[str, Any]: - jobs: dict[str, Any] = {} - missing: dict[str, list[str]] = {} - for job, targets in CI_JOB_TARGETS.items(): - missing_targets: list[str] = [] - for target in targets: - project, task = target.split(":", 1) - if task not in tasks.get(project, {}): - missing_targets.append(target) - jobs[job] = { - "targets": targets, - "allTargetsExist": not missing_targets, - } - if missing_targets: - missing[job] = missing_targets - return { - "metadata": { - "alwaysJobs": sorted(CI_JOBS_CONFIG["always_jobs"]), - "coverageJobProducts": dict(sorted(CI_JOBS_CONFIG["coverage_job_products"].items())), - "wasmRuntimeJobs": sorted(CI_JOBS_CONFIG["wasm_runtime_jobs"]), - "source": "Moon task tags ci-", - }, - "jobs": jobs, - "requiredJobs": sorted(CI_JOB_TARGETS), - "missingTargets": missing, - } - - -def build_graph() -> dict[str, Any]: - 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() - tasks = { - project: {task_id: normalize_task(task) for task_id, task in sorted(project_tasks.items())} - for project, project_tasks in sorted(tasks_raw.items()) - } - products = release_products(release_metadata) - product_ids = list(products) - product_projects = release_product_projects() - dependents = dependents_by_project(projects) - return { - "moonProjects": projects, - "moonTasks": tasks, - "moonDependents": dependents, - "releaseProducts": { - product: { - "owner": config.get("owner"), - "kind": config.get("kind"), - "moonProject": product_projects[product], - "tagPrefix": config.get("tag_prefix"), - "publishTargets": config.get("publish_targets", []), - "releaseArtifacts": config.get("release_artifacts", []), - "moonProjectExists": product_projects[product] in projects, - } - for product, config in products.items() - }, - "releaseOrder": release_order(product_ids), - "coverageExpectations": coverage_expectations(coverage_baseline, tasks_raw), - "ciMatrix": ci_matrix(tasks_raw), - "productIds": product_ids, - "policy": release_metadata.get("policy", {}), - } - - -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_impact = release_plan_for_paths(normalized_paths) - explanations = [] - for path in normalized_paths: - owner = owner_project_for_path(projects, path) - explanations.append( - { - "path": path, - "ownerProject": owner, - "moonAffectedProjects": downstream_closure(owner, dependents) if owner else [], - "coverageProducts": coverage_products_for_path(path, graph), - } - ) - return { - "paths": explanations, - "releasePlan": release_impact, - } - - -def normalize_explain_paths(paths: Iterable[str]) -> list[str]: - normalized: set[str] = set() - for path in paths: - value = path.strip().replace("\\", "/") - if value.startswith("./"): - value = value[2:] - if value: - normalized.add(value) - return sorted(normalized) - - -def coverage_products_for_path(path: str, graph: dict[str, Any]) -> list[str]: - if is_generated_local_state(path): - return [] - products: list[str] = [] - for product, config in graph["coverageExpectations"].items(): - includes = config.get("includeGlobs", []) - excludes = config.get("excludeGlobs", []) - 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") - - -def write_graph(graph: dict[str, Any]) -> None: - GRAPH_ROOT.mkdir(parents=True, exist_ok=True) - write_json( - GRAPH_ROOT / "products.json", - { - "moonProjects": graph["moonProjects"], - "moonDependents": graph["moonDependents"], - "releaseProducts": graph["releaseProducts"], - "releaseOrder": graph["releaseOrder"], - "productIds": graph["productIds"], - }, - ) - write_json(GRAPH_ROOT / "tasks.json", graph["moonTasks"]) - write_json(GRAPH_ROOT / "ci-matrix.json", graph["ciMatrix"]) - write_json(GRAPH_ROOT / "coverage-expectations.json", graph["coverageExpectations"]) - write_json( - GRAPH_ROOT / "explain.json", - { - "usage": "tools/graph/graph.py explain --path ", - "syntheticCases": { - contract: synthetic_contract_cases(contract).get("cases", {}) - for contract in ("affected", "release", "coverage") - }, - }, - ) - - -def synthetic_contract_cases(contract: str) -> dict[str, Any]: - path = SYNTHETIC_ROOT / f"{contract}.toml" - if not path.is_file(): - fail(f"missing synthetic graph fixture: {rel(path)}") - return read_toml(path) - - -def assert_equal_list(label: str, actual: list[str], expected: list[str]) -> None: - if sorted(actual) != sorted(expected): - fail(f"{label}: expected {sorted(expected)}, got {sorted(actual)}") - - -def assert_docs_evidence_paths_do_not_select_builder_jobs() -> None: - forbidden_jobs = { - "extension-artifacts-native", - "extension-artifacts-wasix", - "extension-packages", - "liboliphaunt-wasix-aot", - "liboliphaunt-wasix-release-assets", - "liboliphaunt-wasix-runtime", - "mobile-build-android", - "mobile-build-ios", - "mobile-extension-packages", - } - paths = [ - "src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json", - "src/extensions/generated/docs/extension-evidence.json", - "src/extensions/generated/docs/extensions.json", - ] - for path in paths: - affected = run_moon( - ["query", "affected", "--upstream", "none", "--downstream", "none"], - stdin=f"{path}\n", - ) - jobs = plan_jobs_for_affected( - affected_names(affected.get("projects")), - affected_names(affected.get("tasks")), - ) - unexpected = sorted(jobs & forbidden_jobs) - if unexpected: - fail(f"{path} must not select CI builder jobs, got {unexpected}") - - -def task(graph: dict[str, Any], project: str, task_id: str) -> dict[str, Any]: - try: - return graph["moonTasks"][project][task_id] - except KeyError: - fail(f"missing Moon task {project}:{task_id}") - - -def assert_task_tags(graph: dict[str, Any], project: str, task_id: str, expected: list[str]) -> None: - actual = task(graph, project, task_id).get("tags", []) - missing = sorted(set(expected) - set(actual)) - if missing: - fail(f"{project}:{task_id} tags: missing {missing}, got {sorted(actual)}") - - -def assert_dep_cache_strategy( - graph: dict[str, Any], - project: str, - task_id: str, - target: str, - expected: str, -) -> None: - deps = task(graph, project, task_id).get("deps", []) - for dep in deps: - if dep.get("target") == target: - if dep.get("cacheStrategy") != expected: - fail( - f"{project}:{task_id} dependency {target}: expected cacheStrategy={expected}, " - f"got {dep.get('cacheStrategy')}" - ) - return - fail(f"{project}:{task_id} is missing dependency {target}") - - -def check_graph(graph: dict[str, Any]) -> None: - projects = graph["moonProjects"] - release_products_config = release_products(release_graph()) - product_projects = release_product_projects() - for product, config in release_products_config.items(): - 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") - if "release-product" not in project.get("tags", []): - fail(f"release product {product} Moon project {project_id} must be tagged release-product") - metadata = project.get("project", {}).get("metadata", {}) - release = metadata.get("release") if isinstance(metadata, dict) else None - if not isinstance(release, dict): - release = project.get("project", {}).get("release") - if not isinstance(release, dict): - fail(f"release product {product} Moon project {project_id} must declare project.release metadata") - if release.get("component") != product: - fail(f"release product {product} Moon metadata component mismatch: {release.get('component')}") - if release.get("packagePath") != config.get("path"): - fail(f"release product {product} Moon metadata packagePath mismatch: {release.get('packagePath')}") - - missing_ci_targets = graph["ciMatrix"]["missingTargets"] - if missing_ci_targets: - fail(f"CI matrix references missing Moon targets: {missing_ci_targets}") - - assert_docs_evidence_paths_do_not_select_builder_jobs() - - for project, project_tasks in graph["moonTasks"].items(): - for task_id, config in project_tasks.items(): - if not config.get("tags"): - fail(f"{project}:{task_id} must declare Moon task tags") - - for project in graph["moonProjects"]: - for task_id in ("check", "test"): - if task_id in graph["moonTasks"].get(project, {}): - if task_id == "check": - expected_tags = ["quality", "static"] - elif project == "liboliphaunt-native": - expected_tags = ["quality", "runtime"] - else: - expected_tags = ["quality", "unit"] - assert_task_tags(graph, project, task_id, expected_tags) - - for project in ( - "oliphaunt-rust", - "oliphaunt-swift", - "oliphaunt-kotlin", - "oliphaunt-react-native", - "oliphaunt-js", - "oliphaunt-wasix-rust", - ): - assert_task_tags(graph, project, "coverage", ["coverage", "quality"]) - assert_task_tags(graph, project, "bench-run", ["bench", "measured"]) - - for target in ( - "oliphaunt-rust:coverage", - "oliphaunt-swift:coverage", - "oliphaunt-kotlin:coverage", - "oliphaunt-js:coverage", - "oliphaunt-react-native:coverage", - "oliphaunt-wasix-rust:coverage", - ): - assert_dep_cache_strategy(graph, "repo", "coverage", target, "outputs") - assert_dep_cache_strategy(graph, "docs", "smoke", "docs:build", "outputs") - assert_dep_cache_strategy(graph, "docs", "release-check", "docs:build", "outputs") - - for product, config in graph["coverageExpectations"].items(): - if not config["moonCoverageTask"]: - fail(f"coverage baseline product {product} has no Moon coverage task") - if config["lineThreshold"] is None or config["measuredLineCoverage"] is None: - fail(f"coverage baseline product {product} is missing measured threshold data") - - affected_cases = synthetic_contract_cases("affected").get("cases") - if not isinstance(affected_cases, dict): - fail("tools/graph/synthetic/affected.toml must define [cases.] tables") - for case_id, case in affected_cases.items(): - path = case.get("path") - if not isinstance(path, str): - fail(f"synthetic affected case {case_id} is missing path") - explanation = explain_paths([path], graph) - moon_projects = explanation["paths"][0]["moonAffectedProjects"] - assert_equal_list(f"{case_id} Moon affected projects", moon_projects, case.get("moon_projects", [])) - - release_cases = synthetic_contract_cases("release").get("cases") - if not isinstance(release_cases, dict): - fail("tools/graph/synthetic/release.toml must define [cases.] tables") - 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_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", - release_impact["directProducts"], - case.get("direct_products", []), - ) - assert_equal_list( - f"{case_id} release products", - planned_release_products, - case.get("release_products", []), - ) - if "docs_only" in case and release_impact.get("docsOnly") is not case["docs_only"]: - fail( - f"{case_id} docsOnly: expected {case['docs_only']}, " - f"got {release_impact.get('docsOnly')}" - ) - - coverage_cases = synthetic_contract_cases("coverage").get("cases") - if not isinstance(coverage_cases, dict): - fail("tools/graph/synthetic/coverage.toml must define [cases.] tables") - for case_id, case in coverage_cases.items(): - path = case.get("path") - if not isinstance(path, str): - fail(f"synthetic coverage case {case_id} is missing path") - explanation = explain_paths([path], graph) - assert_equal_list( - f"{case_id} coverage products", - explanation["paths"][0]["coverageProducts"], - case.get("coverage_products", []), - ) - - for project, task_id, expected_cache, expected_output in [ - ("graph-tools", "cache-witness", False, None), - ("graph-tools", "cache-witness-fixture", True, "/target/graph/cache-witness/output.txt"), - ]: - config = task(graph, project, task_id) - if config.get("cache") is not expected_cache: - fail( - f"{project}:{task_id} cache: expected {expected_cache}, " - f"got {config.get('cache')}" - ) - if expected_output is not None and expected_output not in config.get("outputs", []): - fail(f"{project}:{task_id} must declare output {expected_output}") - - -def print_explanation(explanation: dict[str, Any], fmt: str) -> None: - if fmt == "json": - print(json.dumps(explanation, indent=2, sort_keys=True)) - return - for path in explanation["paths"]: - print(f"{path['path']}") - print(f" owner project: {path['ownerProject'] or '(none)'}") - print(" Moon affected: " + (", ".join(path["moonAffectedProjects"]) or "(none)")) - print(" coverage: " + (", ".join(path["coverageProducts"]) or "(none)")) - plan = explanation["releasePlan"] - print("Release direct products: " + (", ".join(plan["directProducts"]) or "(none)")) - print("Release products: " + (", ".join(plan["releaseProducts"]) or "(none)")) - - -def parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - subparsers = parser.add_subparsers(dest="command", required=True) - subparsers.add_parser("generate") - subparsers.add_parser("check") - explain = subparsers.add_parser("explain") - explain.add_argument("--path", action="append", required=True, help="repo-relative path") - explain.add_argument("--format", choices=["text", "json"], default="text") - return parser.parse_args(argv) - - -def main(argv: list[str]) -> int: - args = parse_args(argv) - graph = build_graph() - if args.command == "generate": - write_graph(graph) - print(f"generated graph data in {rel(GRAPH_ROOT)}") - elif args.command == "check": - write_graph(graph) - check_graph(graph) - print(f"graph checks passed ({len(graph['moonProjects'])} Moon projects, {len(graph['productIds'])} release products)") - elif args.command == "explain": - write_graph(graph) - print_explanation(explain_paths(args.path, graph), args.format) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) diff --git a/tools/graph/moon.yml b/tools/graph/moon.yml index bed4a251..4e5ff730 100644 --- a/tools/graph/moon.yml +++ b/tools/graph/moon.yml @@ -1,7 +1,7 @@ $schema: "https://moonrepo.dev/schemas/project.json" id: "graph-tools" -language: "python" +language: "javascript" layer: "tool" stack: "infrastructure" tags: ["tools", "graph", "repo-hygiene"] @@ -19,7 +19,7 @@ owners: tasks: check: tags: ["policy", "assertion", "quality", "static"] - command: "tools/graph/graph.py check" + command: "tools/dev/bun.sh tools/graph/graph.mjs check" inputs: - "/.moon/workspace.yml" - "/.moon/toolchains.yml" @@ -45,7 +45,7 @@ tasks: runFromWorkspaceRoot: true generate: tags: ["generated", "graph"] - command: "tools/graph/graph.py generate" + command: "tools/dev/bun.sh tools/graph/graph.mjs generate" inputs: - "/.moon/workspace.yml" - "/.moon/toolchains.yml" diff --git a/tools/policy/check-repo-structure.sh b/tools/policy/check-repo-structure.sh index 1fb39f9b..9482895b 100755 --- a/tools/policy/check-repo-structure.sh +++ b/tools/policy/check-repo-structure.sh @@ -212,7 +212,7 @@ 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 -require_file tools/graph/graph.py +require_file tools/graph/graph.mjs reject_path tools/graph/synthetic-paths.toml require_file tools/graph/synthetic/affected.toml require_file tools/graph/synthetic/release.toml @@ -417,7 +417,7 @@ require_text tools/policy/check-policy-tools.sh 'bun build "$script" --target=bu require_text tools/policy/check-tooling-stack.sh 'tools/policy/assertions/assert-moon-task-policy.mjs' 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_text tools/graph/moon.yml 'tools/dev/bun.sh tools/graph/graph.mjs check' 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' @@ -550,8 +550,9 @@ require_text tools/graph/ci_plan.mjs 'moonCiJobTargets' require_text tools/graph/ci_plan.mjs 'ci-' require_text tools/graph/ci_plan.mjs 'jobTargetsForJobs' reject_text tools/graph/ci_plan.mjs 'import plan as release_plan' -require_text tools/graph/graph.py 'release_graph_query.mjs' -reject_text tools/graph/graph.py 'import plan as release_plan' +require_file tools/graph/graph.mjs +require_text tools/graph/graph.mjs 'release_graph_query.mjs' +reject_text tools/graph/graph.mjs 'import plan as release_plan' require_text tools/graph/ci_plan.mjs 'WASM_RUNTIME_PORTABLE_TASK' require_text tools/graph/ci_plan.mjs 'WASM_RUNTIME_JOBS' reject_text tools/graph/ci_plan.mjs 'PROJECT_JOBS = {' diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index d9e3248d..da308637 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/graph/graph.py tools/policy/check-release-policy.py tools/release/artifact_targets.py tools/release/build-extension-ci-artifacts.py From 5ac6dc59dad22fc66ef3f49e81dea5cb2b2cfc98 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 02:13:16 +0000 Subject: [PATCH 140/308] chore: port liboliphaunt asset check to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 16 + src/runtimes/liboliphaunt/native/moon.yml | 4 +- .../fixtures/consumer-shape/products.json | 2 +- tools/policy/python-entrypoints.allowlist | 1 - .../check-liboliphaunt-release-assets.mjs | 586 +++++++++++++++ tools/release/check_artifact_targets.py | 4 +- tools/release/check_consumer_shape.py | 2 +- .../check_liboliphaunt_release_assets.py | 679 ------------------ .../package-liboliphaunt-aggregate-assets.sh | 2 +- tools/release/release.py | 2 +- 10 files changed, 611 insertions(+), 687 deletions(-) create mode 100644 tools/release/check-liboliphaunt-release-assets.mjs delete mode 100755 tools/release/check_liboliphaunt_release_assets.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 62f2b268..d8e4a9f0 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -113,6 +113,22 @@ until the current-state gates here are checked with fresh local evidence. tools/policy/check-python-entrypoints.mjs`, `python3 tools/release/check_artifact_targets.py`, `python3 tools/policy/check-release-policy.py`, and `git diff --cached --check`. +- 2026-06-27: Ported liboliphaunt native GitHub release asset validation from + `tools/release/check_liboliphaunt_release_assets.py` to + `tools/release/check-liboliphaunt-release-assets.mjs`. The aggregate + packager and release CLI now invoke the Bun checker through `tools/dev/bun.sh`, + and the intentional Python entrypoint inventory is down to 15 tracked files. + Fresh checks passed: `tools/dev/bun.sh + tools/release/check-liboliphaunt-release-assets.mjs --asset-dir + target/liboliphaunt/release-assets`, `python3 + tools/release/check_artifact_targets.py`, `python3 + tools/release/check_consumer_shape.py --products-json + '["liboliphaunt-native"]'`, `python3 tools/release/check_release_metadata.py`, + `bash tools/policy/check-repo-structure.sh`, `bash + tools/policy/check-tooling-stack.sh`, `tools/dev/bun.sh + tools/policy/check-python-entrypoints.mjs`, `python3 -m py_compile` for + touched Python release checks, full `python3 tools/release/check_consumer_shape.py`, + `tools/release/release.py check`, and `git diff --cached --check`. - 2026-06-27: Added and pushed the native Rust `oliphaunt-tools` Cargo facade crate so consumer manifests can depend on the facade while Cargo selects the target `oliphaunt-tools-*` payload crate. The Rust SDK release renderer now diff --git a/src/runtimes/liboliphaunt/native/moon.yml b/src/runtimes/liboliphaunt/native/moon.yml index 1d5bf9f0..f05b57e1 100644 --- a/src/runtimes/liboliphaunt/native/moon.yml +++ b/src/runtimes/liboliphaunt/native/moon.yml @@ -170,9 +170,11 @@ tasks: - "/src/extensions/generated/sdk/rust.json" - "/src/runtimes/liboliphaunt/native/moon.yml" - "/tools/release/artifact_targets.py" - - "/tools/release/check_liboliphaunt_release_assets.py" + - "/tools/release/check-liboliphaunt-release-assets.mjs" - "/tools/release/package-liboliphaunt-aggregate-assets.sh" - "/tools/release/product_metadata.py" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/liboliphaunt/release-assets/**/*" outputs: - "/target/liboliphaunt/release-assets/**/*" diff --git a/src/shared/fixtures/consumer-shape/products.json b/src/shared/fixtures/consumer-shape/products.json index 84fcdd65..696ee212 100644 --- a/src/shared/fixtures/consumer-shape/products.json +++ b/src/shared/fixtures/consumer-shape/products.json @@ -32,7 +32,7 @@ ], "tools/release/package-liboliphaunt-aggregate-assets.sh": [ "liboliphaunt-${version}-release-assets.sha256", - "check_liboliphaunt_release_assets.py" + "check-liboliphaunt-release-assets.mjs" ] } }, diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index da308637..47708a22 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -6,7 +6,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_liboliphaunt_release_assets.py tools/release/check_release_metadata.py tools/release/check_staged_artifacts.py tools/release/extension_artifact_targets.py diff --git a/tools/release/check-liboliphaunt-release-assets.mjs b/tools/release/check-liboliphaunt-release-assets.mjs new file mode 100644 index 00000000..453ea2bd --- /dev/null +++ b/tools/release/check-liboliphaunt-release-assets.mjs @@ -0,0 +1,586 @@ +#!/usr/bin/env bun +import { createHash } from "node:crypto"; +import { + chmodSync, + existsSync, + mkdirSync, + mkdtempSync, + readdirSync, + readFileSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { gunzipSync, inflateRawSync } from "node:zlib"; + +import { + ROOT, + allArtifactTargets, + compareText, + currentProductVersion, +} from "./release-artifact-targets.mjs"; + +const PREFIX = "check-liboliphaunt-release-assets.mjs"; +const PRODUCT = "liboliphaunt-native"; + +function fail(message) { + console.error(`${PREFIX}: ${message}`); + process.exit(1); +} + +function rel(file) { + const relative = path.relative(ROOT, file); + if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) { + return file; + } + return relative.split(path.sep).join("/"); +} + +function sha256(file) { + return createHash("sha256").update(readFileSync(file)).digest("hex"); +} + +function requireFile(file, description) { + let stat; + try { + stat = statSync(file); + } catch { + fail(`missing ${description}: ${file}`); + } + if (!stat.isFile()) { + fail(`${description} is not a file: ${file}`); + } + if (stat.size <= 0) { + fail(`${description} is empty: ${file}`); + } +} + +function parseChecksumFile(file) { + const checksums = new Map(); + for (const rawLine of readFileSync(file, "utf8").split(/\r?\n/u)) { + if (!rawLine.trim()) { + continue; + } + const parts = rawLine.trim().split(/\s+/u); + if (parts.length !== 2) { + fail(`malformed checksum line in ${file}: ${JSON.stringify(rawLine)}`); + } + const [digest, filename] = parts; + if (!filename.startsWith("./")) { + fail(`checksum path must be relative './name': ${filename}`); + } + checksums.set(filename.slice(2), digest); + } + return checksums; +} + +function validateChecksums(assetDir, checksumFile) { + const checksums = parseChecksumFile(checksumFile); + const expectedAssets = readdirSync(assetDir) + .map((name) => path.join(assetDir, name)) + .filter((file) => statSync(file).isFile() && path.extname(file) !== ".sha256") + .sort(compareText); + if (expectedAssets.length === 0) { + fail(`no release assets found in ${assetDir}`); + } + const assetNames = new Set(expectedAssets.map((file) => path.basename(file))); + for (const asset of expectedAssets) { + const recorded = checksums.get(path.basename(asset)); + if (!recorded) { + fail(`checksum file does not cover release asset: ${path.basename(asset)}`); + } + const actual = sha256(asset); + if (recorded !== actual) { + fail(`checksum mismatch for ${path.basename(asset)}: expected ${recorded}, got ${actual}`); + } + } + const extra = [...checksums.keys()].filter((name) => !assetNames.has(name)).sort(compareText); + if (extra.length > 0) { + fail(`checksum file contains entries for missing assets: ${extra.join(", ")}`); + } +} + +function generatedExtensionMetadata() { + const metadataPath = path.join(ROOT, "src/extensions/generated/sdk/rust.json"); + let metadata; + try { + metadata = JSON.parse(readFileSync(metadataPath, "utf8")); + } catch (error) { + fail(`read generated Rust SDK extension metadata ${metadataPath}: ${error.message}`); + } + if (!Array.isArray(metadata.extensions)) { + fail(`${metadataPath} must define an extensions array`); + } + const expected = new Map(); + for (const [index, row] of metadata.extensions.entries()) { + if (row === null || Array.isArray(row) || typeof row !== "object") { + fail(`${metadataPath} extensions[${index}] must be an object`); + } + const sqlName = row["sql-name"]; + if (typeof sqlName !== "string" || !sqlName) { + fail(`${metadataPath} extensions[${index}] must define sql-name`); + } + const dataFiles = row["runtime-share-data-files"]; + if (!Array.isArray(dataFiles) || !dataFiles.every((value) => typeof value === "string")) { + fail(`${metadataPath} extension ${sqlName} must define runtime-share-data-files`); + } + const nativeModuleStem = row["native-module-stem"]; + if (nativeModuleStem !== null && nativeModuleStem !== undefined && typeof nativeModuleStem !== "string") { + fail(`${metadataPath} extension ${sqlName} native-module-stem must be a string or null`); + } + expected.set(sqlName, { + createsExtension: row["creates-extension"] === true, + dataFiles, + dataFilesTsv: dataFiles.length > 0 ? dataFiles.join(",") : "-", + nativeModuleStem, + }); + } + return expected; +} + +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 checkedArchiveMember(name, archive) { + const normalized = name.replaceAll("\\", "/"); + const parts = normalized.split("/").filter((part) => part && part !== "."); + if (parts.length === 0) { + return null; + } + if (normalized.startsWith("/") || parts.includes("..")) { + fail(`${archive} contains unsafe archive member ${JSON.stringify(name)}`); + } + return parts.join("/"); +} + +function readTarGzEntries(file) { + let buffer; + try { + buffer = gunzipSync(readFileSync(file)); + } catch (error) { + fail(`${file} is not a readable gzip tar archive: ${error.message}`); + } + 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 rawName = parseTarString(header, 0, 100); + const prefix = parseTarString(header, 345, 155); + const fullName = prefix ? `${prefix}/${rawName}` : rawName; + const name = checkedArchiveMember(fullName, file); + const mode = parseTarOctal(header, 100, 8); + const size = parseTarOctal(header, 124, 12); + const type = header.subarray(156, 157).toString("utf8"); + const dataOffset = offset + 512; + if (name) { + entries.set(name, { + mode, + size, + isFile: type === "" || type === "0", + isDirectory: type === "5", + data: buffer.subarray(dataOffset, dataOffset + size), + }); + } + offset = dataOffset + Math.ceil(size / 512) * 512; + } + return entries; +} + +function findEndOfCentralDirectory(buffer, file) { + for (let offset = buffer.length - 22; offset >= Math.max(0, buffer.length - 65557); offset -= 1) { + if (buffer.readUInt32LE(offset) === 0x06054b50) { + return offset; + } + } + fail(`${file} is missing zip end of central directory`); +} + +function readZipEntries(file) { + const buffer = readFileSync(file); + const eocd = findEndOfCentralDirectory(buffer, file); + 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(`${file} has an invalid zip central directory`); + } + const method = buffer.readUInt16LE(offset + 10); + const compressedSize = buffer.readUInt32LE(offset + 20); + 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 localOffset = buffer.readUInt32LE(offset + 42); + const rawName = buffer.subarray(offset + 46, offset + 46 + nameLength).toString("utf8"); + const name = checkedArchiveMember(rawName, file); + if (name) { + entries.set(name, { + mode: externalAttributes >>> 16, + size, + isFile: !rawName.endsWith("/") && (externalAttributes & 0x10) === 0, + isDirectory: rawName.endsWith("/") || (externalAttributes & 0x10) !== 0, + data: () => zipEntryData(buffer, file, localOffset, compressedSize, method), + }); + } + offset += 46 + nameLength + extraLength + commentLength; + } + return entries; +} + +function zipEntryData(buffer, file, offset, compressedSize, method) { + if (buffer.readUInt32LE(offset) !== 0x04034b50) { + fail(`${file} has an invalid zip local file header`); + } + const nameLength = buffer.readUInt16LE(offset + 26); + const extraLength = buffer.readUInt16LE(offset + 28); + const dataStart = offset + 30 + nameLength + extraLength; + const compressed = buffer.subarray(dataStart, dataStart + compressedSize); + if (method === 0) { + return compressed; + } + if (method === 8) { + return inflateRawSync(compressed); + } + fail(`${file} contains unsupported zip compression method ${method}`); +} + +function readArchiveEntries(file) { + if (file.endsWith(".tar.gz")) { + return readTarGzEntries(file); + } + if (path.extname(file) === ".zip") { + return readZipEntries(file); + } + fail(`${file} has unsupported archive extension`); +} + +function archiveMemberNames(file) { + return new Set(readArchiveEntries(file).keys()); +} + +function archiveText(file, memberName) { + const entry = readArchiveEntries(file).get(memberName); + if (!entry) { + fail(`${file} is missing ${memberName}`); + } + if (!entry.isFile) { + fail(`${file} member ${memberName} is not a regular file`); + } + try { + const data = typeof entry.data === "function" ? entry.data() : entry.data; + return Buffer.from(data).toString("utf8"); + } catch (error) { + fail(`${file} member ${memberName} is not readable UTF-8: ${error.message}`); + } +} + +function extractArchive(file, destination) { + rmSync(destination, { recursive: true, force: true }); + mkdirSync(destination, { recursive: true }); + for (const [name, entry] of readArchiveEntries(file)) { + if (entry.isDirectory) { + continue; + } + if (!entry.isFile) { + fail(`${file} member ${name} must be a regular file`); + } + const output = path.join(destination, ...name.split("/")); + mkdirSync(path.dirname(output), { recursive: true }); + const data = typeof entry.data === "function" ? entry.data() : entry.data; + writeFileSync(output, data); + if (entry.mode) { + chmodSync(output, entry.mode & 0o777); + } + } +} + +function validateNativeTargetArtifact(file, target, { requireRuntime, toolSet }) { + const temp = mkdtempSync(path.join(tmpdir(), `oliphaunt-native-${target}-`)); + try { + const extracted = path.join(temp, "payload"); + extractArchive(file, extracted); + const command = [ + "tools/release/optimize_native_runtime_payload.mjs", + extracted, + "--target", + target, + "--tool-set", + toolSet, + "--check", + ]; + if (!requireRuntime) { + command.push("--allow-missing-runtime"); + } + const result = spawnSync("tools/dev/bun.sh", command, { + cwd: ROOT, + stdio: "inherit", + }); + if (result.status !== 0) { + process.exit(result.status ?? 1); + } + } finally { + rmSync(temp, { recursive: true, force: true }); + } +} + +function assetName(target, version) { + return target.asset.replaceAll("{version}", version); +} + +function validateNativeTargetArtifacts(assetDir, version) { + const runtimeTargets = new Set( + allArtifactTargets({ + product: PRODUCT, + kind: "native-runtime", + surface: "rust-native-direct", + publishedOnly: true, + }).map((target) => target.target), + ); + for (const target of allArtifactTargets({ + product: PRODUCT, + kind: "native-runtime", + surface: "github-release", + publishedOnly: true, + })) { + validateNativeTargetArtifact(path.join(assetDir, assetName(target, version)), target.target, { + requireRuntime: runtimeTargets.has(target.target), + toolSet: "runtime", + }); + } + for (const target of allArtifactTargets({ + product: PRODUCT, + kind: "native-tools", + surface: "github-release", + publishedOnly: true, + })) { + validateNativeTargetArtifact(path.join(assetDir, assetName(target, version)), target.target, { + requireRuntime: true, + toolSet: "tools", + }); + } +} + +function validateBaseRuntimeArtifactContents(file, extensionMetadata) { + const names = archiveMemberNames(file); + const runtimePrefix = "oliphaunt/runtime/files/"; + for (const requiredMember of [ + "oliphaunt/package-size.tsv", + "oliphaunt/runtime/manifest.properties", + "oliphaunt/template-pgdata/manifest.properties", + ]) { + if (!names.has(requiredMember)) { + fail(`${file} must contain ${requiredMember}`); + } + } + if (!names.has(`${runtimePrefix}share/postgresql/README.release-fixture`) && ![...names].some((name) => name.startsWith(runtimePrefix))) { + fail(`${file} must contain an oliphaunt/runtime/files tree`); + } + if ([...names].some((name) => name.startsWith(`${runtimePrefix}share/icu/`))) { + fail(`${file} base runtime must not contain ICU data under ${runtimePrefix}share/icu`); + } + for (const [sqlName, metadata] of extensionMetadata) { + const control = `${runtimePrefix}share/postgresql/extension/${sqlName}.control`; + if (names.has(control)) { + fail(`${file} base runtime must not contain optional extension control file ${control}`); + } + for (const dataFile of metadata.dataFiles) { + const dataPath = `${runtimePrefix}share/postgresql/${dataFile}`; + if (names.has(dataPath)) { + fail(`${file} base runtime must not contain optional extension data file ${dataPath}`); + } + } + if (typeof metadata.nativeModuleStem === "string" && metadata.nativeModuleStem) { + for (const suffix of [".dylib", ".so", ".dll"]) { + const module = `${runtimePrefix}lib/postgresql/${metadata.nativeModuleStem}${suffix}`; + if (names.has(module)) { + fail(`${file} base runtime must not contain optional extension module ${module}`); + } + } + } + } +} + +function validateIcuDataArtifactContents(file) { + const names = archiveMemberNames(file); + const icuEntries = [...names] + .filter((name) => { + if (!name.startsWith("share/icu/")) { + return false; + } + const parts = name.slice("share/icu/".length).split("/").filter(Boolean); + return parts.length > 0 && parts[0].startsWith("icudt"); + }) + .sort(compareText); + if (icuEntries.length === 0) { + fail(`${file} must contain ICU data files under share/icu/icudt*`); + } + const unexpected = [...names] + .filter((name) => name !== "." && name !== "share" && name !== "share/icu" && !name.startsWith("share/icu/")) + .sort(compareText); + if (unexpected.length > 0) { + fail(`${file} must contain only share/icu data, found: ${unexpected.slice(0, 5).join(", ")}`); + } +} + +function parseSizeValue(value, file, lineNumber, field) { + const parsed = Number.parseInt(value, 10); + if (!Number.isInteger(parsed) || String(parsed) !== value) { + fail(`${file} line ${lineNumber} has invalid ${field}: ${JSON.stringify(value)}`); + } + if (parsed < 0) { + fail(`${file} line ${lineNumber} has negative ${field}: ${JSON.stringify(value)}`); + } + return parsed; +} + +function parseTsv(file, expectedHeader) { + const lines = readFileSync(file, "utf8").split(/\r?\n/u); + const header = lines.shift()?.split("\t") ?? []; + if (JSON.stringify(header) !== JSON.stringify(expectedHeader)) { + fail(`${file} has unexpected header: ${JSON.stringify(header)}`); + } + return lines + .filter((line) => line.length > 0) + .map((line, index) => { + const values = line.split("\t"); + const row = Object.fromEntries(header.map((column, columnIndex) => [column, values[columnIndex] ?? ""])); + return { row, lineNumber: index + 2 }; + }); +} + +function validatePackageSizeReport(file) { + requireFile(file, "liboliphaunt package-size release report"); + const rows = new Map(); + const extensionRows = []; + for (const { row, lineNumber } of parseTsv(file, ["kind", "id", "extensions", "files", "bytes"])) { + const key = `${row.kind}\0${row.id}`; + if (rows.has(key)) { + fail(`${file} repeats row ${row.kind}/${row.id}`); + } + rows.set(key, row); + parseSizeValue(row.bytes, file, lineNumber, "bytes"); + if (row.kind === "extension") { + extensionRows.push(row.id); + parseSizeValue(row.files, file, lineNumber, "files"); + } else if (row.files !== "-") { + fail(`${file} line ${lineNumber} package rows must use '-' for files`); + } + } + + const requiredRows = [ + ["package", "total"], + ["package", "runtime"], + ["package", "template-pgdata"], + ["package", "static-registry"], + ["extensions", "selected"], + ]; + const missing = requiredRows + .filter(([kind, id]) => !rows.has(`${kind}\0${id}`)) + .map(([kind, id]) => `${kind}/${id}`); + if (missing.length > 0) { + fail(`${file} is missing required row(s): ${missing.join(", ")}`); + } + if (rows.get("extensions\0selected").bytes !== "0") { + fail(`${file} base package-size report must have zero selected extension bytes`); + } + if (extensionRows.length > 0) { + fail(`${file} base package-size report must not include selected extension rows: ${extensionRows.sort(compareText).join(", ")}`); + } + const total = parseSizeValue(rows.get("package\0total").bytes, file, 0, "package total bytes"); + const parts = [ + ["package", "runtime"], + ["package", "template-pgdata"], + ["package", "static-registry"], + ].reduce((sum, [kind, id]) => sum + parseSizeValue(rows.get(`${kind}\0${id}`).bytes, file, 0, `${kind}/${id} bytes`), 0); + if (total !== parts) { + fail(`${file} package total bytes must equal runtime + template-pgdata + static-registry`); + } +} + +function expectedGithubAssets(version) { + return allArtifactTargets({ + product: PRODUCT, + surface: "github-release", + publishedOnly: true, + }).map((target) => assetName(target, version)).sort(compareText); +} + +async function validate(assetDir) { + const version = await currentProductVersion(PRODUCT, PREFIX); + const metadata = generatedExtensionMetadata(); + const required = expectedGithubAssets(version); + const expected = new Set(required); + const actual = new Set(readdirSync(assetDir).filter((name) => statSync(path.join(assetDir, name)).isFile())); + const missing = [...expected].filter((name) => !actual.has(name)).sort(compareText); + if (missing.length > 0) { + fail(`liboliphaunt-native release asset directory is missing expected assets: ${missing.join(", ")}`); + } + const unexpected = [...actual].filter((name) => !expected.has(name)).sort(compareText); + if (unexpected.length > 0) { + fail(`liboliphaunt-native release asset directory contains unexpected assets: ${unexpected.join(", ")}`); + } + for (const filename of required) { + requireFile(path.join(assetDir, filename), `liboliphaunt release artifact ${filename}`); + } + const leakedExtensionAssets = [...actual] + .filter((name) => name.includes("extension") && !name.endsWith("-release-assets.sha256")) + .sort(compareText); + if (leakedExtensionAssets.length > 0) { + fail( + "liboliphaunt-native release assets must not include exact-extension artifacts; " + + `publish them through oliphaunt-extension-* products instead: ${leakedExtensionAssets.join(", ")}`, + ); + } + validateBaseRuntimeArtifactContents( + path.join(assetDir, `liboliphaunt-${version}-runtime-resources.tar.gz`), + metadata, + ); + validateNativeTargetArtifacts(assetDir, version); + validateIcuDataArtifactContents(path.join(assetDir, `liboliphaunt-${version}-icu-data.tar.gz`)); + validatePackageSizeReport(path.join(assetDir, `liboliphaunt-${version}-package-size.tsv`)); + validateChecksums(assetDir, path.join(assetDir, `liboliphaunt-${version}-release-assets.sha256`)); +} + +function parseArgs(argv) { + const args = { + assetDir: path.join(ROOT, "target/liboliphaunt/release-assets"), + }; + 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("--asset-dir requires a value"); + } + args.assetDir = path.resolve(ROOT, value); + index += 1; + } else { + fail(`unknown argument ${arg}`); + } + } + return args; +} + +const args = parseArgs(Bun.argv.slice(2)); +if (!existsSync(args.assetDir) || !statSync(args.assetDir).isDirectory()) { + fail(`release asset directory does not exist: ${args.assetDir}`); +} +await validate(args.assetDir); +console.log(`liboliphaunt release assets validated: ${rel(args.assetDir)}`); diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py index 441e5ac4..acc8c066 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -294,8 +294,8 @@ def validate_github_asset_helpers() -> None: "GitHub release asset checks must derive product assets from product-local artifact targets", ) require_text( - "tools/release/check_liboliphaunt_release_assets.py", - "artifact_targets.expected_assets", + "tools/release/check-liboliphaunt-release-assets.mjs", + "allArtifactTargets", "liboliphaunt release asset checks must derive required assets from product-local artifact targets", ) require_text( diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 21099f87..b69fff8d 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -622,7 +622,7 @@ def check_liboliphaunt(findings: list[Finding]) -> None: ], "tools/release/package-liboliphaunt-aggregate-assets.sh": [ "liboliphaunt-${version}-release-assets.sha256", - "check_liboliphaunt_release_assets.py", + "check-liboliphaunt-release-assets.mjs", ], } for script_path, required_snippets in packaging_scripts.items(): diff --git a/tools/release/check_liboliphaunt_release_assets.py b/tools/release/check_liboliphaunt_release_assets.py deleted file mode 100755 index 08afb46c..00000000 --- a/tools/release/check_liboliphaunt_release_assets.py +++ /dev/null @@ -1,679 +0,0 @@ -#!/usr/bin/env python3 -"""Validate liboliphaunt GitHub release assets before upload.""" - -from __future__ import annotations - -import argparse -import csv -import hashlib -import json -import shutil -import subprocess -import sys -import tarfile -import tempfile -import zipfile -from pathlib import Path, PurePosixPath -from typing import NoReturn - -import artifact_targets -import product_metadata - - -ROOT = Path(__file__).resolve().parents[2] - - -def fail(message: str) -> NoReturn: - print(f"check_liboliphaunt_release_assets.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 require_file(path: Path, description: str) -> None: - if not path.is_file(): - fail(f"missing {description}: {path}") - if path.stat().st_size <= 0: - fail(f"{description} is empty: {path}") - - -def parse_checksum_file(path: Path) -> dict[str, str]: - checksums: dict[str, str] = {} - for line in path.read_text(encoding="utf-8").splitlines(): - if not line.strip(): - continue - parts = line.split() - if len(parts) != 2: - fail(f"malformed checksum line in {path}: {line!r}") - digest, filename = parts - if not filename.startswith("./"): - fail(f"checksum path must be relative './name': {filename}") - checksums[filename[2:]] = digest - return checksums - - -def validate_checksums(asset_dir: Path, checksum_file: Path) -> None: - checksums = parse_checksum_file(checksum_file) - expected_assets = sorted( - path - for path in asset_dir.iterdir() - if path.is_file() and path.suffix != ".sha256" - ) - if not expected_assets: - fail(f"no release assets found in {asset_dir}") - for asset in expected_assets: - recorded = checksums.get(asset.name) - if recorded is None: - fail(f"checksum file does not cover release asset: {asset.name}") - actual = sha256(asset) - if recorded != actual: - fail(f"checksum mismatch for {asset.name}: expected {recorded}, got {actual}") - extra = sorted(set(checksums) - {asset.name for asset in expected_assets}) - if extra: - fail("checksum file contains entries for missing assets: " + ", ".join(extra)) - - -def generated_extension_metadata() -> dict[str, dict[str, object]]: - metadata_path = ROOT / "src/extensions/generated/sdk/rust.json" - try: - metadata = json.loads(metadata_path.read_text(encoding="utf-8")) - except OSError as error: - fail(f"read generated Rust SDK extension metadata {metadata_path}: {error}") - except json.JSONDecodeError as error: - fail(f"parse generated Rust SDK extension metadata {metadata_path}: {error}") - rows = metadata.get("extensions") - if not isinstance(rows, list): - fail(f"{metadata_path} must define an extensions array") - expected: dict[str, dict[str, object]] = {} - for index, row in enumerate(rows): - if not isinstance(row, dict): - fail(f"{metadata_path} extensions[{index}] must be an object") - sql_name = row.get("sql-name") - if not isinstance(sql_name, str) or not sql_name: - fail(f"{metadata_path} extensions[{index}] must define sql-name") - data_files = row.get("runtime-share-data-files") - if not isinstance(data_files, list) or not all(isinstance(value, str) for value in data_files): - fail(f"{metadata_path} extension {sql_name} must define runtime-share-data-files") - native_module_stem = row.get("native-module-stem") - if native_module_stem is not None and not isinstance(native_module_stem, str): - fail(f"{metadata_path} extension {sql_name} native-module-stem must be a string or null") - expected[sql_name] = { - "creates_extension": row.get("creates-extension") is True, - "data_files": data_files, - "data_files_tsv": ",".join(data_files) if data_files else "-", - "native_module_stem": native_module_stem, - } - return expected - - -def tar_member_names(path: Path) -> set[str]: - try: - with tarfile.open(path, "r:*") as archive: - names = set() - for member in archive.getmembers(): - name = member.name.removeprefix("./").rstrip("/") - if name: - names.add(name) - return names - except tarfile.TarError as error: - fail(f"{path} is not a readable tar archive: {error}") - - -def tar_text(path: Path, member_name: str) -> str: - try: - with tarfile.open(path, "r:*") as archive: - member = archive.getmember(member_name) - extracted = archive.extractfile(member) - if extracted is None: - fail(f"{path} member {member_name} is not a regular file") - return extracted.read().decode("utf-8") - except KeyError: - fail(f"{path} is missing {member_name}") - except UnicodeDecodeError as error: - fail(f"{path} member {member_name} is not UTF-8: {error}") - except tarfile.TarError as error: - 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, - tool_set: str, -) -> None: - with tempfile.TemporaryDirectory(prefix=f"oliphaunt-native-{target}-") as temp: - extracted = Path(temp) / "payload" - extract_archive(path, extracted) - command = [ - "tools/dev/bun.sh", - "tools/release/optimize_native_runtime_payload.mjs", - str(extracted), - "--target", - target, - "--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: - 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, - 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", - ) - - -def validate_base_runtime_artifact_contents( - path: Path, - extension_metadata: dict[str, dict[str, object]], -) -> None: - names = tar_member_names(path) - runtime_prefix = "oliphaunt/runtime/files/" - for required_member in [ - "oliphaunt/package-size.tsv", - "oliphaunt/runtime/manifest.properties", - "oliphaunt/template-pgdata/manifest.properties", - ]: - if required_member not in names: - fail(f"{path} must contain {required_member}") - if f"{runtime_prefix}share/postgresql/README.release-fixture" not in names and not any( - name.startswith(runtime_prefix) for name in names - ): - fail(f"{path} must contain an oliphaunt/runtime/files tree") - if any(name.startswith(f"{runtime_prefix}share/icu/") for name in names): - fail(f"{path} base runtime must not contain ICU data under {runtime_prefix}share/icu") - for sql_name, metadata in extension_metadata.items(): - control = f"{runtime_prefix}share/postgresql/extension/{sql_name}.control" - if control in names: - fail(f"{path} base runtime must not contain optional extension control file {control}") - for data_file in metadata["data_files"]: - data_path = f"{runtime_prefix}share/postgresql/{data_file}" - if data_path in names: - fail(f"{path} base runtime must not contain optional extension data file {data_path}") - stem = metadata.get("native_module_stem") - if isinstance(stem, str) and stem: - for suffix in (".dylib", ".so", ".dll"): - module = f"{runtime_prefix}lib/postgresql/{stem}{suffix}" - if module in names: - fail(f"{path} base runtime must not contain optional extension module {module}") - - -def validate_icu_data_artifact_contents(path: Path) -> None: - names = tar_member_names(path) - icu_entries = sorted( - name - for name in names - if name.startswith("share/icu/") - and Path(name).relative_to("share/icu").parts - and Path(name).relative_to("share/icu").parts[0].startswith("icudt") - ) - if not icu_entries: - fail(f"{path} must contain ICU data files under share/icu/icudt*") - unexpected = sorted( - name - for name in names - if name != "." - and name not in {"share", "share/icu"} - and not name.startswith("share/icu/") - ) - if unexpected: - fail(f"{path} must contain only share/icu data, found: {', '.join(unexpected[:5])}") - - -def validate_extension_runtime_artifact_contents( - path: Path, - row: dict[str, str], - extension_metadata: dict[str, dict[str, object]], -) -> None: - sql_name = row["sql_name"] - metadata = extension_metadata[sql_name] - names = tar_member_names(path) - manifest = tar_text(path, "manifest.properties") - for expected in [ - "packageLayout=oliphaunt-extension-artifact-v1\n", - f"sqlName={sql_name}\n", - "files=files\n", - ]: - if expected not in manifest: - fail(f"{path} manifest must contain {expected.strip()!r}") - if not any(name.startswith("files/") for name in names): - fail(f"{path} must contain a files/ runtime tree") - if metadata["creates_extension"]: - control = f"files/share/postgresql/extension/{sql_name}.control" - if control not in names: - fail(f"{path} must contain selected extension control file {control}") - sql_prefix = f"files/share/postgresql/extension/{sql_name}--" - if not any(name.startswith(sql_prefix) and name.endswith(".sql") for name in names): - fail(f"{path} must contain at least one selected extension SQL file under {sql_prefix}*.sql") - stem = row["native_module_stem"] - if stem != "-": - module = f"files/lib/postgresql/{stem}.dylib" - if module not in names: - fail(f"{path} must contain selected extension native module {module}") - expected_data_files = set(metadata["data_files"]) - for data_file in sorted(expected_data_files): - data_path = f"files/share/postgresql/{data_file}" - if data_path not in names: - fail(f"{path} must contain selected extension data file {data_path}") - for other_sql_name, other_metadata in extension_metadata.items(): - if other_sql_name == sql_name: - continue - other_control = f"files/share/postgresql/extension/{other_sql_name}.control" - if other_control in names: - fail(f"{path} for {sql_name} must not contain unselected extension control file {other_control}") - other_stem = other_metadata.get("native_module_stem") - if isinstance(other_stem, str) and other_stem: - for suffix in (".dylib", ".so", ".dll"): - other_module = f"files/lib/postgresql/{other_stem}{suffix}" - if other_module in names: - fail(f"{path} for {sql_name} must not contain unselected extension module {other_module}") - for data_file in other_metadata["data_files"]: - if data_file in expected_data_files: - continue - other_data = f"files/share/postgresql/{data_file}" - if other_data in names: - fail(f"{path} for {sql_name} must not contain unselected extension data file {other_data}") - - -def validate_android_extension_artifact( - path: Path, - row: dict[str, str], - abi: str, -) -> None: - sql_name = row["sql_name"] - stem = row["native_module_stem"] - names = tar_member_names(path) - manifest = tar_text(path, "manifest.properties") - expected_archive = f"extensions/{stem}/liboliphaunt_extension_{stem}.a" - for expected in [ - "packageLayout=liboliphaunt-android-extension-artifact-v1\n", - f"abi={abi}\n", - f"sqlName={sql_name}\n", - f"nativeModuleStem={stem}\n", - f"archive={expected_archive}\n", - ]: - if expected not in manifest: - fail(f"{path} manifest must contain {expected.strip()!r}") - if expected_archive not in names: - fail(f"{path} must contain selected Android static archive {expected_archive}") - - -def validate_extension_index( - asset_dir: Path, - index_file: Path, - extension_metadata: dict[str, dict[str, object]], -) -> None: - required_columns = [ - "sql_name", - "creates_extension", - "native_module_stem", - "dependencies", - "shared_preload", - "mobile_prebuilt", - "mobile_static_archive_targets", - "runtime_artifact", - "ios_xcframework_artifact", - "android_arm64_artifact", - "android_x86_64_artifact", - "runtime_artifact_bytes", - "ios_xcframework_artifact_bytes", - "android_arm64_artifact_bytes", - "android_x86_64_artifact_bytes", - "data_files", - ] - with index_file.open("r", encoding="utf-8", newline="") as file: - reader = csv.DictReader(file, delimiter="\t") - if reader.fieldnames != required_columns: - fail(f"{index_file} has unexpected header: {reader.fieldnames}") - row_count = 0 - seen_sql_names: set[str] = set() - for row in reader: - row_count += 1 - sql_name = row["sql_name"] - if not sql_name: - fail(f"{index_file} row {row_count} has empty sql_name") - if sql_name in seen_sql_names: - fail(f"{index_file} contains duplicate sql_name {sql_name}") - seen_sql_names.add(sql_name) - runtime_artifact = row["runtime_artifact"] - if runtime_artifact == "-": - fail(f"{sql_name} must reference a runtime extension artifact") - require_file(asset_dir / runtime_artifact, f"{sql_name} runtime extension artifact") - metadata = extension_metadata.get(sql_name) - if metadata is None: - fail(f"{sql_name} is missing from generated Rust SDK extension metadata") - expected_creates_extension = "yes" if metadata["creates_extension"] else "no" - if row["creates_extension"] != expected_creates_extension: - fail( - f"{sql_name} creates_extension must match generated metadata: " - f"expected {expected_creates_extension!r}, got {row['creates_extension']!r}" - ) - expected_stem = metadata["native_module_stem"] or "-" - if row["native_module_stem"] != expected_stem: - fail( - f"{sql_name} native_module_stem must match generated metadata: " - f"expected {expected_stem!r}, got {row['native_module_stem']!r}" - ) - expected_data_files = metadata["data_files_tsv"] - if row["data_files"] != expected_data_files: - fail( - f"{sql_name} release artifact index data_files must match generated metadata: " - f"expected {expected_data_files!r}, got {row['data_files']!r}" - ) - validate_extension_runtime_artifact_contents( - asset_dir / runtime_artifact, - row, - extension_metadata, - ) - validate_recorded_bytes( - asset_dir, - runtime_artifact, - row["runtime_artifact_bytes"], - f"{sql_name} runtime extension artifact", - ) - if row["mobile_prebuilt"] == "yes" and row["native_module_stem"] != "-": - ios_artifact = row["ios_xcframework_artifact"] - android_arm64_artifact = row["android_arm64_artifact"] - android_x86_64_artifact = row["android_x86_64_artifact"] - if ios_artifact == "-" or android_arm64_artifact == "-" or android_x86_64_artifact == "-": - fail(f"{sql_name} is mobile-prebuilt but missing mobile artifact references") - require_file(asset_dir / ios_artifact, f"{sql_name} iOS extension artifact") - validate_swiftpm_xcframework_zip( - asset_dir / ios_artifact, - f"liboliphaunt_extension_{row['native_module_stem']}.xcframework", - f"{sql_name} iOS SwiftPM extension artifact", - ) - require_file(asset_dir / android_arm64_artifact, f"{sql_name} Android arm64 extension artifact") - require_file(asset_dir / android_x86_64_artifact, f"{sql_name} Android x86_64 extension artifact") - validate_android_extension_artifact( - asset_dir / android_arm64_artifact, - row, - "arm64-v8a", - ) - validate_android_extension_artifact( - asset_dir / android_x86_64_artifact, - row, - "x86_64", - ) - validate_recorded_bytes( - asset_dir, - ios_artifact, - row["ios_xcframework_artifact_bytes"], - f"{sql_name} iOS extension artifact", - ) - validate_recorded_bytes( - asset_dir, - android_arm64_artifact, - row["android_arm64_artifact_bytes"], - f"{sql_name} Android arm64 extension artifact", - ) - validate_recorded_bytes( - asset_dir, - android_x86_64_artifact, - row["android_x86_64_artifact_bytes"], - f"{sql_name} Android x86_64 extension artifact", - ) - else: - for column in [ - "ios_xcframework_artifact", - "android_arm64_artifact", - "android_x86_64_artifact", - "ios_xcframework_artifact_bytes", - "android_arm64_artifact_bytes", - "android_x86_64_artifact_bytes", - ]: - if row[column] != "-": - fail(f"{sql_name} {column} must be '-' when no mobile artifact is referenced") - if row_count == 0: - fail(f"{index_file} contains no extension rows") - - -def validate_recorded_bytes( - asset_dir: Path, - artifact: str, - recorded: str, - description: str, -) -> None: - if artifact == "-": - if recorded != "-": - fail(f"{description} byte count must be '-' when artifact is '-'") - return - try: - expected = int(recorded) - except ValueError: - fail(f"{description} byte count is not an integer: {recorded!r}") - actual = (asset_dir / artifact).stat().st_size - if expected != actual: - fail(f"{description} byte count mismatch for {artifact}: expected {expected}, got {actual}") - - -def parse_size_value(value: str, path: Path, line_number: int, field: str) -> int: - try: - parsed = int(value) - except ValueError: - fail(f"{path} line {line_number} has invalid {field}: {value!r}") - if parsed < 0: - fail(f"{path} line {line_number} has negative {field}: {value!r}") - return parsed - - -def validate_package_size_report(path: Path) -> None: - require_file(path, "liboliphaunt package-size release report") - with path.open("r", encoding="utf-8", newline="") as file: - reader = csv.DictReader(file, delimiter="\t") - expected_header = ["kind", "id", "extensions", "files", "bytes"] - if reader.fieldnames != expected_header: - fail(f"{path} has unexpected header: {reader.fieldnames}") - rows: dict[tuple[str, str], dict[str, str]] = {} - extension_rows: list[str] = [] - for line_number, row in enumerate(reader, start=2): - key = (row["kind"], row["id"]) - if key in rows: - fail(f"{path} repeats row {row['kind']}/{row['id']}") - rows[key] = row - parse_size_value(row["bytes"], path, line_number, "bytes") - if row["kind"] == "extension": - extension_rows.append(row["id"]) - parse_size_value(row["files"], path, line_number, "files") - elif row["files"] != "-": - fail(f"{path} line {line_number} package rows must use '-' for files") - - required_rows = [ - ("package", "total"), - ("package", "runtime"), - ("package", "template-pgdata"), - ("package", "static-registry"), - ("extensions", "selected"), - ] - missing = [f"{kind}/{identifier}" for kind, identifier in required_rows if (kind, identifier) not in rows] - if missing: - fail(f"{path} is missing required row(s): {', '.join(missing)}") - if rows[("extensions", "selected")]["bytes"] != "0": - fail(f"{path} base package-size report must have zero selected extension bytes") - if extension_rows: - fail( - f"{path} base package-size report must not include selected extension rows: " - + ", ".join(sorted(extension_rows)) - ) - total = parse_size_value(rows[("package", "total")]["bytes"], path, 0, "package total bytes") - parts = sum( - parse_size_value(rows[key]["bytes"], path, 0, f"{key[0]}/{key[1]} bytes") - for key in [ - ("package", "runtime"), - ("package", "template-pgdata"), - ("package", "static-registry"), - ] - ) - if total != parts: - fail(f"{path} package total bytes must equal runtime + template-pgdata + static-registry") - - -def validate_swiftpm_xcframework_zip(path: Path, expected_xcframework: str, description: str) -> None: - if path.suffix != ".zip": - fail(f"{description} must be a SwiftPM-compatible XCFramework .zip artifact: {path.name}") - try: - with zipfile.ZipFile(path) as archive: - names = archive.namelist() - except zipfile.BadZipFile: - fail(f"{description} is not a valid zip archive: {path}") - info_plist = f"{expected_xcframework}/Info.plist" - if info_plist not in names: - fail(f"{description} must contain {info_plist}") - nested_manifests = [name for name in names if name.endswith("/manifest.properties")] - if nested_manifests: - fail( - f"{description} must contain exactly the XCFramework for SwiftPM, " - "not the generic staged extension tarball layout" - ) - - -def validate(asset_dir: Path) -> None: - version = product_metadata.read_current_version("liboliphaunt-native") - metadata = generated_extension_metadata() - required = artifact_targets.expected_assets("liboliphaunt-native", version, surface="github-release") - expected = set(required) - actual = {path.name for path in asset_dir.iterdir() if path.is_file()} - missing = sorted(expected - actual) - if missing: - fail("liboliphaunt-native release asset directory is missing expected assets: " + ", ".join(missing)) - unexpected = sorted(actual - expected) - if unexpected: - fail("liboliphaunt-native release asset directory contains unexpected assets: " + ", ".join(unexpected)) - for filename in required: - require_file(asset_dir / filename, f"liboliphaunt release artifact {filename}") - leaked_extension_assets = sorted( - path.name - for path in asset_dir.iterdir() - if path.is_file() - and "extension" in path.name - and not path.name.endswith("-release-assets.sha256") - ) - if leaked_extension_assets: - fail( - "liboliphaunt-native release assets must not include exact-extension artifacts; " - "publish them through oliphaunt-extension-* products instead: " - + ", ".join(leaked_extension_assets) - ) - validate_base_runtime_artifact_contents( - 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") - - -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", - ) - return parser.parse_args(argv) - - -def main(argv: list[str]) -> int: - args = parse_args(argv) - asset_dir = (ROOT / args.asset_dir).resolve() - if not asset_dir.is_dir(): - fail(f"release asset directory does not exist: {asset_dir}") - validate(asset_dir) - print(f"liboliphaunt release assets validated: {asset_dir}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) diff --git a/tools/release/package-liboliphaunt-aggregate-assets.sh b/tools/release/package-liboliphaunt-aggregate-assets.sh index fa838a1d..08c490db 100755 --- a/tools/release/package-liboliphaunt-aggregate-assets.sh +++ b/tools/release/package-liboliphaunt-aggregate-assets.sh @@ -26,4 +26,4 @@ tools/release/write_checksum_manifest.mjs \ --pattern '*.zip' \ --pattern '*.tsv' -tools/release/check_liboliphaunt_release_assets.py --asset-dir "$asset_dir" +tools/dev/bun.sh tools/release/check-liboliphaunt-release-assets.mjs --asset-dir "$asset_dir" diff --git a/tools/release/release.py b/tools/release/release.py index 62f207f9..c4cf0684 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -1289,7 +1289,7 @@ def liboliphaunt_release_assets_ready() -> bool: def ensure_liboliphaunt_release_assets() -> None: if liboliphaunt_release_assets_ready(): - run(["tools/release/check_liboliphaunt_release_assets.py", "--asset-dir", "target/liboliphaunt/release-assets"]) + run(["tools/dev/bun.sh", "tools/release/check-liboliphaunt-release-assets.mjs", "--asset-dir", "target/liboliphaunt/release-assets"]) return fail( "liboliphaunt-native requires staged release assets under " From bfbc87bff9d4af134b2158c23a86f128fc416408 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 02:23:00 +0000 Subject: [PATCH 141/308] chore: port release pr sync to bun --- .github/workflows/release.yml | 4 +- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 13 + tools/policy/python-entrypoints.allowlist | 1 - tools/release/release.py | 2 +- tools/release/sync-release-pr.mjs | 809 ++++++++++++++++++ tools/release/sync_release_pr.py | 638 -------------- 6 files changed, 825 insertions(+), 642 deletions(-) create mode 100644 tools/release/sync-release-pr.mjs delete mode 100755 tools/release/sync_release_pr.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 51576768..09b0aaa5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -149,8 +149,8 @@ jobs: git fetch origin "+refs/heads/${release_pr_head}:refs/remotes/origin/${release_pr_head}" git switch -C "${release_pr_head}" "origin/${release_pr_head}" - tools/release/sync_release_pr.py - tools/release/sync_release_pr.py --check + tools/dev/bun.sh tools/release/sync-release-pr.mjs + tools/dev/bun.sh tools/release/sync-release-pr.mjs --check tools/release/release.py check if git diff --quiet; then diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index d8e4a9f0..0e54d955 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -129,6 +129,19 @@ until the current-state gates here are checked with fresh local evidence. tools/policy/check-python-entrypoints.mjs`, `python3 -m py_compile` for touched Python release checks, full `python3 tools/release/check_consumer_shape.py`, `tools/release/release.py check`, and `git diff --cached --check`. +- 2026-06-27: Ported release PR derived-file synchronization from + `tools/release/sync_release_pr.py` to `tools/release/sync-release-pr.mjs`. + The release workflow and `release.py check` now use the Bun sync/check path + through `tools/dev/bun.sh`; the script still delegates extension evidence + validation to the existing extension model generator and preserves the + `--check`/write contract. Fresh parity checks passed: + `tools/dev/bun.sh tools/release/sync-release-pr.mjs --check` and + `tools/release/sync_release_pr.py --check` before removing the Python file. + Follow-up checks passed: `tools/dev/bun.sh + tools/policy/check-python-entrypoints.mjs`, `bash + tools/policy/check-tooling-stack.sh`, `python3 + tools/policy/check-release-policy.py`, `tools/release/release.py check`, and + `git diff --cached --check`. - 2026-06-27: Added and pushed the native Rust `oliphaunt-tools` Cargo facade crate so consumer manifests can depend on the facade while Cargo selects the target `oliphaunt-tools-*` payload crate. The Rust SDK release renderer now diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index 47708a22..a9950d42 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -14,4 +14,3 @@ 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/sync_release_pr.py diff --git a/tools/release/release.py b/tools/release/release.py index c4cf0684..f7437052 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -1793,7 +1793,7 @@ def command_check(args: list[str]) -> None: run(["python3", "tools/policy/check-release-policy.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(["tools/dev/bun.sh", "tools/release/sync-release-pr.mjs", "--check"]) 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"]) diff --git a/tools/release/sync-release-pr.mjs b/tools/release/sync-release-pr.mjs new file mode 100644 index 00000000..4a115ac8 --- /dev/null +++ b/tools/release/sync-release-pr.mjs @@ -0,0 +1,809 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; +import { + existsSync, + readdirSync, + readFileSync, + realpathSync, + statSync, + writeFileSync, +} from "node:fs"; +import path from "node:path"; + +import { + ROOT, + allArtifactTargets, + compareText, + currentProductVersion, + exactExtensionProducts, + extensionArtifactTargets, +} from "./release-artifact-targets.mjs"; +import { loadGraph } from "./release-graph.mjs"; + +const PREFIX = "sync-release-pr.mjs"; +const DEPENDENCY_TABLES = ["dependencies", "dev-dependencies", "build-dependencies"]; +const LOCKFILES = [ + path.join(ROOT, "Cargo.lock"), + path.join(ROOT, "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock"), +]; +const PNPM_LOCKFILE = path.join(ROOT, "pnpm-lock.yaml"); +const PACKAGE_START_RE = /^\s*\[\[package\]\]\s*$/u; +const STRING_KEY_RE = /^\s*([A-Za-z0-9_-]+)\s*=\s*"([^"]*)"\s*(?:#.*)?$/u; +const VERSION_LINE_RE = /^(\s*version\s*=\s*)"[^"]*"(\s*(?:#.*)?)$/u; +const TOML_TABLE_RE = /^\s*\[([A-Za-z0-9_.-]+)\]\s*(?:#.*)?$/u; +const PNPM_TYPESCRIPT_OPTIONAL_RUNTIME_KEY_RE = + /^(\s*)'(@oliphaunt\/(?:broker|liboliphaunt|node-direct|tools)-[^']+)':\s*$/u; +const PNPM_SPECIFIER_RE = /^(\s*specifier:\s*)(\S+)(\s*)$/u; +const ASSET_INPUT_FINGERPRINT_PATH = path.join( + ROOT, + "src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256", +); +const ASSET_INPUT_FINGERPRINT_MISMATCH_RE = + /committed asset input fingerprint must be '([0-9a-f]+)', got '([0-9a-f]+)'/u; +const EXTENSION_EVIDENCE_PATHS = [ + path.join(ROOT, "src/extensions/evidence/matrix.toml"), + path.join(ROOT, "src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json"), + path.join(ROOT, "src/extensions/generated/docs/extension-evidence.json"), +]; +const EXTENSION_EVIDENCE_STALE_RE = + /([^:\n]+\.json) sourceDigest is stale; expected (sha256:[0-9a-f]{64}), got '([^']*)'/gu; + +function fail(message) { + console.error(`${PREFIX}: ${message}`); + process.exit(2); +} + +function rel(file) { + const relative = path.relative(ROOT, file); + if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) { + return file.split(path.sep).join("/"); + } + return relative.split(path.sep).join("/"); +} + +function readText(file) { + return readFileSync(file, "utf8"); +} + +function readOptionalText(file) { + return existsSync(file) ? readText(file) : undefined; +} + +function readJsonObject(file) { + const value = JSON.parse(readText(file)); + if (value === null || Array.isArray(value) || typeof value !== "object") { + fail(`${rel(file)} must contain a JSON object`); + } + return value; +} + +function jsonText(value) { + return `${JSON.stringify(value, null, 2)}\n`; +} + +function writeTextIfChanged(file, text, changes, detail, { write }) { + const before = readText(file); + if (before === text) { + return; + } + changes.push({ path: file, detail }); + if (write) { + writeFileSync(file, text, "utf8"); + } +} + +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 graphProducts() { + return loadGraph(PREFIX).products; +} + +function productConfig(product) { + const products = graphProducts(); + const config = products[product]; + if (!config) { + fail(`unknown release product ${JSON.stringify(product)}`); + } + return config; +} + +function packagePath(product) { + return productConfig(product).path; +} + +function compatibilityVersionLinks() { + const products = graphProducts(); + const known = new Set(Object.keys(products)); + const specs = {}; + for (const [product, config] of Object.entries(products)) { + const rawSpecs = config.compatibility_versions ?? {}; + if (rawSpecs === null || Array.isArray(rawSpecs) || typeof rawSpecs !== "object") { + fail(`${product}.compatibility_versions must be a table when present`); + } + for (const [specId, spec] of Object.entries(rawSpecs)) { + if (!specId) { + fail(`${product}.compatibility_versions keys must be non-empty strings`); + } + if (spec === null || Array.isArray(spec) || typeof spec !== "object") { + fail(`${product}.compatibility_versions.${specId} must be a table`); + } + const sourceProduct = spec.source_product; + if (typeof sourceProduct !== "string" || !sourceProduct) { + fail(`${product}.compatibility_versions.${specId}.source_product must be a non-empty string`); + } + if (!known.has(sourceProduct)) { + fail(`${product}.compatibility_versions.${specId}.source_product must name a release product, got ${JSON.stringify(sourceProduct)}`); + } + const specPath = spec.path; + const parser = spec.parser; + if (typeof specPath !== "string" || !specPath) { + fail(`${product}.compatibility_versions.${specId}.path must be a non-empty string`); + } + if (typeof parser !== "string" || !parser) { + fail(`${product}.compatibility_versions.${specId}.parser must be a non-empty string`); + } + if (!existsSync(path.join(ROOT, specPath))) { + fail(`${product}.compatibility_versions.${specId} path does not exist: ${specPath}`); + } + specs[specId] = [sourceProduct, specPath, parser]; + } + } + return specs; +} + +function setJsonPath(data, dotted, expected, context) { + let current = data; + const parts = dotted.split("."); + for (const part of parts.slice(0, -1)) { + if (current === null || Array.isArray(current) || typeof current !== "object" || current[part] === null || Array.isArray(current[part]) || typeof current[part] !== "object") { + fail(`${context} is missing object path ${parts.slice(0, -1).join(".")}`); + } + current = current[part]; + } + if (current === null || Array.isArray(current) || typeof current !== "object") { + fail(`${context} is missing object path ${parts.slice(0, -1).join(".")}`); + } + const key = parts.at(-1); + const actual = current[key]; + if (actual === expected) { + return undefined; + } + current[key] = expected; + return `${context} ${JSON.stringify(actual)} -> ${JSON.stringify(expected)}`; +} + +function setTomlStringPath(file, dotted, expected, context) { + const parts = dotted.split("."); + if (parts.length < 2) { + fail(`${context} TOML parser must use table.key dotted syntax`); + } + const table = parts.slice(0, -1); + const key = parts.at(-1); + const lines = readText(file).split(/(?<=\n)/u); + let currentTable = []; + let sawTable = false; + const keyPattern = new RegExp(`^(\\s*${escapeRegExp(key)}\\s*=\\s*)"([^"]*)"(.*)$`, "u"); + + for (const [index, line] of lines.entries()) { + const [body, newline] = stripNewline(line); + const tableMatch = TOML_TABLE_RE.exec(body); + if (tableMatch) { + currentTable = tableMatch[1].split("."); + sawTable = arraysEqual(currentTable, table); + continue; + } + if (!arraysEqual(currentTable, table)) { + continue; + } + const keyMatch = keyPattern.exec(body); + if (!keyMatch) { + continue; + } + const actual = keyMatch[2]; + if (actual === expected) { + return [undefined, undefined]; + } + lines[index] = `${keyMatch[1]}"${expected}"${keyMatch[3]}${newline}`; + return [lines.join(""), `${context} ${JSON.stringify(actual)} -> ${JSON.stringify(expected)}`]; + } + + if (sawTable) { + fail(`${context} did not find TOML key ${JSON.stringify(key)} in ${rel(file)}`); + } + fail(`${context} did not find TOML table ${JSON.stringify(table.join("."))} in ${rel(file)}`); +} + +function setRustConstString(file, constName, expected, context) { + const lines = readText(file).split(/(?<=\n)/u); + const pattern = new RegExp(`^(\\s*(?:pub\\s+)?const\\s+${escapeRegExp(constName)}\\s*:\\s*&str\\s*=\\s*)"([^"]*)"(;.*)$`, "u"); + for (const [index, line] of lines.entries()) { + const [body, newline] = stripNewline(line); + const match = pattern.exec(body); + if (!match) { + continue; + } + const actual = match[2]; + if (actual === expected) { + return [undefined, undefined]; + } + lines[index] = `${match[1]}"${expected}"${match[3]}${newline}`; + return [lines.join(""), `${context} ${JSON.stringify(actual)} -> ${JSON.stringify(expected)}`]; + } + fail(`${context} did not find Rust const ${JSON.stringify(constName)} in ${rel(file)}`); +} + +function tomlArrayAssignment(key, values) { + if (values.length === 1) { + return `${key} = [${JSON.stringify(values[0])}]\n`; + } + return `${key} = [\n${values.map((value) => ` ${JSON.stringify(value)},\n`).join("")}]\n`; +} + +function replaceTopLevelArrayAssignment(text, key, values, context) { + const lines = text.split(/(?<=\n)/u); + const output = []; + let index = 0; + let replaced = false; + const pattern = new RegExp(`^${escapeRegExp(key)}\\s*=\\s*\\[`, "u"); + while (index < lines.length) { + const line = lines[index]; + if (!replaced && pattern.test(line)) { + output.push(tomlArrayAssignment(key, values)); + replaced = true; + if (!line.includes("]")) { + index += 1; + while (index < lines.length && !lines[index].includes("]")) { + index += 1; + } + } + index += 1; + continue; + } + output.push(line); + index += 1; + } + if (!replaced) { + fail(`${context} did not find top-level TOML array ${JSON.stringify(key)}`); + } + return output.join(""); +} + +function publishedAndroidMavenTargets(product) { + return extensionArtifactTargets({ product, family: "native", publishedOnly: true }, PREFIX) + .filter((target) => target.kind === "native-static-registry" && target.target.startsWith("android-")) + .sort((left, right) => compareText(left.target, right.target)); +} + +function syncExtensionMavenRegistryMetadata(changes, { write }) { + const expectedPublishTargets = ["github-release-assets", "maven-central"]; + for (const product of exactExtensionProducts(PREFIX)) { + const releaseToml = path.join(ROOT, packagePath(product), "release.toml"); + const expectedRegistryPackages = publishedAndroidMavenTargets(product).map( + (target) => `maven:dev.oliphaunt.extensions:${product}-${target.target}`, + ); + const text = readText(releaseToml); + let updated = replaceTopLevelArrayAssignment(text, "publish_targets", expectedPublishTargets, product); + updated = replaceTopLevelArrayAssignment(updated, "registry_packages", expectedRegistryPackages, product); + if (updated !== text) { + writeTextIfChanged(releaseToml, updated, changes, "synced explicit Maven registry metadata", { write }); + } + } +} + +async function syncCompatibilityVersions(changes, { write }) { + const links = compatibilityVersionLinks(); + for (const specId of Object.keys(links).sort(compareText)) { + const [sourceProduct, pathText, parser] = links[specId]; + const file = path.join(ROOT, pathText); + const expected = await currentProductVersion(sourceProduct, PREFIX); + if (parser === "raw") { + writeTextIfChanged(file, `${expected}\n`, changes, `${specId} -> ${sourceProduct} ${expected}`, { write }); + continue; + } + if (parser.startsWith("json:")) { + const data = readJsonObject(file); + const detail = setJsonPath(data, parser.split(":", 2)[1], expected, specId); + if (detail !== undefined) { + writeTextIfChanged(file, jsonText(data), changes, detail, { write }); + } + continue; + } + if (parser.startsWith("toml:")) { + const [text, detail] = setTomlStringPath(file, parser.split(":", 2)[1], expected, specId); + if (text !== undefined && detail !== undefined) { + writeTextIfChanged(file, text, changes, detail, { write }); + } + continue; + } + if (parser.startsWith("rust-const:")) { + const [text, detail] = setRustConstString(file, parser.split(":", 2)[1], expected, specId); + if (text !== undefined && detail !== undefined) { + writeTextIfChanged(file, text, changes, detail, { write }); + } + continue; + } + fail(`${specId} uses unsupported sync parser ${JSON.stringify(parser)}`); + } +} + +async function expectedTypescriptOptionalRuntimeVersions() { + const versions = {}; + for (const [packageName, product] of typescriptOptionalRuntimePackageProducts()) { + versions[packageName] = `workspace:${await currentProductVersion(product, PREFIX)}`; + } + return versions; +} + +function typescriptOptionalRuntimePackageProducts() { + const selected = allArtifactTargets({ publishedOnly: true }, PREFIX) + .filter((target) => { + if (target.product === "oliphaunt-broker" && target.kind === "broker-helper") { + return target.surfaces.includes("typescript-broker"); + } + if (target.product === "liboliphaunt-native" && ["native-runtime", "native-tools"].includes(target.kind)) { + return target.surfaces.includes("typescript-native-direct"); + } + if (target.product === "oliphaunt-node-direct" && target.kind === "node-direct-addon") { + return target.surfaces.includes("npm-optional"); + } + return false; + }); + if (selected.length === 0) { + fail("no TypeScript optional runtime package targets found"); + } + const pairs = []; + const seen = new Set(); + for (const target of selected) { + if (typeof target.npmPackage !== "string" || !target.npmPackage) { + fail(`${target.id} must declare npmPackage for TypeScript optional dependencies`); + } + if (seen.has(target.npmPackage)) { + fail(`duplicate TypeScript optional package target ${target.npmPackage}`); + } + seen.add(target.npmPackage); + pairs.push([target.npmPackage, target.product]); + } + return pairs.sort(([left], [right]) => compareText(left, right)); +} + +function typescriptOptionalRuntimePackages() { + return typescriptOptionalRuntimePackageProducts().map(([packageName]) => packageName); +} + +async function syncTypescriptOptionalRuntimeDependencies(changes, { write }) { + const file = path.join(ROOT, "src/sdks/js/package.json"); + const data = readJsonObject(file); + const optional = data.optionalDependencies; + if (optional === null || Array.isArray(optional) || typeof optional !== "object") { + fail(`${rel(file)} must declare optionalDependencies`); + } + const expectedPackages = typescriptOptionalRuntimePackages(); + const expectedKeys = new Set(expectedPackages); + const actualKeys = new Set(Object.keys(optional)); + if (!setsEqual(actualKeys, expectedKeys)) { + fail(`${rel(file)} optionalDependencies must be exactly ${expectedPackages.join(", ")}`); + } + const expectedVersions = await expectedTypescriptOptionalRuntimeVersions(); + let changed = false; + const details = []; + for (const packageName of expectedPackages) { + const expectedVersion = expectedVersions[packageName]; + const actual = optional[packageName]; + if (actual !== expectedVersion) { + optional[packageName] = expectedVersion; + changed = true; + details.push(`${packageName} ${JSON.stringify(actual)} -> ${JSON.stringify(expectedVersion)}`); + } + } + if (changed) { + writeTextIfChanged(file, jsonText(data), changes, details.join("; "), { write }); + } +} + +async function syncPnpmTypescriptOptionalRuntimeSpecifiers(changes, { write }) { + const expectedVersions = await expectedTypescriptOptionalRuntimeVersions(); + const lines = readText(PNPM_LOCKFILE).split(/(?<=\n)/u); + const expectedPackages = new Set(typescriptOptionalRuntimePackages()); + const seen = new Set(); + const fileChanges = []; + + for (const [index, line] of lines.entries()) { + const [body] = stripNewline(line); + const packageMatch = PNPM_TYPESCRIPT_OPTIONAL_RUNTIME_KEY_RE.exec(body); + if (!packageMatch) { + continue; + } + const packageName = packageMatch[2]; + if (!expectedPackages.has(packageName)) { + fail(`${rel(PNPM_LOCKFILE)} contains unexpected TypeScript optional runtime package ${packageName}`); + } + seen.add(packageName); + const packageIndent = packageMatch[1].length; + const expectedVersion = expectedVersions[packageName]; + + let found = false; + for (let specifierIndex = index + 1; specifierIndex < lines.length; specifierIndex += 1) { + const [specifierBody, specifierNewline] = stripNewline(lines[specifierIndex]); + if (specifierBody.trim()) { + const specifierIndent = specifierBody.length - specifierBody.trimStart().length; + if (specifierIndent <= packageIndent) { + break; + } + } + const specifierMatch = PNPM_SPECIFIER_RE.exec(specifierBody); + if (!specifierMatch) { + continue; + } + found = true; + const actual = specifierMatch[2]; + if (actual !== expectedVersion) { + lines[specifierIndex] = `${specifierMatch[1]}${expectedVersion}${specifierMatch[3]}${specifierNewline}`; + fileChanges.push(`${packageName} ${JSON.stringify(actual)} -> ${JSON.stringify(expectedVersion)}`); + } + break; + } + if (!found) { + fail(`${rel(PNPM_LOCKFILE)} is missing a specifier for ${packageName}`); + } + } + + const missing = [...expectedPackages].filter((name) => !seen.has(name)).sort(compareText); + if (missing.length > 0) { + fail(`${rel(PNPM_LOCKFILE)} is missing TypeScript optional runtime package specifiers: ${missing.join(", ")}`); + } + if (fileChanges.length > 0) { + writeTextIfChanged(PNPM_LOCKFILE, lines.join(""), changes, fileChanges.join("; "), { write }); + } +} + +function cargoManifestPaths() { + const ignoredRoots = new Set([".git", "target", "node_modules"]); + const manifests = []; + function walk(directory) { + for (const entry of readdirSync(directory, { withFileTypes: true })) { + const file = path.join(directory, entry.name); + const relativeParts = rel(file).split("/"); + if (relativeParts.some((part) => ignoredRoots.has(part))) { + continue; + } + if (entry.isDirectory()) { + walk(file); + } else if (entry.isFile() && entry.name === "Cargo.toml") { + manifests.push(file); + } + } + } + walk(ROOT); + return manifests.sort(compareText); +} + +function localCargoPackagesByManifest() { + const packages = new Map(); + for (const manifest of cargoManifestPaths()) { + const data = Bun.TOML.parse(readText(manifest)); + const packageConfig = data.package; + if (packageConfig === null || Array.isArray(packageConfig) || typeof packageConfig !== "object") { + continue; + } + const name = packageConfig.name; + const version = packageConfig.version; + if (typeof name !== "string" || typeof version !== "string") { + continue; + } + packages.set(realpathSync(manifest), [name, version]); + } + return packages; +} + +function localCargoPackageVersions() { + const versions = new Map(); + for (const [manifest, [name, version]] of localCargoPackagesByManifest()) { + const existing = versions.get(name); + if (existing !== undefined && existing !== version) { + fail(`local Cargo package ${name} has conflicting versions including ${rel(manifest)}`); + } + versions.set(name, version); + } + return versions; +} + +function iterDependencyTables(manifest) { + const tables = []; + for (const tableName of DEPENDENCY_TABLES) { + const table = manifest[tableName]; + if (table !== null && !Array.isArray(table) && typeof table === "object") { + tables.push(table); + } + } + const targets = manifest.target; + if (targets !== null && !Array.isArray(targets) && typeof targets === "object") { + for (const target of Object.values(targets)) { + if (target === null || Array.isArray(target) || typeof target !== "object") { + continue; + } + for (const tableName of DEPENDENCY_TABLES) { + const table = target[tableName]; + if (table !== null && !Array.isArray(table) && typeof table === "object") { + tables.push(table); + } + } + } + } + return tables; +} + +function desiredCargoPathDependencyVersions(manifestPath, localPackages) { + const manifest = Bun.TOML.parse(readText(manifestPath)); + const desired = new Map(); + for (const table of iterDependencyTables(manifest)) { + for (const [dependencyName, dependency] of Object.entries(table)) { + if (dependency === null || Array.isArray(dependency) || typeof dependency !== "object") { + continue; + } + const pathValue = dependency.path; + const versionValue = dependency.version; + if (typeof pathValue !== "string" || typeof versionValue !== "string") { + continue; + } + const dependencyManifest = path.resolve(path.dirname(manifestPath), pathValue, "Cargo.toml"); + const packageInfo = localPackages.get(realpathIfExists(dependencyManifest)); + if (packageInfo === undefined) { + continue; + } + const packageVersion = packageInfo[1]; + desired.set(dependencyName, versionValue.startsWith("=") ? `=${packageVersion}` : packageVersion); + } + } + return desired; +} + +function syncCargoPathDependencyPins(changes, { write }) { + const localPackages = localCargoPackagesByManifest(); + for (const manifestPath of cargoManifestPaths()) { + const desired = desiredCargoPathDependencyVersions(manifestPath, localPackages); + if (desired.size === 0) { + continue; + } + const lines = readText(manifestPath).split(/(?<=\n)/u); + const seen = new Set(); + const fileChanges = []; + for (const [index, line] of lines.entries()) { + const [body, newline] = stripNewline(line); + for (const [dependencyName, expected] of desired) { + const pattern = new RegExp(`^(\\s*${escapeRegExp(dependencyName)}\\s*=\\s*\\{[^}]*\\bversion\\s*=\\s*")([^"]+)(".*)$`, "u"); + const match = pattern.exec(body); + if (!match) { + continue; + } + seen.add(dependencyName); + const actual = match[2]; + if (actual !== expected) { + lines[index] = `${match[1]}${expected}${match[3]}${newline}`; + fileChanges.push(`${dependencyName} ${JSON.stringify(actual)} -> ${JSON.stringify(expected)}`); + } + } + } + const missing = [...desired.keys()].filter((name) => !seen.has(name)).sort(compareText); + if (missing.length > 0) { + fail(`${rel(manifestPath)} has non-inline local path dependency pins: ${missing.join(", ")}`); + } + if (fileChanges.length > 0) { + writeTextIfChanged(manifestPath, lines.join(""), changes, fileChanges.join("; "), { write }); + } + } +} + +function stringKey(line, key) { + const [body] = stripNewline(line); + const match = STRING_KEY_RE.exec(body); + return match?.[1] === key ? match[2] : undefined; +} + +function packageBlockRanges(lines) { + const starts = lines.flatMap((line, index) => (PACKAGE_START_RE.test(line) ? [index] : [])); + return starts.map((start, index) => [start, index + 1 < starts.length ? starts[index + 1] : lines.length]); +} + +function replaceVersionLine(line, version) { + const [body, newline] = stripNewline(line); + const match = VERSION_LINE_RE.exec(body); + if (!match) { + fail(`cannot update Cargo.lock version line: ${line.trimEnd()}`); + } + return `${match[1]}"${version}"${match[2]}${newline}`; +} + +function syncLockfile(lockfile, versions, changes, { write }) { + const data = Bun.TOML.parse(readText(lockfile)); + if (!Array.isArray(data.package)) { + fail(`${rel(lockfile)} is missing [[package]] entries`); + } + const lines = readText(lockfile).split(/(?<=\n)/u); + const fileChanges = []; + for (const [start, end] of packageBlockRanges(lines)) { + const block = lines.slice(start, end); + let name; + let versionIndex; + let currentVersion; + let hasSource = false; + for (const [offset, line] of block.entries()) { + if (stringKey(line, "source") !== undefined) { + hasSource = true; + } + const keyName = stringKey(line, "name"); + if (keyName !== undefined) { + name = keyName; + } + const keyVersion = stringKey(line, "version"); + if (keyVersion !== undefined) { + versionIndex = start + offset; + currentVersion = keyVersion; + } + } + if (!versions.has(name) || hasSource) { + continue; + } + if (versionIndex === undefined || currentVersion === undefined) { + fail(`${rel(lockfile)} package ${name} is missing version`); + } + const expectedVersion = versions.get(name); + if (currentVersion !== expectedVersion) { + lines[versionIndex] = replaceVersionLine(lines[versionIndex], expectedVersion); + fileChanges.push(`${name} ${currentVersion} -> ${expectedVersion}`); + } + } + if (fileChanges.length > 0) { + writeTextIfChanged(lockfile, lines.join(""), changes, fileChanges.join("; "), { write }); + } +} + +function syncLockfiles(changes, { write }) { + const versions = localCargoPackageVersions(); + for (const lockfile of LOCKFILES) { + syncLockfile(lockfile, versions, changes, { write }); + } +} + +function commandOutputForError(result) { + const parts = [result.stdout, result.stderr] + .map((value) => String(value ?? "").trim()) + .filter(Boolean); + return parts.join("\n") || `exit ${result.status}`; +} + +function syncAssetInputFingerprint(changes, { write }) { + const command = ["run", "-p", "xtask", "--", "assets", "input-fingerprint"]; + if (write) { + command.push("--write"); + } + const before = readOptionalText(ASSET_INPUT_FINGERPRINT_PATH); + const result = spawnSync("cargo", command, { + cwd: ROOT, + encoding: "utf8", + }); + const output = commandOutputForError(result); + if (result.status !== 0) { + const mismatch = ASSET_INPUT_FINGERPRINT_MISMATCH_RE.exec(output); + if (!write && mismatch !== null) { + changes.push({ + path: ASSET_INPUT_FINGERPRINT_PATH, + detail: `${mismatch[1]} -> ${mismatch[2]}`, + }); + return; + } + fail(`\`cargo ${command.join(" ")}\` failed:\n${output}`); + } + if (!write) { + return; + } + const after = readOptionalText(ASSET_INPUT_FINGERPRINT_PATH); + if (before !== after) { + changes.push({ + path: ASSET_INPUT_FINGERPRINT_PATH, + detail: `${before?.trim() ?? ""} -> ${after?.trim() ?? ""}`, + }); + } +} + +function syncExtensionEvidence(changes, { write }) { + const command = ["src/extensions/tools/check-extension-model.py", write ? "--write-evidence" : "--check"]; + const before = Object.fromEntries(EXTENSION_EVIDENCE_PATHS.map((file) => [file, readOptionalText(file)])); + const result = spawnSync("python3", command, { + cwd: ROOT, + encoding: "utf8", + }); + const output = commandOutputForError(result); + if (result.status !== 0) { + const stale = [...output.matchAll(EXTENSION_EVIDENCE_STALE_RE)]; + if (!write && stale.length > 0) { + for (const match of stale) { + changes.push({ + path: path.join(ROOT, match[1]), + detail: `${match[3]} -> ${match[2]}`, + }); + } + return; + } + fail(`\`python3 ${command.join(" ")}\` failed:\n${output}`); + } + if (!write) { + return; + } + for (const file of EXTENSION_EVIDENCE_PATHS) { + if (before[file] !== readOptionalText(file)) { + changes.push({ path: file, detail: "regenerated extension evidence" }); + } + } +} + +function parseArgs(argv) { + const args = { check: false }; + for (const arg of argv) { + if (arg === "--check") { + args.check = true; + } else if (arg === "--help" || arg === "-h") { + console.log("usage: tools/release/sync-release-pr.mjs [--check]"); + process.exit(0); + } else { + fail(`unknown argument ${arg}`); + } + } + return args; +} + +async function main(argv) { + const args = parseArgs(argv); + const changes = []; + const write = !args.check; + await syncCompatibilityVersions(changes, { write }); + syncExtensionMavenRegistryMetadata(changes, { write }); + await syncTypescriptOptionalRuntimeDependencies(changes, { write }); + await syncPnpmTypescriptOptionalRuntimeSpecifiers(changes, { write }); + syncCargoPathDependencyPins(changes, { write }); + syncLockfiles(changes, { write }); + syncAssetInputFingerprint(changes, { write }); + syncExtensionEvidence(changes, { write }); + + if (changes.length === 0) { + console.log("release PR derived files are in sync"); + return; + } + for (const change of changes) { + console.error(`${rel(change.path)}: ${change.detail}`); + } + if (args.check) { + console.error("release PR derived files are stale; run `tools/release/sync-release-pr.mjs`"); + process.exit(1); + } + console.log("updated release PR derived files"); +} + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); +} + +function arraysEqual(left, right) { + return left.length === right.length && left.every((value, index) => value === right[index]); +} + +function setsEqual(left, right) { + return left.size === right.size && [...left].every((value) => right.has(value)); +} + +function realpathIfExists(file) { + try { + return realpathSync(file); + } catch { + return file; + } +} + +await main(Bun.argv.slice(2)); diff --git a/tools/release/sync_release_pr.py b/tools/release/sync_release_pr.py deleted file mode 100755 index ce74f5ea..00000000 --- a/tools/release/sync_release_pr.py +++ /dev/null @@ -1,638 +0,0 @@ -#!/usr/bin/env python3 -"""Synchronize release-derived files after release-please updates a PR.""" - -from __future__ import annotations - -import argparse -import json -import re -import subprocess -import sys -import tomllib -from dataclasses import dataclass -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] -DEPENDENCY_TABLES = ("dependencies", "dev-dependencies", "build-dependencies") -LOCKFILES = [ - ROOT / "Cargo.lock", - ROOT / "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock", -] -PNPM_LOCKFILE = ROOT / "pnpm-lock.yaml" -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*(?:#.*)?)$') -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|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" -ASSET_INPUT_FINGERPRINT_MISMATCH_RE = re.compile( - r"committed asset input fingerprint must be '([0-9a-f]+)', got '([0-9a-f]+)'" -) -EXTENSION_EVIDENCE_PATHS = [ - ROOT / "src/extensions/evidence/matrix.toml", - ROOT / "src/extensions/evidence/runs/2026-06-07-transitional-catalog-smoke.json", - ROOT / "src/extensions/generated/docs/extension-evidence.json", -] -EXTENSION_EVIDENCE_STALE_RE = re.compile( - r"([^:\n]+\.json) sourceDigest is stale; expected (sha256:[0-9a-f]{64}), got '([^']*)'" -) - - -@dataclass(frozen=True) -class Change: - path: Path - detail: str - - -def fail(message: str) -> NoReturn: - print(f"sync_release_pr.py: {message}", file=sys.stderr) - raise SystemExit(2) - - -def rel(path: Path) -> str: - return path.relative_to(ROOT).as_posix() - - -def read_json_object(path: Path) -> dict[str, Any]: - value = json.loads(path.read_text(encoding="utf-8")) - if not isinstance(value, dict): - fail(f"{rel(path)} must contain a JSON object") - return value - - -def json_text(value: dict[str, Any]) -> str: - return json.dumps(value, indent=2) + "\n" - - -def write_text_if_changed(path: Path, text: str, changes: list[Change], detail: str, *, write: bool) -> None: - before = path.read_text(encoding="utf-8") - if before == text: - return - changes.append(Change(path, detail)) - if write: - path.write_text(text, encoding="utf-8") - - -def set_json_path(data: dict[str, Any], dotted: str, expected: str, context: str) -> str | None: - current: Any = data - parts = dotted.split(".") - for part in parts[:-1]: - if not isinstance(current, dict) or not isinstance(current.get(part), dict): - fail(f"{context} is missing object path {'.'.join(parts[:-1])}") - current = current[part] - if not isinstance(current, dict): - fail(f"{context} is missing object path {'.'.join(parts[:-1])}") - key = parts[-1] - actual = current.get(key) - if actual == expected: - return None - current[key] = expected - return f"{context} {actual!r} -> {expected!r}" - - -def set_toml_string_path(path: Path, dotted: str, expected: str, context: str) -> tuple[str | None, str | None]: - parts = dotted.split(".") - if len(parts) < 2: - fail(f"{context} TOML parser must use table.key dotted syntax") - table = parts[:-1] - key = parts[-1] - lines = path.read_text(encoding="utf-8").splitlines(keepends=True) - current_table: list[str] = [] - saw_table = False - key_pattern = re.compile(rf'^(\s*{re.escape(key)}\s*=\s*)"([^"]*)"(.*)$') - - for index, line in enumerate(lines): - body, newline = strip_newline(line) - table_match = TOML_TABLE_RE.match(body) - if table_match: - current_table = table_match.group(1).split(".") - saw_table = current_table == table - continue - if current_table != table: - continue - key_match = key_pattern.match(body) - if key_match is None: - continue - actual = key_match.group(2) - if actual == expected: - return None, None - lines[index] = f'{key_match.group(1)}"{expected}"{key_match.group(3)}{newline}' - return "".join(lines), f"{context} {actual!r} -> {expected!r}" - - if saw_table: - fail(f"{context} did not find TOML key {key!r} in {rel(path)}") - fail(f"{context} did not find TOML table {'.'.join(table)!r} in {rel(path)}") - - -def set_rust_const_string(path: Path, const_name: str, expected: str, context: str) -> tuple[str | None, str | None]: - lines = path.read_text(encoding="utf-8").splitlines(keepends=True) - pattern = re.compile(rf'^(\s*(?:pub\s+)?const\s+{re.escape(const_name)}\s*:\s*&str\s*=\s*)"([^"]*)"(;.*)$') - for index, line in enumerate(lines): - body, newline = strip_newline(line) - match = pattern.match(body) - if match is None: - continue - actual = match.group(2) - if actual == expected: - return None, None - lines[index] = f'{match.group(1)}"{expected}"{match.group(3)}{newline}' - return "".join(lines), f"{context} {actual!r} -> {expected!r}" - 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 - expected = product_metadata.read_current_version(source_product) - if parser == "raw": - write_text_if_changed( - path, - expected + "\n", - changes, - f"{spec_id} -> {source_product} {expected}", - write=write, - ) - continue - if parser.startswith("json:"): - data = read_json_object(path) - detail = set_json_path(data, parser.split(":", 1)[1], expected, spec_id) - if detail is not None: - write_text_if_changed(path, json_text(data), changes, detail, write=write) - continue - if parser.startswith("toml:"): - text, detail = set_toml_string_path(path, parser.split(":", 1)[1], expected, spec_id) - if text is not None and detail is not None: - write_text_if_changed(path, text, changes, detail, write=write) - continue - if parser.startswith("rust-const:"): - text, detail = set_rust_const_string(path, parser.split(":", 1)[1], expected, spec_id) - if text is not None and detail is not None: - write_text_if_changed(path, text, changes, detail, write=write) - continue - fail(f"{spec_id} uses unsupported sync parser {parser!r}") - - -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 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_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(expected_packages)}" - ) - - expected_versions = expected_typescript_optional_runtime_versions() - changed = False - details = [] - for package_name in expected_packages: - expected_version = expected_versions[package_name] - actual = optional.get(package_name) - if actual != expected_version: - optional[package_name] = expected_version - changed = True - details.append(f"{package_name} {actual!r} -> {expected_version!r}") - if changed: - write_text_if_changed(path, json_text(data), changes, "; ".join(details), write=write) - - -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()) - seen: set[str] = set() - file_changes: list[str] = [] - - for index, line in enumerate(lines): - body, _ = strip_newline(line) - package_match = PNPM_TYPESCRIPT_OPTIONAL_RUNTIME_KEY_RE.match(body) - if package_match is None: - continue - package_name = package_match.group(2) - if package_name not in expected_packages: - fail(f"{rel(PNPM_LOCKFILE)} contains unexpected TypeScript optional runtime package {package_name}") - seen.add(package_name) - package_indent = len(package_match.group(1)) - expected_version = expected_versions[package_name] - - for specifier_index in range(index + 1, len(lines)): - specifier_body, specifier_newline = strip_newline(lines[specifier_index]) - if specifier_body.strip(): - specifier_indent = len(specifier_body) - len(specifier_body.lstrip(" ")) - if specifier_indent <= package_indent: - break - specifier_match = PNPM_SPECIFIER_RE.match(specifier_body) - if specifier_match is None: - continue - actual = specifier_match.group(2) - if actual != expected_version: - lines[specifier_index] = ( - f"{specifier_match.group(1)}{expected_version}" - f"{specifier_match.group(3)}{specifier_newline}" - ) - file_changes.append(f"{package_name} {actual!r} -> {expected_version!r}") - break - else: - fail(f"{rel(PNPM_LOCKFILE)} is missing a specifier for {package_name}") - - missing = expected_packages - seen - if missing: - fail( - f"{rel(PNPM_LOCKFILE)} is missing TypeScript optional runtime package specifiers: " - f"{', '.join(sorted(missing))}" - ) - if file_changes: - write_text_if_changed(PNPM_LOCKFILE, "".join(lines), changes, "; ".join(file_changes), write=write) - - -def cargo_manifest_name_version(path: Path) -> tuple[str, str]: - data = tomllib.loads(path.read_text(encoding="utf-8")) - package = data.get("package") - if not isinstance(package, dict): - fail(f"{rel(path)} is missing [package]") - name = package.get("name") - version = package.get("version") - if not isinstance(name, str) or not name: - fail(f"{rel(path)} is missing package.name") - if not isinstance(version, str) or not version: - fail(f"{rel(path)} is missing package.version") - return name, version - - -def cargo_manifest_paths() -> list[Path]: - ignored_roots = {".git", "target", "node_modules"} - return sorted( - path - for path in ROOT.rglob("Cargo.toml") - if not any(part in ignored_roots for part in path.relative_to(ROOT).parts) - ) - - -def local_cargo_packages_by_manifest() -> dict[Path, tuple[str, str]]: - packages = {} - for manifest in cargo_manifest_paths(): - data = tomllib.loads(manifest.read_text(encoding="utf-8")) - package = data.get("package") - if not isinstance(package, dict): - continue - name = package.get("name") - version = package.get("version") - if not isinstance(name, str) or not isinstance(version, str): - continue - packages[manifest.resolve()] = (name, version) - return packages - - -def local_cargo_package_versions() -> dict[str, str]: - versions: dict[str, str] = {} - for manifest, (name, version) in local_cargo_packages_by_manifest().items(): - existing = versions.get(name) - if existing is not None and existing != version: - fail(f"local Cargo package {name} has conflicting versions including {rel(manifest)}") - 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 iter_dependency_tables(manifest: dict[str, Any]) -> list[dict[str, Any]]: - tables = [] - for table_name in DEPENDENCY_TABLES: - table = manifest.get(table_name) - if isinstance(table, dict): - tables.append(table) - targets = manifest.get("target") - if isinstance(targets, dict): - for target in targets.values(): - if not isinstance(target, dict): - continue - for table_name in DEPENDENCY_TABLES: - table = target.get(table_name) - if isinstance(table, dict): - tables.append(table) - return tables - - -def desired_cargo_path_dependency_versions( - manifest_path: Path, - local_packages: dict[Path, tuple[str, str]], -) -> dict[str, str]: - manifest = tomllib.loads(manifest_path.read_text(encoding="utf-8")) - desired: dict[str, str] = {} - for table in iter_dependency_tables(manifest): - for dependency_name, dependency in table.items(): - if not isinstance(dependency, dict): - continue - path_value = dependency.get("path") - version_value = dependency.get("version") - if not isinstance(path_value, str) or not isinstance(version_value, str): - continue - dependency_manifest = (manifest_path.parent / path_value / "Cargo.toml").resolve() - package = local_packages.get(dependency_manifest) - if package is None: - continue - _, package_version = package - desired[dependency_name] = f"={package_version}" if version_value.startswith("=") else package_version - return desired - - -def sync_cargo_path_dependency_pins(changes: list[Change], *, write: bool) -> None: - local_packages = local_cargo_packages_by_manifest() - for manifest_path in cargo_manifest_paths(): - desired = desired_cargo_path_dependency_versions(manifest_path, local_packages) - if not desired: - continue - lines = manifest_path.read_text(encoding="utf-8").splitlines(keepends=True) - seen: set[str] = set() - file_changes: list[str] = [] - - for index, line in enumerate(lines): - body, newline = strip_newline(line) - for dependency_name, expected in desired.items(): - pattern = re.compile( - rf'^(\s*{re.escape(dependency_name)}\s*=\s*\{{[^}}]*\bversion\s*=\s*")([^"]+)(".*)$' - ) - match = pattern.match(body) - if match is None: - continue - seen.add(dependency_name) - actual = match.group(2) - if actual != expected: - lines[index] = f"{match.group(1)}{expected}{match.group(3)}{newline}" - file_changes.append(f"{dependency_name} {actual!r} -> {expected!r}") - - missing = sorted(set(desired) - seen) - if missing: - fail(f"{rel(manifest_path)} has non-inline local path dependency pins: {', '.join(missing)}") - if file_changes: - write_text_if_changed( - manifest_path, - "".join(lines), - changes, - "; ".join(file_changes), - write=write, - ) - - -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 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 replace_version_line(line: str, version: str) -> str: - body, newline = strip_newline(line) - match = VERSION_LINE_RE.match(body) - if not match: - fail(f"cannot update Cargo.lock version line: {line.rstrip()}") - return f'{match.group(1)}"{version}"{match.group(2)}{newline}' - - -def sync_lockfile(lockfile: Path, versions: dict[str, str], changes: list[Change], *, write: bool) -> None: - data = tomllib.loads(lockfile.read_text(encoding="utf-8")) - packages = data.get("package") - if not isinstance(packages, list): - fail(f"{rel(lockfile)} is missing [[package]] entries") - lines = lockfile.read_text(encoding="utf-8").splitlines(keepends=True) - file_changes: list[str] = [] - - 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: - fail(f"{rel(lockfile)} package {name} is missing version") - - expected_version = versions[name] - if current_version != expected_version: - lines[version_idx] = replace_version_line(lines[version_idx], expected_version) - file_changes.append(f"{name} {current_version} -> {expected_version}") - - if file_changes: - write_text_if_changed(lockfile, "".join(lines), changes, "; ".join(file_changes), write=write) - - -def sync_lockfiles(changes: list[Change], *, write: bool) -> None: - versions = local_cargo_package_versions() - for lockfile in LOCKFILES: - sync_lockfile(lockfile, versions, changes, write=write) - - -def read_optional_text(path: Path) -> str | None: - if not path.exists(): - return None - return path.read_text(encoding="utf-8") - - -def command_output_for_error(result: subprocess.CompletedProcess[str]) -> str: - parts = [part.strip() for part in (result.stdout, result.stderr) if part.strip()] - return "\n".join(parts) or f"exit {result.returncode}" - - -def sync_asset_input_fingerprint(changes: list[Change], *, write: bool) -> None: - command = ["cargo", "run", "-p", "xtask", "--", "assets", "input-fingerprint"] - if write: - command.append("--write") - - before = read_optional_text(ASSET_INPUT_FINGERPRINT_PATH) - result = subprocess.run(command, cwd=ROOT, text=True, capture_output=True, check=False) - output = command_output_for_error(result) - - if result.returncode != 0: - mismatch = ASSET_INPUT_FINGERPRINT_MISMATCH_RE.search(output) - if not write and mismatch is not None: - changes.append( - Change( - ASSET_INPUT_FINGERPRINT_PATH, - f"{mismatch.group(1)} -> {mismatch.group(2)}", - ) - ) - return - fail(f"`{' '.join(command)}` failed:\n{output}") - - if not write: - return - - after = read_optional_text(ASSET_INPUT_FINGERPRINT_PATH) - if before != after: - old = before.strip() if before is not None else "" - new = after.strip() if after is not None else "" - changes.append(Change(ASSET_INPUT_FINGERPRINT_PATH, f"{old} -> {new}")) - - -def sync_extension_evidence(changes: list[Change], *, write: bool) -> None: - command = ["python3", "src/extensions/tools/check-extension-model.py"] - command.append("--write-evidence" if write else "--check") - before = {path: read_optional_text(path) for path in EXTENSION_EVIDENCE_PATHS} - result = subprocess.run(command, cwd=ROOT, text=True, capture_output=True, check=False) - output = command_output_for_error(result) - - if result.returncode != 0: - stale = EXTENSION_EVIDENCE_STALE_RE.findall(output) - if not write and stale: - for path_text, expected, actual in stale: - changes.append(Change(ROOT / path_text, f"{actual} -> {expected}")) - return - fail(f"`{' '.join(command)}` failed:\n{output}") - - if not write: - return - - for path in EXTENSION_EVIDENCE_PATHS: - if before[path] != read_optional_text(path): - changes.append(Change(path, "regenerated extension evidence")) - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--check", action="store_true", help="fail instead of writing updates") - args = parser.parse_args() - - 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) - sync_lockfiles(changes, write=write) - sync_asset_input_fingerprint(changes, write=write) - sync_extension_evidence(changes, write=write) - - if not changes: - print("release PR derived files are in sync") - return 0 - - for change in changes: - print(f"{rel(change.path)}: {change.detail}", file=sys.stderr) - if args.check: - print("release PR derived files are stale; run `tools/release/sync_release_pr.py`", file=sys.stderr) - return 1 - print("updated release PR derived files") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) From 4195b0de3f93b5d431761484d8dd32b70dc17279 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 02:45:08 +0000 Subject: [PATCH 142/308] chore: port extension artifact staging to bun --- docs/internal/IMPLEMENTATION_CHECKLIST.md | 10 +- src/extensions/artifacts/packages/moon.yml | 18 +- .../tools/package-mobile-release-assets.sh | 2 +- src/extensions/contrib/amcheck/moon.yml | 8 +- src/extensions/contrib/auto_explain/moon.yml | 8 +- src/extensions/contrib/bloom/moon.yml | 8 +- src/extensions/contrib/btree_gin/moon.yml | 8 +- src/extensions/contrib/btree_gist/moon.yml | 8 +- src/extensions/contrib/citext/moon.yml | 8 +- src/extensions/contrib/cube/moon.yml | 8 +- src/extensions/contrib/dict_int/moon.yml | 8 +- src/extensions/contrib/dict_xsyn/moon.yml | 8 +- src/extensions/contrib/earthdistance/moon.yml | 8 +- src/extensions/contrib/file_fdw/moon.yml | 8 +- src/extensions/contrib/fuzzystrmatch/moon.yml | 8 +- src/extensions/contrib/hstore/moon.yml | 8 +- src/extensions/contrib/intarray/moon.yml | 8 +- src/extensions/contrib/isn/moon.yml | 8 +- src/extensions/contrib/lo/moon.yml | 8 +- src/extensions/contrib/ltree/moon.yml | 8 +- src/extensions/contrib/pageinspect/moon.yml | 8 +- .../contrib/pg_buffercache/moon.yml | 8 +- .../contrib/pg_freespacemap/moon.yml | 8 +- src/extensions/contrib/pg_surgery/moon.yml | 8 +- src/extensions/contrib/pg_trgm/moon.yml | 8 +- src/extensions/contrib/pg_visibility/moon.yml | 8 +- src/extensions/contrib/pg_walinspect/moon.yml | 8 +- src/extensions/contrib/pgcrypto/moon.yml | 8 +- src/extensions/contrib/seg/moon.yml | 8 +- src/extensions/contrib/tablefunc/moon.yml | 8 +- src/extensions/contrib/tcn/moon.yml | 8 +- .../contrib/tsm_system_rows/moon.yml | 8 +- .../contrib/tsm_system_time/moon.yml | 8 +- src/extensions/contrib/unaccent/moon.yml | 8 +- src/extensions/contrib/uuid_ossp/moon.yml | 8 +- ...2026-06-07-transitional-catalog-smoke.json | 2 +- src/extensions/external/pg_hashids/moon.yml | 8 +- src/extensions/external/pg_ivm/moon.yml | 8 +- .../external/pg_textsearch/moon.yml | 8 +- src/extensions/external/pg_uuidv7/moon.yml | 8 +- src/extensions/external/pgtap/moon.yml | 8 +- src/extensions/external/postgis/moon.yml | 8 +- src/extensions/external/vector/moon.yml | 8 +- .../generated/docs/extension-evidence.json | 80 +- .../assets/generated/asset-inputs.sha256 | 2 +- tools/graph/moon.yml | 4 +- tools/policy/check-moon-product-graph.mjs | 4 +- tools/policy/check-release-policy.py | 2 +- tools/policy/python-entrypoints.allowlist | 1 - .../release/build-extension-ci-artifacts.mjs | 798 ++++++++++++++++++ tools/release/build-extension-ci-artifacts.py | 531 ------------ tools/release/check_artifact_targets.py | 30 +- 52 files changed, 1030 insertions(+), 766 deletions(-) create mode 100644 tools/release/build-extension-ci-artifacts.mjs delete mode 100755 tools/release/build-extension-ci-artifacts.py diff --git a/docs/internal/IMPLEMENTATION_CHECKLIST.md b/docs/internal/IMPLEMENTATION_CHECKLIST.md index 58c33a78..bead8288 100644 --- a/docs/internal/IMPLEMENTATION_CHECKLIST.md +++ b/docs/internal/IMPLEMENTATION_CHECKLIST.md @@ -392,21 +392,21 @@ or CI/build output proves the contract. a stale `target/extensions/native/release-assets/test-mobile` directory no longer creates duplicate vector package rows. - [x] Exact-extension package assembly has no broad native-index fallback. - Evidence: `tools/release/build-extension-ci-artifacts.py` now requires + Evidence: `tools/release/build-extension-ci-artifacts.mjs` now requires product-scoped target indexes from `target/extensions/native/release-assets///...` and fails when required target artifacts are missing. - [x] Mobile exact-extension package assembly filters to the requested mobile native targets instead of carrying every downloaded desktop/native artifact into mobile build handoff artifacts. Evidence: - `python3 tools/release/build-extension-ci-artifacts.py + `tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-vector --output-root target/extension-artifacts-mobile-validate --require-native-target android-x86_64 --require-native-target ios-xcframework` stages only `android-x86_64` and `ios-xcframework` vector assets. - [x] Exact-extension release packages emit JSON manifest, ecosystem-friendly `.properties` manifest, and checksum manifest. Evidence: - `tools/release/build-extension-ci-artifacts.py oliphaunt-extension-vector + `tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-vector --output-root target/extension-artifacts-test` staged `oliphaunt-extension-vector-0.1.0-manifest.properties` and `oliphaunt-extension-vector-0.1.0-release-assets.sha256`. @@ -541,7 +541,7 @@ Run before claiming this architecture complete: - [x] `bash -n tools/release/build-sdk-ci-artifacts.sh src/sdks/swift/tools/check-sdk.sh` - [x] `python3 -m py_compile tools/release/release.py - tools/release/build-extension-ci-artifacts.py + tools/release/build-extension-ci-artifacts.mjs tools/release/check_artifact_targets.py tools/release/check_release_metadata.py` - [x] `tools/dev/bun.sh tools/graph/ci_plan.mjs --help` @@ -688,7 +688,7 @@ Run before claiming this architecture complete: NDK `27.0.12077973`, CMake `3.22.1`, and compile SDK `36`. - [x] `bash src/sdks/kotlin/tools/check-sdk.sh check-static` - [x] `bash src/runtimes/node-direct/tools/build-node-addon.sh` -- [x] `python3 tools/release/build-extension-ci-artifacts.py +- [x] `tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-vector --output-root target/extension-artifacts-validate --require-native-target android-x86_64 --require-native-target ios-xcframework` diff --git a/src/extensions/artifacts/packages/moon.yml b/src/extensions/artifacts/packages/moon.yml index f089b89d..1504c3c1 100644 --- a/src/extensions/artifacts/packages/moon.yml +++ b/src/extensions/artifacts/packages/moon.yml @@ -1,7 +1,7 @@ $schema: "https://moonrepo.dev/schemas/project.json" id: "extension-packages" -language: "python" +language: "javascript" layer: "tool" stack: "systems" tags: ["extensions", "artifacts", "release"] @@ -32,10 +32,9 @@ tasks: - "!/src/extensions/evidence/**" - "/src/runtimes/liboliphaunt/native/moon.yml" - "/src/shared/extension-runtime-contract/**/*" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/artifact_targets.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" outputs: - "/target/extension-artifacts/**/*" @@ -46,7 +45,7 @@ tasks: assemble-release: tags: ["release", "artifact-package", "ci-extension-packages"] - command: "python3 tools/release/build-extension-ci-artifacts.py --all --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs --all --require-native --require-wasix" inputs: - "/.release-please-manifest.json" - "/release-please-config.json" @@ -58,10 +57,9 @@ tasks: - "/src/runtimes/liboliphaunt/native/moon.yml" - "/src/runtimes/liboliphaunt/wasix/moon.yml" - "/src/shared/extension-runtime-contract/**/*" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/artifact_targets.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" - "/target/extensions/wasix/aot-artifacts/**/*" diff --git a/src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh b/src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh index 707eee8d..36905cae 100755 --- a/src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh +++ b/src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh @@ -61,5 +61,5 @@ case " ${args[*]} " in ;; esac -python3 tools/release/build-extension-ci-artifacts.py "${args[@]}" +tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs "${args[@]}" python3 tools/release/check_staged_artifacts.py "${validation_args[@]}" diff --git a/src/extensions/contrib/amcheck/moon.yml b/src/extensions/contrib/amcheck/moon.yml index ef0686ed..904cfde3 100644 --- a/src/extensions/contrib/amcheck/moon.yml +++ b/src/extensions/contrib/amcheck/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-amcheck --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-amcheck --require-native --require-wasix" deps: - "oliphaunt-extension-amcheck:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/amcheck/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/auto_explain/moon.yml b/src/extensions/contrib/auto_explain/moon.yml index 2b8406fd..b940db59 100644 --- a/src/extensions/contrib/auto_explain/moon.yml +++ b/src/extensions/contrib/auto_explain/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-auto-explain --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-auto-explain --require-native --require-wasix" deps: - "oliphaunt-extension-auto-explain:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/auto_explain/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/bloom/moon.yml b/src/extensions/contrib/bloom/moon.yml index 7a0434d3..f1c757c3 100644 --- a/src/extensions/contrib/bloom/moon.yml +++ b/src/extensions/contrib/bloom/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-bloom --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-bloom --require-native --require-wasix" deps: - "oliphaunt-extension-bloom:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/bloom/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/btree_gin/moon.yml b/src/extensions/contrib/btree_gin/moon.yml index 0c3979e3..adee35d5 100644 --- a/src/extensions/contrib/btree_gin/moon.yml +++ b/src/extensions/contrib/btree_gin/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-btree-gin --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-btree-gin --require-native --require-wasix" deps: - "oliphaunt-extension-btree-gin:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/btree_gin/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/btree_gist/moon.yml b/src/extensions/contrib/btree_gist/moon.yml index 9cb10d33..abb04a4c 100644 --- a/src/extensions/contrib/btree_gist/moon.yml +++ b/src/extensions/contrib/btree_gist/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-btree-gist --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-btree-gist --require-native --require-wasix" deps: - "oliphaunt-extension-btree-gist:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/btree_gist/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/citext/moon.yml b/src/extensions/contrib/citext/moon.yml index 3fe5ebd0..ba58a545 100644 --- a/src/extensions/contrib/citext/moon.yml +++ b/src/extensions/contrib/citext/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-citext --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-citext --require-native --require-wasix" deps: - "oliphaunt-extension-citext:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/citext/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/cube/moon.yml b/src/extensions/contrib/cube/moon.yml index f980e98e..438c2bdd 100644 --- a/src/extensions/contrib/cube/moon.yml +++ b/src/extensions/contrib/cube/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-cube --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-cube --require-native --require-wasix" deps: - "oliphaunt-extension-cube:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/cube/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/dict_int/moon.yml b/src/extensions/contrib/dict_int/moon.yml index cf4d35c0..863c77c5 100644 --- a/src/extensions/contrib/dict_int/moon.yml +++ b/src/extensions/contrib/dict_int/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-dict-int --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-dict-int --require-native --require-wasix" deps: - "oliphaunt-extension-dict-int:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/dict_int/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/dict_xsyn/moon.yml b/src/extensions/contrib/dict_xsyn/moon.yml index f0bd2e52..ccccc335 100644 --- a/src/extensions/contrib/dict_xsyn/moon.yml +++ b/src/extensions/contrib/dict_xsyn/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-dict-xsyn --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-dict-xsyn --require-native --require-wasix" deps: - "oliphaunt-extension-dict-xsyn:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/dict_xsyn/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/earthdistance/moon.yml b/src/extensions/contrib/earthdistance/moon.yml index bc940002..df26cdb4 100644 --- a/src/extensions/contrib/earthdistance/moon.yml +++ b/src/extensions/contrib/earthdistance/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-earthdistance --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-earthdistance --require-native --require-wasix" deps: - "oliphaunt-extension-earthdistance:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/earthdistance/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/file_fdw/moon.yml b/src/extensions/contrib/file_fdw/moon.yml index ce821c8f..694cb4de 100644 --- a/src/extensions/contrib/file_fdw/moon.yml +++ b/src/extensions/contrib/file_fdw/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-file-fdw --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-file-fdw --require-native --require-wasix" deps: - "oliphaunt-extension-file-fdw:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/file_fdw/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/fuzzystrmatch/moon.yml b/src/extensions/contrib/fuzzystrmatch/moon.yml index ad2c4d9b..27d30a39 100644 --- a/src/extensions/contrib/fuzzystrmatch/moon.yml +++ b/src/extensions/contrib/fuzzystrmatch/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-fuzzystrmatch --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-fuzzystrmatch --require-native --require-wasix" deps: - "oliphaunt-extension-fuzzystrmatch:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/fuzzystrmatch/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/hstore/moon.yml b/src/extensions/contrib/hstore/moon.yml index 8ab1cb14..275c5d81 100644 --- a/src/extensions/contrib/hstore/moon.yml +++ b/src/extensions/contrib/hstore/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-hstore --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-hstore --require-native --require-wasix" deps: - "oliphaunt-extension-hstore:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/hstore/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/intarray/moon.yml b/src/extensions/contrib/intarray/moon.yml index aa9fcd01..6ecd281f 100644 --- a/src/extensions/contrib/intarray/moon.yml +++ b/src/extensions/contrib/intarray/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-intarray --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-intarray --require-native --require-wasix" deps: - "oliphaunt-extension-intarray:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/intarray/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/isn/moon.yml b/src/extensions/contrib/isn/moon.yml index 630fe7f4..26726738 100644 --- a/src/extensions/contrib/isn/moon.yml +++ b/src/extensions/contrib/isn/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-isn --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-isn --require-native --require-wasix" deps: - "oliphaunt-extension-isn:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/isn/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/lo/moon.yml b/src/extensions/contrib/lo/moon.yml index 552ba3a7..90918272 100644 --- a/src/extensions/contrib/lo/moon.yml +++ b/src/extensions/contrib/lo/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-lo --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-lo --require-native --require-wasix" deps: - "oliphaunt-extension-lo:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/lo/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/ltree/moon.yml b/src/extensions/contrib/ltree/moon.yml index 15901950..16d7af0f 100644 --- a/src/extensions/contrib/ltree/moon.yml +++ b/src/extensions/contrib/ltree/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-ltree --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-ltree --require-native --require-wasix" deps: - "oliphaunt-extension-ltree:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/ltree/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/pageinspect/moon.yml b/src/extensions/contrib/pageinspect/moon.yml index 3ed2117a..cb11482c 100644 --- a/src/extensions/contrib/pageinspect/moon.yml +++ b/src/extensions/contrib/pageinspect/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-pageinspect --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-pageinspect --require-native --require-wasix" deps: - "oliphaunt-extension-pageinspect:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/pageinspect/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/pg_buffercache/moon.yml b/src/extensions/contrib/pg_buffercache/moon.yml index a361e0aa..3e76eb02 100644 --- a/src/extensions/contrib/pg_buffercache/moon.yml +++ b/src/extensions/contrib/pg_buffercache/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-pg-buffercache --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-pg-buffercache --require-native --require-wasix" deps: - "oliphaunt-extension-pg-buffercache:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/pg_buffercache/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/pg_freespacemap/moon.yml b/src/extensions/contrib/pg_freespacemap/moon.yml index 071f7456..a35b44ec 100644 --- a/src/extensions/contrib/pg_freespacemap/moon.yml +++ b/src/extensions/contrib/pg_freespacemap/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-pg-freespacemap --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-pg-freespacemap --require-native --require-wasix" deps: - "oliphaunt-extension-pg-freespacemap:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/pg_freespacemap/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/pg_surgery/moon.yml b/src/extensions/contrib/pg_surgery/moon.yml index ecda81cd..a8bcf7c7 100644 --- a/src/extensions/contrib/pg_surgery/moon.yml +++ b/src/extensions/contrib/pg_surgery/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-pg-surgery --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-pg-surgery --require-native --require-wasix" deps: - "oliphaunt-extension-pg-surgery:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/pg_surgery/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/pg_trgm/moon.yml b/src/extensions/contrib/pg_trgm/moon.yml index 74f09d15..7469c222 100644 --- a/src/extensions/contrib/pg_trgm/moon.yml +++ b/src/extensions/contrib/pg_trgm/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-pg-trgm --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-pg-trgm --require-native --require-wasix" deps: - "oliphaunt-extension-pg-trgm:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/pg_trgm/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/pg_visibility/moon.yml b/src/extensions/contrib/pg_visibility/moon.yml index 4e89ee28..40de1ce7 100644 --- a/src/extensions/contrib/pg_visibility/moon.yml +++ b/src/extensions/contrib/pg_visibility/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-pg-visibility --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-pg-visibility --require-native --require-wasix" deps: - "oliphaunt-extension-pg-visibility:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/pg_visibility/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/pg_walinspect/moon.yml b/src/extensions/contrib/pg_walinspect/moon.yml index 06cd002c..32fac31f 100644 --- a/src/extensions/contrib/pg_walinspect/moon.yml +++ b/src/extensions/contrib/pg_walinspect/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-pg-walinspect --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-pg-walinspect --require-native --require-wasix" deps: - "oliphaunt-extension-pg-walinspect:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/pg_walinspect/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/pgcrypto/moon.yml b/src/extensions/contrib/pgcrypto/moon.yml index 75cc03e2..07fe208b 100644 --- a/src/extensions/contrib/pgcrypto/moon.yml +++ b/src/extensions/contrib/pgcrypto/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-pgcrypto --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-pgcrypto --require-native --require-wasix" deps: - "oliphaunt-extension-pgcrypto:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/pgcrypto/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/seg/moon.yml b/src/extensions/contrib/seg/moon.yml index d9b6eb98..9d297db6 100644 --- a/src/extensions/contrib/seg/moon.yml +++ b/src/extensions/contrib/seg/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-seg --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-seg --require-native --require-wasix" deps: - "oliphaunt-extension-seg:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/seg/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/tablefunc/moon.yml b/src/extensions/contrib/tablefunc/moon.yml index 4f3ce7e1..03f49b21 100644 --- a/src/extensions/contrib/tablefunc/moon.yml +++ b/src/extensions/contrib/tablefunc/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-tablefunc --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-tablefunc --require-native --require-wasix" deps: - "oliphaunt-extension-tablefunc:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/tablefunc/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/tcn/moon.yml b/src/extensions/contrib/tcn/moon.yml index fa13a6a1..1d93a231 100644 --- a/src/extensions/contrib/tcn/moon.yml +++ b/src/extensions/contrib/tcn/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-tcn --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-tcn --require-native --require-wasix" deps: - "oliphaunt-extension-tcn:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/tcn/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/tsm_system_rows/moon.yml b/src/extensions/contrib/tsm_system_rows/moon.yml index 53a5cc38..787ce898 100644 --- a/src/extensions/contrib/tsm_system_rows/moon.yml +++ b/src/extensions/contrib/tsm_system_rows/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-tsm-system-rows --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-tsm-system-rows --require-native --require-wasix" deps: - "oliphaunt-extension-tsm-system-rows:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/tsm_system_rows/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/tsm_system_time/moon.yml b/src/extensions/contrib/tsm_system_time/moon.yml index eb9a8a56..82901bbc 100644 --- a/src/extensions/contrib/tsm_system_time/moon.yml +++ b/src/extensions/contrib/tsm_system_time/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-tsm-system-time --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-tsm-system-time --require-native --require-wasix" deps: - "oliphaunt-extension-tsm-system-time:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/tsm_system_time/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/unaccent/moon.yml b/src/extensions/contrib/unaccent/moon.yml index 88c87b91..01d9b33e 100644 --- a/src/extensions/contrib/unaccent/moon.yml +++ b/src/extensions/contrib/unaccent/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-unaccent --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-unaccent --require-native --require-wasix" deps: - "oliphaunt-extension-unaccent:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/unaccent/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/contrib/uuid_ossp/moon.yml b/src/extensions/contrib/uuid_ossp/moon.yml index e4f2dfb1..41d593c7 100644 --- a/src/extensions/contrib/uuid_ossp/moon.yml +++ b/src/extensions/contrib/uuid_ossp/moon.yml @@ -38,7 +38,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-uuid-ossp --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-uuid-ossp --require-native --require-wasix" deps: - "oliphaunt-extension-uuid-ossp:check" inputs: @@ -47,9 +47,9 @@ tasks: - "/src/extensions/contrib/uuid_ossp/**/*" - "/src/extensions/contrib/postgres18.toml" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: 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 04c7a770..c67f4ac0 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:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d", + "sourceDigest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4", "sourceDigestInputs": [ "src/postgres/versions/18/source.toml", "src/extensions/catalog/extensions.promoted.toml", diff --git a/src/extensions/external/pg_hashids/moon.yml b/src/extensions/external/pg_hashids/moon.yml index 0bb64bbd..485e17b3 100644 --- a/src/extensions/external/pg_hashids/moon.yml +++ b/src/extensions/external/pg_hashids/moon.yml @@ -33,7 +33,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-pg-hashids --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-pg-hashids --require-native --require-wasix" deps: - "oliphaunt-extension-pg-hashids:check" inputs: @@ -41,9 +41,9 @@ tasks: - "/release-please-config.json" - "/src/extensions/external/pg_hashids/**/*" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/external/pg_ivm/moon.yml b/src/extensions/external/pg_ivm/moon.yml index 778949bd..997a85fc 100644 --- a/src/extensions/external/pg_ivm/moon.yml +++ b/src/extensions/external/pg_ivm/moon.yml @@ -33,7 +33,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-pg-ivm --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-pg-ivm --require-native --require-wasix" deps: - "oliphaunt-extension-pg-ivm:check" inputs: @@ -41,9 +41,9 @@ tasks: - "/release-please-config.json" - "/src/extensions/external/pg_ivm/**/*" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/external/pg_textsearch/moon.yml b/src/extensions/external/pg_textsearch/moon.yml index 09036e61..9c44610c 100644 --- a/src/extensions/external/pg_textsearch/moon.yml +++ b/src/extensions/external/pg_textsearch/moon.yml @@ -33,7 +33,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-pg-textsearch --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-pg-textsearch --require-native --require-wasix" deps: - "oliphaunt-extension-pg-textsearch:check" inputs: @@ -41,9 +41,9 @@ tasks: - "/release-please-config.json" - "/src/extensions/external/pg_textsearch/**/*" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/external/pg_uuidv7/moon.yml b/src/extensions/external/pg_uuidv7/moon.yml index d30dc80a..cee6b6c4 100644 --- a/src/extensions/external/pg_uuidv7/moon.yml +++ b/src/extensions/external/pg_uuidv7/moon.yml @@ -33,7 +33,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-pg-uuidv7 --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-pg-uuidv7 --require-native --require-wasix" deps: - "oliphaunt-extension-pg-uuidv7:check" inputs: @@ -41,9 +41,9 @@ tasks: - "/release-please-config.json" - "/src/extensions/external/pg_uuidv7/**/*" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/external/pgtap/moon.yml b/src/extensions/external/pgtap/moon.yml index a406c33e..4a0326f4 100644 --- a/src/extensions/external/pgtap/moon.yml +++ b/src/extensions/external/pgtap/moon.yml @@ -33,7 +33,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-pgtap --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-pgtap --require-native --require-wasix" deps: - "oliphaunt-extension-pgtap:check" inputs: @@ -41,9 +41,9 @@ tasks: - "/release-please-config.json" - "/src/extensions/external/pgtap/**/*" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/external/postgis/moon.yml b/src/extensions/external/postgis/moon.yml index e0050ee6..cef4f1ef 100644 --- a/src/extensions/external/postgis/moon.yml +++ b/src/extensions/external/postgis/moon.yml @@ -33,7 +33,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-postgis --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-postgis --require-native --require-wasix" deps: - "oliphaunt-extension-postgis:check" inputs: @@ -41,9 +41,9 @@ tasks: - "/release-please-config.json" - "/src/extensions/external/postgis/**/*" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/external/vector/moon.yml b/src/extensions/external/vector/moon.yml index 2cf5aeda..16ccc0ad 100644 --- a/src/extensions/external/vector/moon.yml +++ b/src/extensions/external/vector/moon.yml @@ -33,7 +33,7 @@ tasks: assemble-release: tags: ["release", "artifact-package"] - command: "python3 tools/release/build-extension-ci-artifacts.py oliphaunt-extension-vector --require-native --require-wasix" + command: "bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs oliphaunt-extension-vector --require-native --require-wasix" deps: - "oliphaunt-extension-vector:check" inputs: @@ -41,9 +41,9 @@ tasks: - "/release-please-config.json" - "/src/extensions/external/vector/**/*" - "/src/extensions/generated/wasix/extensions.json" - - "/tools/release/build-extension-ci-artifacts.py" - - "/tools/release/extension_artifact_targets.py" - - "/tools/release/product_metadata.py" + - "/tools/release/build-extension-ci-artifacts.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/target/extensions/native/release-assets/**/*" - "/target/extensions/wasix/release-assets/**/*" outputs: diff --git a/src/extensions/generated/docs/extension-evidence.json b/src/extensions/generated/docs/extension-evidence.json index 57b985e5..c696c0cb 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:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -56,7 +56,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -92,7 +92,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -128,7 +128,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -164,7 +164,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -200,7 +200,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -236,7 +236,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -272,7 +272,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -308,7 +308,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -344,7 +344,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -380,7 +380,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -416,7 +416,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -452,7 +452,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -488,7 +488,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -524,7 +524,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -560,7 +560,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -596,7 +596,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -632,7 +632,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -668,7 +668,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -704,7 +704,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -740,7 +740,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -776,7 +776,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -812,7 +812,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -848,7 +848,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -884,7 +884,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -920,7 +920,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -956,7 +956,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -992,7 +992,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -1028,7 +1028,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -1064,7 +1064,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -1100,7 +1100,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -1136,7 +1136,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -1172,7 +1172,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -1208,7 +1208,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -1244,7 +1244,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -1280,7 +1280,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -1316,7 +1316,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -1352,7 +1352,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -1388,7 +1388,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d" + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" } ], "platform-targets": [ @@ -1420,7 +1420,7 @@ "path": "src/extensions/evidence/runs" } ], - "source-digest": "sha256:300d8d8f0e0ae79a9ff6ff1c85ebeb97de5ffcf800fab76d9c764fdadc023c8d", + "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4", "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 2668898d..aa809b8d 100644 --- a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 +++ b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 @@ -1 +1 @@ -cddc150a6d1d2817d4856b7a439c3ab81a3b92c1d4f85504b281e8c30e81c50e +9cbe9b35eaa955c2f205314933cf7c5aeaa6ce0638089378a261326e15851f22 diff --git a/tools/graph/moon.yml b/tools/graph/moon.yml index 4e5ff730..ad36e816 100644 --- a/tools/graph/moon.yml +++ b/tools/graph/moon.yml @@ -19,7 +19,7 @@ owners: tasks: check: tags: ["policy", "assertion", "quality", "static"] - command: "tools/dev/bun.sh tools/graph/graph.mjs check" + command: "bash tools/dev/bun.sh tools/graph/graph.mjs check" inputs: - "/.moon/workspace.yml" - "/.moon/toolchains.yml" @@ -45,7 +45,7 @@ tasks: runFromWorkspaceRoot: true generate: tags: ["generated", "graph"] - command: "tools/dev/bun.sh tools/graph/graph.mjs generate" + command: "bash tools/dev/bun.sh tools/graph/graph.mjs generate" inputs: - "/.moon/workspace.yml" - "/.moon/toolchains.yml" diff --git a/tools/policy/check-moon-product-graph.mjs b/tools/policy/check-moon-product-graph.mjs index 79967245..c07585af 100755 --- a/tools/policy/check-moon-product-graph.mjs +++ b/tools/policy/check-moon-product-graph.mjs @@ -774,12 +774,12 @@ assertTaskOutput(tasks, 'extension-artifacts-native', 'build-target', 'target/ex assertTaskCommand(tasks, 'extension-artifacts-wasix', 'build-target', 'src/extensions/artifacts/wasix/tools/package-release-assets.sh'); assertTaskDependency(tasks, 'extension-artifacts-wasix', 'build-target', 'liboliphaunt-wasix:runtime-portable'); assertTaskOutput(tasks, 'extension-artifacts-wasix', 'build-target', 'target/extensions/wasix/release-assets/**/*'); -assertTaskCommand(tasks, 'extension-packages', 'assemble-release', 'python3 tools/release/build-extension-ci-artifacts.py --all --require-native --require-wasix'); +assertTaskCommand(tasks, 'extension-packages', 'assemble-release', 'bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs --all --require-native --require-wasix'); assertTaskOutput(tasks, 'extension-packages', 'assemble-release', 'target/extension-artifacts/**/*'); assertTaskCommand(tasks, 'extension-packages', 'assemble-mobile', 'src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh'); assertTaskOutput(tasks, 'extension-packages', 'assemble-mobile', 'target/extension-artifacts/**/*'); for (const projectId of exactExtensionProducts) { - assertTaskCommand(tasks, projectId, 'assemble-release', `python3 tools/release/build-extension-ci-artifacts.py ${projectId} --require-native --require-wasix`); + assertTaskCommand(tasks, projectId, 'assemble-release', `bash tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs ${projectId} --require-native --require-wasix`); assertTaskOutput(tasks, projectId, 'assemble-release', `target/extension-artifacts/${projectId}/**/*`); assertTaskCache(tasks, projectId, 'assemble-release', false); } diff --git a/tools/policy/check-release-policy.py b/tools/policy/check-release-policy.py index 4259ce87..b64c7418 100644 --- a/tools/policy/check-release-policy.py +++ b/tools/policy/check-release-policy.py @@ -580,7 +580,7 @@ def check_ci_policy() -> None: "CI extension package builders must consume selected exact-extension products from the affected plan", ) assert_contains( - "tools/release/build-extension-ci-artifacts.py", + "tools/release/build-extension-ci-artifacts.mjs", "OLIPHAUNT_EXTENSION_PACKAGE_PRODUCTS", "exact-extension package builder must support selected product subsets", ) diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index a9950d42..384cc4ea 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -3,7 +3,6 @@ src/extensions/tools/check-extension-model.py tools/policy/check-release-policy.py 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_release_metadata.py diff --git a/tools/release/build-extension-ci-artifacts.mjs b/tools/release/build-extension-ci-artifacts.mjs new file mode 100644 index 00000000..89fa0c25 --- /dev/null +++ b/tools/release/build-extension-ci-artifacts.mjs @@ -0,0 +1,798 @@ +#!/usr/bin/env bun +import { createHash } from "node:crypto"; +import { + copyFileSync, + cpSync, + existsSync, + mkdirSync, + readdirSync, + readFileSync, + rmSync, + statSync, + writeFileSync, + chmodSync, +} from "node:fs"; +import path from "node:path"; + +import { + ROOT, + compareText, + currentProductVersion, + exactExtensionProducts, + extensionArtifactTargets, +} from "./release-artifact-targets.mjs"; +import { loadGraph } from "./release-graph.mjs"; + +const PREFIX = "build-extension-ci-artifacts.mjs"; +const EXTENSION_VERSIONING_BY_CLASS = { + contrib: "postgres-bound", + external: "upstream-bound", + "first-party": "repo-bound", +}; +const EXTENSION_RUNTIME_CONTRACT_PATH = "src/shared/extension-runtime-contract/contract.toml"; +const POSTGRES18_SOURCE_PATH = "src/postgres/versions/18/source.toml"; + +function fail(message) { + console.error(`${PREFIX}: ${message}`); + process.exit(1); +} + +function rel(file) { + return path.relative(ROOT, file).split(path.sep).join("/"); +} + +function sha256(file) { + return createHash("sha256").update(readFileSync(file)).digest("hex"); +} + +function graphProducts() { + return loadGraph(PREFIX).products; +} + +function productConfig(product) { + const config = graphProducts()[product]; + if (!config) { + fail(`unknown release product ${product}`); + } + return config; +} + +function packagePath(product) { + return releaseMetadataRelativePath( + nonEmptyString(productConfig(product).path, `${product}.path`), + `${product}.path`, + ); +} + +function extensionProducts() { + return exactExtensionProducts(PREFIX); +} + +function extensionSqlName(product) { + const value = productConfig(product).extension_sql_name; + if (typeof value !== "string" || !value) { + fail(`${product} release metadata must declare extension_sql_name`); + } + return value; +} + +function generatedExtensionRow(sqlName) { + const metadata = path.join(ROOT, "src/extensions/generated/sdk/kotlin.json"); + const data = JSON.parse(readFileSync(metadata, "utf8")); + const row = (data.extensions ?? []).find((item) => item && item["sql-name"] === sqlName); + if (!row) { + fail(`generated extension metadata has no row for ${sqlName}`); + } + return row; +} + +function stringList(value) { + if (!Array.isArray(value)) { + return []; + } + return value.map((item) => String(item)).filter(Boolean).sort(compareText); +} + +function propertiesCsv(values) { + return values.join(","); +} + +function publicAsset(asset) { + const result = {}; + for (const key of ["name", "family", "target", "kind", "sha256", "bytes"]) { + if (Object.hasOwn(asset, key)) { + result[key] = asset[key]; + } + } + return result; +} + +function resolveRepoPath(value, { label }) { + const resolved = path.resolve(ROOT, value); + const relative = path.relative(ROOT, resolved); + if (relative.startsWith("..") || path.isAbsolute(relative)) { + fail(`${label} must be inside the repository: ${resolved}`); + } + return resolved; +} + +function nativeReleaseAssetRoot() { + return resolveRepoPath(process.env.OLIPHAUNT_NATIVE_EXTENSION_RELEASE_ASSET_ROOT ?? "target/extensions/native/release-assets", { + label: "native extension release asset root", + }); +} + +function wasixReleaseAssetRoot() { + return resolveRepoPath(process.env.OLIPHAUNT_WASIX_EXTENSION_RELEASE_ASSET_ROOT ?? "target/extensions/wasix/release-assets", { + label: "WASIX extension release asset root", + }); +} + +function wasixAotArtifactRoot() { + return resolveRepoPath(process.env.OLIPHAUNT_WASIX_EXTENSION_AOT_ARTIFACT_ROOT ?? "target/extensions/wasix/aot-artifacts", { + label: "WASIX extension AOT artifact root", + }); +} + +function parseTsv(file) { + const lines = readFileSync(file, "utf8").split(/\r?\n/u).filter((line) => line.length > 0); + if (lines.length === 0) { + return []; + } + const header = lines[0].split("\t"); + return lines.slice(1).map((line) => { + const values = line.split("\t"); + return Object.fromEntries(header.map((column, index) => [column, values[index] ?? ""])); + }); +} + +function indexContainsSqlName(index, sqlName) { + return parseTsv(index).some((row) => row.sql_name === sqlName); +} + +function publishedTargetIds(family) { + return [...new Set( + extensionArtifactTargets({ family, publishedOnly: true }, PREFIX).map((target) => target.target), + )].sort(compareText); +} + +function nativeExtensionAssetIndexes(sqlName, product = undefined) { + const version = currentProductVersionSync("liboliphaunt-native"); + const root = nativeReleaseAssetRoot(); + const indexes = []; + for (const target of publishedTargetIds("native")) { + const targetRoot = path.join(root, target); + if (product !== undefined) { + const productIndex = path.join(targetRoot, product, `liboliphaunt-${version}-native-extension-assets.tsv`); + if (existsSync(productIndex) && indexContainsSqlName(productIndex, sqlName)) { + indexes.push(productIndex); + continue; + } + } + const directIndex = path.join(targetRoot, `liboliphaunt-${version}-native-extension-assets.tsv`); + if (existsSync(directIndex)) { + indexes.push(directIndex); + } + } + return indexes.sort(compareText); +} + +function nativeAssetsFromTargetIndexes(sqlName, { product = undefined, required = false } = {}) { + const indexes = nativeExtensionAssetIndexes(sqlName, product); + if (indexes.length === 0) { + return []; + } + const assets = []; + const seen = new Set(); + for (const index of indexes) { + for (const row of parseTsv(index)) { + if (row.sql_name !== sqlName) { + continue; + } + const { target, kind, artifact } = row; + if (!target || !kind || !artifact) { + fail(`${rel(index)} has an incomplete native asset row for ${sqlName}`); + } + const dedupeKey = `${target}\0${kind}`; + if (seen.has(dedupeKey)) { + fail(`duplicate native extension asset row for ${sqlName} target=${target} kind=${kind}`); + } + seen.add(dedupeKey); + const asset = path.join(path.dirname(index), artifact); + if (!existsSync(asset) || !statSync(asset).isFile()) { + fail(`${rel(index)} references missing native asset ${rel(asset)}`); + } + assets.push([asset, target, kind]); + } + } + if (required && assets.length === 0) { + fail(`${sqlName} has no native extension assets in native target asset indexes`); + } + return assets; +} + +function nativeAssetsFor(sqlName, { product = undefined, required = false } = {}) { + const indexed = nativeAssetsFromTargetIndexes(sqlName, { product, required: false }); + if (indexed.length > 0) { + return indexed; + } + if (required) { + fail(`${sqlName}${product ? ` for ${product}` : ""} has no native extension assets in native target asset indexes`); + } + return []; +} + +function wasixArchiveFor(sqlName, { product = undefined, required = false } = {}) { + const version = currentProductVersionSync("liboliphaunt-wasix"); + const root = wasixReleaseAssetRoot(); + const indexes = []; + for (const target of publishedTargetIds("wasix")) { + const targetRoot = path.join(root, target); + if (product !== undefined) { + const productIndex = path.join(targetRoot, product, `liboliphaunt-wasix-${version}-wasix-extension-assets.tsv`); + if (existsSync(productIndex)) { + indexes.push(productIndex); + continue; + } + } + const directIndex = path.join(targetRoot, `liboliphaunt-wasix-${version}-wasix-extension-assets.tsv`); + if (existsSync(directIndex)) { + indexes.push(directIndex); + } + } + const assets = []; + for (const index of indexes) { + for (const row of parseTsv(index)) { + if (row.sql_name !== sqlName) { + continue; + } + const { target, kind, artifact } = row; + if (target !== "wasix-portable" || kind !== "wasix-runtime" || !artifact) { + fail(`${rel(index)} has an invalid WASIX asset row for ${sqlName}`); + } + const asset = path.join(path.dirname(index), artifact); + if (!existsSync(asset) || !statSync(asset).isFile()) { + fail(`${rel(index)} references missing WASIX asset ${rel(asset)}`); + } + assets.push(asset); + } + } + if (assets.length > 1) { + fail(`${sqlName} has duplicate WASIX extension assets: ${assets.map(rel).join(", ")}`); + } + if (assets.length === 1) { + return assets[0]; + } + if (required) { + fail(`${sqlName} has no WASIX extension assets in target/extensions/wasix/release-assets target indexes`); + } + return undefined; +} + +function wasixAotDirsFor(sqlName) { + const root = wasixAotArtifactRoot(); + if (!existsSync(root) || !statSync(root).isDirectory()) { + return []; + } + return readdirSync(root, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => [entry.name, path.join(root, entry.name, sqlName)]) + .filter(([, candidate]) => existsSync(path.join(candidate, "manifest.json"))) + .sort(([left], [right]) => compareText(left, right)); +} + +function copyAsset(source, destinationDir, { name }) { + mkdirSync(destinationDir, { recursive: true }); + const destination = path.join(destinationDir, name); + copyFileSync(source, destination); + chmodSync(destination, statSync(source).mode & 0o777); + return { + name: path.basename(destination), + path: rel(destination), + source: rel(source), + sha256: sha256(destination), + bytes: statSync(destination).size, + }; +} + +function nativeAssetName(product, version, target, kind, source) { + const suffix = archiveSuffix(source); + if (target === "macos-arm64") { + return `${product}-${version}-native-macos-arm64-runtime${suffix}`; + } + if (target.startsWith("linux-")) { + return `${product}-${version}-native-${target}-runtime${suffix}`; + } + if (target.startsWith("windows-")) { + return `${product}-${version}-native-${target}-runtime${suffix}`; + } + if (target === "ios-xcframework") { + if (kind === "runtime") { + return `${product}-${version}-native-ios-runtime${suffix}`; + } + if (kind === "ios-xcframework") { + return `${product}-${version}-native-ios-xcframework${suffix}`; + } + fail(`unsupported iOS extension artifact kind ${kind} for ${path.basename(source)}`); + } + if (target.startsWith("android-")) { + if (kind === "runtime") { + return `${product}-${version}-native-${target}-runtime${suffix}`; + } + if (kind === "android-static-archive") { + return `${product}-${version}-native-${target}-static${suffix}`; + } + fail(`unsupported Android extension artifact kind ${kind} for ${path.basename(source)}`); + } + fail(`unsupported native extension artifact target ${target} for ${path.basename(source)}`); +} + +function archiveSuffix(source) { + for (const suffix of [".tar.gz", ".tar.zst", ".zip"]) { + if (source.endsWith(suffix)) { + return suffix; + } + } + fail(`native extension asset ${path.basename(source)} must use .tar.gz, .tar.zst, or .zip`); +} + +function validateStagedTargets(product, assets, { requireNative, requireWasix, requireNativeTargets }) { + const declaredNativeTargets = new Set( + extensionArtifactTargets({ product, family: "native", publishedOnly: true }, PREFIX).map((target) => target.target), + ); + const declaredWasixTargets = new Set( + extensionArtifactTargets({ product, family: "wasix", publishedOnly: true }, PREFIX).map((target) => target.target), + ); + const stagedNativeTargets = new Set(assets.filter((asset) => asset.family === "native").map((asset) => String(asset.target))); + const stagedWasixTargets = new Set(assets.filter((asset) => asset.family === "wasix").map((asset) => String(asset.target))); + const extraNative = [...stagedNativeTargets].filter((target) => !declaredNativeTargets.has(target)).sort(compareText); + const extraWasix = [...stagedWasixTargets].filter((target) => !declaredWasixTargets.has(target)).sort(compareText); + if (extraNative.length > 0) { + fail(`${product} staged undeclared native extension targets: ${extraNative.join(", ")}`); + } + if (extraWasix.length > 0) { + fail(`${product} staged undeclared WASIX extension targets: ${extraWasix.join(", ")}`); + } + if (requireNativeTargets.size > 0) { + const unknownRequired = [...requireNativeTargets].filter((target) => !declaredNativeTargets.has(target)).sort(compareText); + if (unknownRequired.length > 0) { + fail(`${product} was asked to require undeclared native targets: ${unknownRequired.join(", ")}`); + } + const missingNative = [...requireNativeTargets].filter((target) => !stagedNativeTargets.has(target)).sort(compareText); + if (missingNative.length > 0) { + fail(`${product} is missing native extension artifacts for: ${missingNative.join(", ")}`); + } + } else if (requireNative) { + const missingNative = [...declaredNativeTargets].filter((target) => !stagedNativeTargets.has(target)).sort(compareText); + if (missingNative.length > 0) { + fail(`${product} is missing native extension artifacts for: ${missingNative.join(", ")}`); + } + } + if (requireWasix) { + const missingWasix = [...declaredWasixTargets].filter((target) => !stagedWasixTargets.has(target)).sort(compareText); + if (missingWasix.length > 0) { + fail(`${product} is missing WASIX extension artifacts for: ${missingWasix.join(", ")}`); + } + } +} + +function extensionMetadata(product) { + const config = productConfig(product); + if (config.kind !== "exact-extension-artifact") { + fail(`${product} is not an exact-extension artifact product`); + } + const topLevelSqlName = config.extension_sql_name; + if (typeof topLevelSqlName !== "string" || !topLevelSqlName) { + fail(`${product} release metadata must declare extension_sql_name`); + } + const metadata = config.extension; + if (metadata === null || Array.isArray(metadata) || typeof metadata !== "object") { + fail(`${product} release metadata must declare [extension]`); + } + const sqlName = nonEmptyString(metadata.sql_name, `${product}.extension.sql_name`); + if (sqlName !== topLevelSqlName) { + fail(`${product}.extension.sql_name ${JSON.stringify(sqlName)} must match extension_sql_name ${JSON.stringify(topLevelSqlName)}`); + } + const extensionClass = nonEmptyString(metadata.class, `${product}.extension.class`); + if (!(extensionClass in EXTENSION_VERSIONING_BY_CLASS)) { + fail(`${product}.extension.class must be one of ${Object.keys(EXTENSION_VERSIONING_BY_CLASS).sort(compareText).join(", ")}`); + } + const versioning = nonEmptyString(metadata.versioning, `${product}.extension.versioning`); + const expectedVersioning = EXTENSION_VERSIONING_BY_CLASS[extensionClass]; + if (versioning !== expectedVersioning) { + fail(`${product}.extension.versioning must be ${JSON.stringify(expectedVersioning)} for class ${JSON.stringify(extensionClass)}, got ${JSON.stringify(versioning)}`); + } + const source = metadata.source; + if (source === null || Array.isArray(source) || typeof source !== "object") { + fail(`${product}.extension must declare [extension.source]`); + } + const sourcePath = releaseMetadataRelativePath(nonEmptyString(source.path, `${product}.extension.source.path`), `${product}.extension.source.path`); + const packageRoot = packagePath(product); + if (extensionClass === "contrib" && sourcePath !== POSTGRES18_SOURCE_PATH) { + fail(`${product}.extension.source.path must be ${JSON.stringify(POSTGRES18_SOURCE_PATH)} for contrib extensions`); + } + if (extensionClass === "external" && sourcePath !== `${packageRoot}/source.toml`) { + fail(`${product}.extension.source.path must be ${packageRoot}/source.toml for external extensions`); + } + if (extensionClass === "first-party" && !(sourcePath === packageRoot || sourcePath.startsWith(`${packageRoot}/`))) { + fail(`${product}.extension.source.path must stay inside ${packageRoot}/ for first-party extensions`); + } + + const compatibility = metadata.compatibility; + if (compatibility === null || Array.isArray(compatibility) || typeof compatibility !== "object") { + fail(`${product}.extension must declare [extension.compatibility]`); + } + const postgresMajor = nonEmptyString(compatibility.postgres_major, `${product}.extension.compatibility.postgres_major`); + if (postgresMajor !== "18") { + fail(`${product}.extension.compatibility.postgres_major must be '18', got ${JSON.stringify(postgresMajor)}`); + } + const contractPath = releaseMetadataRelativePath( + nonEmptyString(compatibility.extension_runtime_contract, `${product}.extension.compatibility.extension_runtime_contract`), + `${product}.extension.compatibility.extension_runtime_contract`, + ); + if (contractPath !== EXTENSION_RUNTIME_CONTRACT_PATH) { + fail(`${product}.extension.compatibility.extension_runtime_contract must be ${JSON.stringify(EXTENSION_RUNTIME_CONTRACT_PATH)}`); + } + const nativeProduct = nonEmptyString(compatibility.native_runtime_product, `${product}.extension.compatibility.native_runtime_product`); + const wasixProduct = nonEmptyString(compatibility.wasix_runtime_product, `${product}.extension.compatibility.wasix_runtime_product`); + if (nativeProduct !== "liboliphaunt-native") { + fail(`${product}.extension.compatibility.native_runtime_product must be 'liboliphaunt-native'`); + } + if (wasixProduct !== "liboliphaunt-wasix") { + fail(`${product}.extension.compatibility.wasix_runtime_product must be 'liboliphaunt-wasix'`); + } + const nativeVersion = nonEmptyString(compatibility.native_runtime_version, `${product}.extension.compatibility.native_runtime_version`); + const wasixVersion = nonEmptyString(compatibility.wasix_runtime_version, `${product}.extension.compatibility.wasix_runtime_version`); + const expectedNativeVersion = currentProductVersionSync(nativeProduct); + const expectedWasixVersion = currentProductVersionSync(wasixProduct); + if (nativeVersion !== expectedNativeVersion) { + fail(`${product}.extension.compatibility.native_runtime_version must be ${JSON.stringify(expectedNativeVersion)}, got ${JSON.stringify(nativeVersion)}`); + } + if (wasixVersion !== expectedWasixVersion) { + fail(`${product}.extension.compatibility.wasix_runtime_version must be ${JSON.stringify(expectedWasixVersion)}, got ${JSON.stringify(wasixVersion)}`); + } + return { + sqlName, + class: extensionClass, + versioning, + sourcePath, + compatibility: { + postgresMajor, + extensionRuntimeContract: contractPath, + nativeRuntimeProduct: nativeProduct, + nativeRuntimeVersion: nativeVersion, + wasixRuntimeProduct: wasixProduct, + wasixRuntimeVersion: wasixVersion, + }, + }; +} + +function extensionSourceIdentity(product) { + const metadata = extensionMetadata(product); + const source = Bun.TOML.parse(readFileSync(path.join(ROOT, metadata.sourcePath), "utf8")); + if (metadata.class === "contrib") { + const postgresql = source.postgresql; + if (postgresql === null || Array.isArray(postgresql) || typeof postgresql !== "object") { + fail(`${metadata.sourcePath} must declare [postgresql] for contrib extension products`); + } + return { + kind: "postgres-contrib", + name: "postgresql", + version: nonEmptyString(postgresql.version, `${metadata.sourcePath}.postgresql.version`), + url: nonEmptyString(postgresql.url, `${metadata.sourcePath}.postgresql.url`), + sha256: nonEmptyString(postgresql.sha256, `${metadata.sourcePath}.postgresql.sha256`), + }; + } + if (metadata.class === "external") { + return { + kind: "external", + name: nonEmptyString(source.name, `${metadata.sourcePath}.name`), + url: nonEmptyString(source.url, `${metadata.sourcePath}.url`), + branch: nonEmptyString(source.branch, `${metadata.sourcePath}.branch`), + commit: nonEmptyString(source.commit, `${metadata.sourcePath}.commit`), + }; + } + if (metadata.class === "first-party") { + return { + kind: "repo", + name: metadata.sqlName, + path: metadata.sourcePath, + version: currentProductVersionSync(product), + }; + } + fail(`${product}.extension.class has unsupported source identity class ${JSON.stringify(metadata.class)}`); +} + +async function stageProduct(product, { outputRoot, requireNative, requireWasix, requireNativeTargets }) { + const known = new Set(extensionProducts()); + if (!known.has(product)) { + fail(`unknown exact-extension product ${product}; expected one of: ${[...known].sort(compareText).join(", ")}`); + } + const sqlName = extensionSqlName(product); + const extensionRow = generatedExtensionRow(sqlName); + const version = await currentProductVersion(product, PREFIX); + const productRoot = path.join(outputRoot, product); + const assetDir = path.join(productRoot, "release-assets"); + rmSync(productRoot, { recursive: true, force: true }); + mkdirSync(assetDir, { recursive: true }); + + const assets = []; + for (const [nativeAsset, target, kind] of nativeAssetsFor(sqlName, { product, required: requireNative })) { + if (requireNativeTargets.size > 0 && !requireNativeTargets.has(target)) { + continue; + } + const metadata = copyAsset(nativeAsset, assetDir, { + name: nativeAssetName(product, version, target, kind, nativeAsset), + }); + metadata.family = "native"; + metadata.kind = kind; + metadata.target = target; + assets.push(metadata); + } + + const wasixArchive = wasixArchiveFor(sqlName, { product, required: requireWasix }); + if (wasixArchive !== undefined) { + const metadata = copyAsset(wasixArchive, assetDir, { + name: `${product}-${version}-wasix-portable.tar.zst`, + }); + metadata.family = "wasix"; + metadata.kind = "wasix-runtime"; + metadata.target = "wasix-portable"; + assets.push(metadata); + } + + for (const [targetId, source] of wasixAotDirsFor(sqlName)) { + const destination = path.join(productRoot, "wasix-aot", targetId); + rmSync(destination, { recursive: true, force: true }); + cpSync(source, destination, { recursive: true }); + } + + validateStagedTargets(product, assets, { + requireNative, + requireWasix, + requireNativeTargets, + }); + if (assets.length === 0) { + fail(`${product} produced no extension artifacts`); + } + + const manifest = { + schema: "oliphaunt-extension-ci-artifacts-v1", + product, + version, + sqlName, + dependencies: stringList(extensionRow["selected-extension-dependencies"]), + nativeModuleStem: extensionRow["native-module-stem"], + sharedPreloadLibraries: stringList(extensionRow["shared-preload-libraries"]), + mobileReleaseReady: extensionRow["mobile-release-ready"] === true, + desktopReleaseReady: extensionRow["desktop-release-ready"] === true, + assets, + }; + writeFileSync(path.join(productRoot, "extension-artifacts.json"), `${JSON.stringify(sortValue(manifest), null, 2)}\n`, "utf8"); + + const releaseMetadata = extensionMetadata(product); + const releaseData = { + schema: "oliphaunt-extension-release-manifest-v1", + product, + version, + sqlName, + extensionClass: releaseMetadata.class, + versioning: releaseMetadata.versioning, + sourceIdentity: extensionSourceIdentity(product), + compatibility: releaseMetadata.compatibility, + dependencies: manifest.dependencies, + nativeModuleStem: manifest.nativeModuleStem, + sharedPreloadLibraries: manifest.sharedPreloadLibraries, + mobileReleaseReady: manifest.mobileReleaseReady, + desktopReleaseReady: manifest.desktopReleaseReady, + assets: assets.map(publicAsset), + }; + const releaseManifest = path.join(assetDir, `${product}-${version}-manifest.json`); + writeFileSync(releaseManifest, `${JSON.stringify(sortValue(releaseData), null, 2)}\n`, "utf8"); + + const propertiesManifest = path.join(assetDir, `${product}-${version}-manifest.properties`); + const sourceIdentity = releaseData.sourceIdentity; + const propertiesLines = [ + "schema=oliphaunt-extension-release-manifest-v1\n", + `product=${product}\n`, + `version=${version}\n`, + `sqlName=${sqlName}\n`, + `extensionClass=${releaseData.extensionClass}\n`, + `versioning=${releaseData.versioning}\n`, + `sourceKind=${sourceIdentity.kind}\n`, + `dependencies=${propertiesCsv(manifest.dependencies)}\n`, + `nativeModuleStem=${manifest.nativeModuleStem || ""}\n`, + `sharedPreloadLibraries=${propertiesCsv(manifest.sharedPreloadLibraries)}\n`, + `mobileReleaseReady=${manifest.mobileReleaseReady ? "true" : "false"}\n`, + `desktopReleaseReady=${manifest.desktopReleaseReady ? "true" : "false"}\n`, + ]; + for (const asset of [...assets].sort((left, right) => compareText(`${left.family}\0${left.target}\0${left.kind}`, `${right.family}\0${right.target}\0${right.kind}`))) { + propertiesLines.push(`asset.${asset.family}.${asset.target}.${asset.kind}=${asset.name}\n`); + } + writeFileSync(propertiesManifest, propertiesLines.join(""), "utf8"); + + const checksumManifest = path.join(assetDir, `${product}-${version}-release-assets.sha256`); + const checksumLines = readdirSync(assetDir) + .map((name) => path.join(assetDir, name)) + .filter((file) => statSync(file).isFile() && file !== checksumManifest) + .sort(compareText) + .map((file) => `${sha256(file)} ./${path.basename(file)}\n`); + writeFileSync(checksumManifest, checksumLines.join(""), "utf8"); + writeFileSync( + path.join(productRoot, "artifacts.txt"), + [ + ...assets.map((asset) => `${asset.path}\n`), + `${rel(releaseManifest)}\n`, + `${rel(propertiesManifest)}\n`, + `${rel(checksumManifest)}\n`, + ].join(""), + "utf8", + ); + console.log(`${product}: staged ${assets.length} exact-extension artifact(s) in ${rel(productRoot)}`); +} + +function selectedProductsFromEnv() { + const raw = process.env.OLIPHAUNT_EXTENSION_PACKAGE_PRODUCTS ?? ""; + const products = [...new Set(raw.split(",").map((item) => item.trim()).filter(Boolean))].sort(compareText); + if (products.length === 0) { + return []; + } + const known = new Set(extensionProducts()); + const unknown = products.filter((product) => !known.has(product)); + if (unknown.length > 0) { + fail(`OLIPHAUNT_EXTENSION_PACKAGE_PRODUCTS contains unknown exact-extension product(s): ${unknown.join(", ")}`); + } + return products; +} + +function parseArgs(argv) { + const args = { + products: [], + all: false, + outputRoot: "target/extension-artifacts", + requireNative: false, + requireWasix: false, + requireNativeTargets: new Set(), + }; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === "--all") { + args.all = true; + } else if (arg === "--output-root") { + const value = argv[index + 1]; + if (!value) { + fail("--output-root requires a value"); + } + args.outputRoot = value; + index += 1; + } else if (arg === "--require-native") { + args.requireNative = true; + } else if (arg === "--require-native-target") { + const value = argv[index + 1]; + if (!value) { + fail("--require-native-target requires a value"); + } + args.requireNativeTargets.add(value); + index += 1; + } else if (arg === "--require-wasix") { + args.requireWasix = true; + } else if (arg === "--help" || arg === "-h") { + console.log("usage: tools/release/build-extension-ci-artifacts.mjs [--all] [--output-root DIR] [--require-native] [--require-native-target TARGET] [--require-wasix] [products...]"); + process.exit(0); + } else if (arg.startsWith("--")) { + fail(`unknown argument ${arg}`); + } else { + args.products.push(arg); + } + } + return args; +} + +function sortValue(value) { + if (Array.isArray(value)) { + return value.map(sortValue); + } + if (value !== null && typeof value === "object") { + return Object.fromEntries(Object.keys(value).sort(compareText).map((key) => [key, sortValue(value[key])])); + } + return value; +} + +const versionCache = new Map(); + +function currentProductVersionSync(product) { + if (!versionCache.has(product)) { + const versionFile = productConfig(product).version_files?.[0]; + if (typeof versionFile !== "string" || !versionFile) { + fail(`${product} does not declare a canonical version file`); + } + const file = path.join(ROOT, versionFile); + const text = readFileSync(file, "utf8"); + const name = path.basename(file); + let version = ""; + if (name === "Cargo.toml") { + 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; + } + const match = inPackage ? /^version\s*=\s*"([^"]+)"/u.exec(line) : null; + if (match) { + version = match[1]; + break; + } + } + } else if (name === "package.json" || name === "jsr.json") { + const data = JSON.parse(text); + version = typeof data.version === "string" ? data.version : ""; + } else if (name === "gradle.properties") { + for (const rawLine of text.split(/\r?\n/u)) { + const line = rawLine.trim(); + if (!line || line.startsWith("#") || !line.includes("=")) { + continue; + } + const [key, ...rest] = line.split("="); + if (key.trim() === "VERSION_NAME") { + version = rest.join("=").trim(); + break; + } + } + } else if (name === "VERSION" || name === "LIBOLIPHAUNT_VERSION") { + version = text.trim(); + } else { + fail(`${product}.version_files has unsupported version file type: ${versionFile}`); + } + if (!version) { + fail(`${versionFile} does not define a release version for ${product}`); + } + versionCache.set(product, version); + } + return versionCache.get(product); +} + +function nonEmptyString(value, context) { + if (typeof value === "string" && value.length > 0) { + return value; + } + fail(`${context} must be a non-empty string`); +} + +function releaseMetadataRelativePath(value, context) { + const candidate = path.normalize(value).split(path.sep).join("/"); + if (path.isAbsolute(value) || candidate.split("/").includes("..")) { + fail(`${context} must be a repository-relative path: ${JSON.stringify(value)}`); + } + if (!existsSync(path.join(ROOT, candidate))) { + fail(`${context} path does not exist: ${candidate}`); + } + return candidate; +} + +async function main(argv) { + const args = parseArgs(argv); + const envProducts = selectedProductsFromEnv(); + const products = envProducts.length > 0 + ? envProducts + : args.all + ? extensionProducts() + : args.products; + if (products.length === 0) { + fail("pass --all or at least one exact-extension product id"); + } + const outputRoot = resolveRepoPath(args.outputRoot, { label: "output root" }); + for (const product of products) { + await stageProduct(product, { + outputRoot, + requireNative: args.requireNative, + requireWasix: args.requireWasix, + requireNativeTargets: args.requireNativeTargets, + }); + } +} + +await main(Bun.argv.slice(2)); diff --git a/tools/release/build-extension-ci-artifacts.py b/tools/release/build-extension-ci-artifacts.py deleted file mode 100755 index 60e6a4d7..00000000 --- a/tools/release/build-extension-ci-artifacts.py +++ /dev/null @@ -1,531 +0,0 @@ -#!/usr/bin/env python3 -"""Stage publishable exact-extension artifacts from built runtime outputs.""" - -from __future__ import annotations - -import argparse -import csv -import hashlib -import json -import os -import shutil -import sys -from pathlib import Path -from typing import NoReturn - -import product_metadata -import extension_artifact_targets - - -ROOT = Path(__file__).resolve().parents[2] - - -def fail(message: str) -> NoReturn: - print(f"build-extension-ci-artifacts.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 extension_products() -> list[str]: - products = [] - for product in product_metadata.product_ids(): - config = product_metadata.product_config(product) - if config.get("kind") == "exact-extension-artifact": - products.append(product) - return sorted(products) - - -def extension_sql_name(product: str) -> str: - config = product_metadata.product_config(product) - value = config.get("extension_sql_name") - if not isinstance(value, str) or not value: - fail(f"{product} release metadata must declare extension_sql_name") - return value - - -def generated_extension_row(sql_name: str) -> dict[str, object]: - metadata = ROOT / "src/extensions/generated/sdk/kotlin.json" - with metadata.open("r", encoding="utf-8") as handle: - data = json.load(handle) - for row in data.get("extensions", []): - if isinstance(row, dict) and row.get("sql-name") == sql_name: - return row - fail(f"generated extension metadata has no row for {sql_name}") - - -def string_list(value: object) -> list[str]: - if not isinstance(value, list): - return [] - return sorted(str(item) for item in value if str(item)) - - -def properties_csv(values: list[str]) -> str: - return ",".join(values) - - -def public_asset(asset: dict[str, object]) -> dict[str, object]: - return { - key: asset[key] - for key in ("name", "family", "target", "kind", "sha256", "bytes") - if key in asset - } - - -def resolve_repo_path(value: str, *, label: str) -> Path: - path = Path(value) - if not path.is_absolute(): - path = ROOT / path - try: - path.relative_to(ROOT) - except ValueError: - fail(f"{label} must be inside the repository: {path}") - return path - - -def native_release_asset_root() -> Path: - return resolve_repo_path( - os.environ.get("OLIPHAUNT_NATIVE_EXTENSION_RELEASE_ASSET_ROOT", "target/extensions/native/release-assets"), - label="native extension release asset root", - ) - - -def wasix_release_asset_root() -> Path: - return resolve_repo_path( - os.environ.get("OLIPHAUNT_WASIX_EXTENSION_RELEASE_ASSET_ROOT", "target/extensions/wasix/release-assets"), - label="WASIX extension release asset root", - ) - - -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")) - - -def native_extension_asset_indexes(sql_name: str, product: str | None = None) -> list[Path]: - version = product_metadata.read_current_version("liboliphaunt-native") - root = native_release_asset_root() - indexes: list[Path] = [] - for target in extension_artifact_targets.published_target_ids(family="native"): - target_root = root / target - if product is not None: - product_index = target_root / product / f"liboliphaunt-{version}-native-extension-assets.tsv" - if product_index.is_file() and index_contains_sql_name(product_index, sql_name): - indexes.append(product_index) - continue - direct_index = target_root / f"liboliphaunt-{version}-native-extension-assets.tsv" - if direct_index.is_file(): - indexes.append(direct_index) - return sorted(indexes) - - -def native_assets_from_target_indexes( - sql_name: str, - *, - product: str | None = None, - required: bool, -) -> list[tuple[Path, str, str]]: - indexes = native_extension_asset_indexes(sql_name, product) - if not indexes: - return [] - - assets: list[tuple[Path, str, str]] = [] - seen: set[tuple[str, str]] = set() - for index in indexes: - with index.open("r", encoding="utf-8", newline="") as handle: - rows = list(csv.DictReader(handle, delimiter="\t")) - for row in rows: - if row.get("sql_name") != sql_name: - continue - target = row.get("target") - kind = row.get("kind") - artifact = row.get("artifact") - if not target or not kind or not artifact: - fail(f"{index.relative_to(ROOT)} has an incomplete native asset row for {sql_name}") - dedupe_key = (target, kind) - if dedupe_key in seen: - fail(f"duplicate native extension asset row for {sql_name} target={target} kind={kind}") - seen.add(dedupe_key) - path = index.parent / artifact - if not path.is_file(): - fail(f"{index.relative_to(ROOT)} references missing native asset {path.relative_to(ROOT)}") - assets.append((path, target, kind)) - - if required and not assets: - fail(f"{sql_name} has no native extension assets in native target asset indexes") - return assets - - -def native_assets_for(sql_name: str, *, product: str | None = None, required: bool) -> list[tuple[Path, str, str]]: - indexed = native_assets_from_target_indexes(sql_name, product=product, required=False) - if indexed: - return indexed - if required: - product_hint = f" for {product}" if product else "" - fail(f"{sql_name}{product_hint} has no native extension assets in native target asset indexes") - return [] - - -def wasix_archive_for(sql_name: str, *, product: str | None = None, required: bool) -> Path | None: - version = product_metadata.read_current_version("liboliphaunt-wasix") - root = wasix_release_asset_root() - indexes: list[Path] = [] - for target in extension_artifact_targets.published_target_ids(family="wasix"): - target_root = root / target - if product is not None: - product_index = target_root / product / f"liboliphaunt-wasix-{version}-wasix-extension-assets.tsv" - if product_index.is_file(): - indexes.append(product_index) - continue - direct_index = target_root / f"liboliphaunt-wasix-{version}-wasix-extension-assets.tsv" - if direct_index.is_file(): - indexes.append(direct_index) - assets: list[Path] = [] - for index in indexes: - with index.open("r", encoding="utf-8", newline="") as handle: - rows = list(csv.DictReader(handle, delimiter="\t")) - for row in rows: - if row.get("sql_name") != sql_name: - continue - target = row.get("target") - kind = row.get("kind") - artifact = row.get("artifact") - if target != "wasix-portable" or kind != "wasix-runtime" or not artifact: - fail(f"{index.relative_to(ROOT)} has an invalid WASIX asset row for {sql_name}") - path = index.parent / artifact - if not path.is_file(): - fail(f"{index.relative_to(ROOT)} references missing WASIX asset {path.relative_to(ROOT)}") - assets.append(path) - if len(assets) > 1: - fail(f"{sql_name} has duplicate WASIX extension assets: {', '.join(str(path.relative_to(ROOT)) for path in assets)}") - if assets: - return assets[0] - - if required: - fail( - f"{sql_name} has no WASIX extension assets in " - "target/extensions/wasix/release-assets target indexes" - ) - 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 - shutil.copy2(source, destination) - return { - "name": destination.name, - "path": str(destination.relative_to(ROOT)), - "source": str(source.relative_to(ROOT)), - "sha256": sha256(destination), - "bytes": destination.stat().st_size, - } - - -def native_asset_name(product: str, version: str, target: str, kind: str, source: Path) -> str: - suffix = archive_suffix(source) - if target == "macos-arm64": - return f"{product}-{version}-native-macos-arm64-runtime{suffix}" - if target.startswith("linux-"): - return f"{product}-{version}-native-{target}-runtime{suffix}" - if target.startswith("windows-"): - return f"{product}-{version}-native-{target}-runtime{suffix}" - if target == "ios-xcframework": - if kind == "runtime": - return f"{product}-{version}-native-ios-runtime{suffix}" - if kind == "ios-xcframework": - return f"{product}-{version}-native-ios-xcframework{suffix}" - fail(f"unsupported iOS extension artifact kind {kind} for {source.name}") - if target.startswith("android-"): - if kind == "runtime": - return f"{product}-{version}-native-{target}-runtime{suffix}" - if kind == "android-static-archive": - return f"{product}-{version}-native-{target}-static{suffix}" - fail(f"unsupported Android extension artifact kind {kind} for {source.name}") - fail(f"unsupported native extension artifact target {target} for {source.name}") - - -def archive_suffix(source: Path) -> str: - for suffix in (".tar.gz", ".tar.zst", ".zip"): - if source.name.endswith(suffix): - return suffix - fail(f"native extension asset {source.name} must use .tar.gz, .tar.zst, or .zip") - - -def validate_staged_targets( - product: str, - assets: list[dict[str, object]], - *, - require_native: bool, - require_wasix: bool, - require_native_targets: set[str], -) -> None: - 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, - ) - } - staged_native_targets = { - str(asset["target"]) - for asset in assets - if asset.get("family") == "native" - } - staged_wasix_targets = { - str(asset["target"]) - for asset in assets - if asset.get("family") == "wasix" - } - - extra_native = staged_native_targets - declared_native_targets - extra_wasix = staged_wasix_targets - declared_wasix_targets - if extra_native: - fail(f"{product} staged undeclared native extension targets: {', '.join(sorted(extra_native))}") - if extra_wasix: - fail(f"{product} staged undeclared WASIX extension targets: {', '.join(sorted(extra_wasix))}") - - if require_native_targets: - unknown_required = require_native_targets - declared_native_targets - if unknown_required: - fail(f"{product} was asked to require undeclared native targets: {', '.join(sorted(unknown_required))}") - missing_native = require_native_targets - staged_native_targets - if missing_native: - fail(f"{product} is missing native extension artifacts for: {', '.join(sorted(missing_native))}") - elif require_native: - missing_native = declared_native_targets - staged_native_targets - if missing_native: - fail(f"{product} is missing native extension artifacts for: {', '.join(sorted(missing_native))}") - if require_wasix: - missing_wasix = declared_wasix_targets - staged_wasix_targets - if missing_wasix: - fail(f"{product} is missing WASIX extension artifacts for: {', '.join(sorted(missing_wasix))}") - - -def resolve_output_root(value: str) -> Path: - return resolve_repo_path(value, label="output root") - - -def stage_product( - product: str, - *, - output_root: Path, - require_native: bool, - require_wasix: bool, - require_native_targets: set[str], -) -> None: - known = set(extension_products()) - if product not in known: - fail(f"unknown exact-extension product {product}; expected one of: {', '.join(sorted(known))}") - - sql_name = extension_sql_name(product) - extension_row = generated_extension_row(sql_name) - version = product_metadata.read_current_version(product) - product_root = output_root / product - asset_dir = product_root / "release-assets" - if product_root.exists(): - shutil.rmtree(product_root) - asset_dir.mkdir(parents=True, exist_ok=True) - - assets: list[dict[str, object]] = [] - for native_asset, target, kind in native_assets_for(sql_name, product=product, required=require_native): - if require_native_targets and target not in require_native_targets: - continue - metadata = copy_asset( - native_asset, - asset_dir, - name=native_asset_name(product, version, target, kind, native_asset), - ) - metadata["family"] = "native" - metadata["kind"] = kind - metadata["target"] = target - assets.append(metadata) - - wasix_archive = wasix_archive_for(sql_name, product=product, required=require_wasix) - if wasix_archive is not None: - wasix_name = f"{product}-{version}-wasix-portable.tar.zst" - metadata = copy_asset(wasix_archive, asset_dir, name=wasix_name) - metadata["family"] = "wasix" - metadata["kind"] = "wasix-runtime" - 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, - require_native=require_native, - require_wasix=require_wasix, - require_native_targets=require_native_targets, - ) - if not assets: - fail(f"{product} produced no extension artifacts") - - manifest = { - "schema": "oliphaunt-extension-ci-artifacts-v1", - "product": product, - "version": version, - "sqlName": sql_name, - "dependencies": string_list(extension_row.get("selected-extension-dependencies")), - "nativeModuleStem": extension_row.get("native-module-stem"), - "sharedPreloadLibraries": string_list(extension_row.get("shared-preload-libraries")), - "mobileReleaseReady": extension_row.get("mobile-release-ready") is True, - "desktopReleaseReady": extension_row.get("desktop-release-ready") is True, - "assets": assets, - } - (product_root / "extension-artifacts.json").write_text( - json.dumps(manifest, indent=2, sort_keys=True) + "\n", - encoding="utf-8", - ) - extension_metadata = product_metadata.extension_metadata(product) - release_data = { - "schema": "oliphaunt-extension-release-manifest-v1", - "product": product, - "version": version, - "sqlName": sql_name, - "extensionClass": extension_metadata["class"], - "versioning": extension_metadata["versioning"], - "sourceIdentity": product_metadata.extension_source_identity(product), - "compatibility": extension_metadata["compatibility"], - "dependencies": manifest["dependencies"], - "nativeModuleStem": manifest["nativeModuleStem"], - "sharedPreloadLibraries": manifest["sharedPreloadLibraries"], - "mobileReleaseReady": manifest["mobileReleaseReady"], - "desktopReleaseReady": manifest["desktopReleaseReady"], - "assets": [public_asset(asset) for asset in assets], - } - release_manifest = asset_dir / f"{product}-{version}-manifest.json" - release_manifest.write_text( - json.dumps(release_data, indent=2, sort_keys=True) + "\n", - encoding="utf-8", - ) - properties_manifest = asset_dir / f"{product}-{version}-manifest.properties" - source_identity = release_data["sourceIdentity"] - properties_lines = [ - "schema=oliphaunt-extension-release-manifest-v1\n", - f"product={product}\n", - f"version={version}\n", - f"sqlName={sql_name}\n", - f"extensionClass={release_data['extensionClass']}\n", - f"versioning={release_data['versioning']}\n", - f"sourceKind={source_identity['kind']}\n", - f"dependencies={properties_csv(manifest['dependencies'])}\n", - f"nativeModuleStem={manifest['nativeModuleStem'] or ''}\n", - f"sharedPreloadLibraries={properties_csv(manifest['sharedPreloadLibraries'])}\n", - f"mobileReleaseReady={'true' if manifest['mobileReleaseReady'] else 'false'}\n", - f"desktopReleaseReady={'true' if manifest['desktopReleaseReady'] else 'false'}\n", - ] - for asset in sorted(assets, key=lambda value: (str(value["family"]), str(value["target"]), str(value["kind"]))): - key = f"asset.{asset['family']}.{asset['target']}.{asset['kind']}" - properties_lines.append(f"{key}={asset['name']}\n") - properties_manifest.write_text("".join(properties_lines), encoding="utf-8") - checksum_manifest = asset_dir / f"{product}-{version}-release-assets.sha256" - checksum_lines = [] - for asset in sorted(path for path in asset_dir.iterdir() if path.is_file() and path != checksum_manifest): - checksum_lines.append(f"{sha256(asset)} ./{asset.name}\n") - checksum_manifest.write_text("".join(checksum_lines), encoding="utf-8") - (product_root / "artifacts.txt").write_text( - "".join( - [ - *(f"{asset['path']}\n" for asset in assets), - f"{release_manifest.relative_to(ROOT)}\n", - f"{properties_manifest.relative_to(ROOT)}\n", - f"{checksum_manifest.relative_to(ROOT)}\n", - ] - ), - encoding="utf-8", - ) - print(f"{product}: staged {len(assets)} exact-extension artifact(s) in {product_root.relative_to(ROOT)}") - - -def parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("products", nargs="*", help="exact-extension product id(s)") - parser.add_argument("--all", action="store_true", help="stage every exact-extension product") - parser.add_argument( - "--output-root", - default="target/extension-artifacts", - help="repository-relative staging root for package-shaped extension artifacts", - ) - parser.add_argument("--require-native", action="store_true", help="fail if native extension assets are missing") - parser.add_argument( - "--require-native-target", - action="append", - default=[], - help="fail if the named native extension target is missing; may be passed more than once", - ) - parser.add_argument("--require-wasix", action="store_true", help="fail if WASIX extension archives are missing") - return parser.parse_args(argv) - - -def selected_products_from_env() -> list[str]: - raw = os.environ.get("OLIPHAUNT_EXTENSION_PACKAGE_PRODUCTS", "") - products = sorted({item.strip() for item in raw.split(",") if item.strip()}) - if not products: - return [] - known = set(extension_products()) - unknown = sorted(set(products) - known) - if unknown: - fail(f"OLIPHAUNT_EXTENSION_PACKAGE_PRODUCTS contains unknown exact-extension product(s): {', '.join(unknown)}") - return products - - -def main(argv: list[str]) -> int: - args = parse_args(argv) - products = selected_products_from_env() or (extension_products() if args.all else args.products) - if not products: - fail("pass --all or at least one exact-extension product id") - output_root = resolve_output_root(args.output_root) - require_native_targets = set(args.require_native_target) - for product in products: - stage_product( - product, - output_root=output_root, - require_native=args.require_native, - require_wasix=args.require_wasix, - require_native_targets=require_native_targets, - ) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py index acc8c066..d34aaf41 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -741,8 +741,8 @@ def validate_ci_release_artifacts() -> None: "release CLI must verify staged exact-extension checksum manifests exactly", ) require_text( - "tools/release/build-extension-ci-artifacts.py", - "native_asset_name(product, version", + "tools/release/build-extension-ci-artifacts.mjs", + "nativeAssetName(product, version", "exact-extension package artifacts must be named by extension product version", ) require_text( @@ -761,32 +761,32 @@ def validate_ci_release_artifacts() -> None: "WASIX exact-extension artifact producers must support product-scoped builds", ) require_text( - "tools/release/build-extension-ci-artifacts.py", - "native_assets_from_target_indexes", + "tools/release/build-extension-ci-artifacts.mjs", + "nativeAssetsFromTargetIndexes", "exact-extension package staging must consume target-addressed native asset indexes", ) require_text( - "tools/release/build-extension-ci-artifacts.py", - 'published_target_ids(family="native")', + "tools/release/build-extension-ci-artifacts.mjs", + 'publishedTargetIds("native")', "exact-extension package staging must only read declared published native target artifact indexes", ) require_text( - "tools/release/build-extension-ci-artifacts.py", - 'published_target_ids(family="wasix")', + "tools/release/build-extension-ci-artifacts.mjs", + 'publishedTargetIds("wasix")', "exact-extension package staging must only read declared published WASIX target artifact indexes", ) require_text( - "tools/release/build-extension-ci-artifacts.py", - "if require_native_targets and target not in require_native_targets:", + "tools/release/build-extension-ci-artifacts.mjs", + "if (requireNativeTargets.size > 0 && !requireNativeTargets.has(target))", "mobile exact-extension package staging must filter out native targets that the mobile build did not request", ) require_text( - "tools/release/build-extension-ci-artifacts.py", - "index_contains_sql_name(product_index, sql_name)", + "tools/release/build-extension-ci-artifacts.mjs", + "indexContainsSqlName(productIndex, sqlName)", "exact-extension package staging must not let stale empty product-scoped native indexes shadow target-level indexes", ) require_text( - "tools/release/build-extension-ci-artifacts.py", + "tools/release/build-extension-ci-artifacts.mjs", "-manifest.json", "exact-extension package artifacts must publish a machine-readable release manifest", ) @@ -1011,7 +1011,7 @@ def validate_ci_release_artifacts() -> None: "src/runtimes/node-direct/tools/build-node-addon.sh", "src/extensions/artifacts/native/tools/package-release-assets.sh", "src/extensions/artifacts/wasix/tools/package-release-assets.sh", - "tools/release/build-extension-ci-artifacts.py", + "tools/release/build-extension-ci-artifacts.mjs", "src/sdks/kotlin/tools/check-sdk.sh", "src/sdks/react-native/tools/check-sdk.sh", "src/sdks/js/tools/check-sdk.sh", @@ -1147,7 +1147,7 @@ def validate_target_matrices() -> None: ) require_text( "src/extensions/artifacts/packages/moon.yml", - "tools/release/build-extension-ci-artifacts.py --all --require-native --require-wasix", + "tools/release/build-extension-ci-artifacts.mjs --all --require-native --require-wasix", "CI exact-extension package producer must use the shared product artifact builder", ) require_text( From 9fa76d34a7b79ecc5d71ca56171687bfdae1a96c Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 03:09:32 +0000 Subject: [PATCH 143/308] chore: port staged artifact check to bun --- .github/workflows/ci.yml | 4 +- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 22 +- docs/internal/IMPLEMENTATION_CHECKLIST.md | 32 +- .../tools/package-mobile-release-assets.sh | 2 +- tools/policy/check-release-policy.py | 4 +- .../check-sdk-mobile-extension-surface.sh | 6 +- tools/policy/python-entrypoints.allowlist | 1 - tools/release/build-sdk-ci-artifacts.sh | 2 +- tools/release/check-staged-artifacts.mjs | 1678 +++++++++++++++++ tools/release/check_artifact_targets.py | 12 +- tools/release/check_staged_artifacts.py | 1163 ------------ tools/release/release.py | 2 +- 12 files changed, 1729 insertions(+), 1199 deletions(-) create mode 100644 tools/release/check-staged-artifacts.mjs delete mode 100755 tools/release/check_staged_artifacts.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9cd88e1b..82332440 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1603,7 +1603,7 @@ jobs: OLIPHAUNT_CI_JOB_TARGETS_JSON='${{ needs.affected.outputs.job_targets }}' OLIPHAUNT_MOON_UPSTREAM=none MOON_CACHE=off .github/scripts/run-planned-moon-job.sh mobile-build-android - name: Validate Android mobile app artifacts - run: python3 tools/release/check_staged_artifacts.py --require-mobile android --require-mobile-prebuilt-extensions + run: tools/dev/bun.sh tools/release/check-staged-artifacts.mjs --require-mobile android --require-mobile-prebuilt-extensions - name: Upload Android mobile build logs if: ${{ always() }} @@ -1694,7 +1694,7 @@ jobs: OLIPHAUNT_CI_JOB_TARGETS_JSON='${{ needs.affected.outputs.job_targets }}' OLIPHAUNT_MOON_UPSTREAM=none MOON_CACHE=off .github/scripts/run-planned-moon-job.sh mobile-build-ios - name: Validate iOS mobile app artifacts - run: python3 tools/release/check_staged_artifacts.py --require-mobile ios --require-mobile-prebuilt-extensions + run: tools/dev/bun.sh tools/release/check-staged-artifacts.mjs --require-mobile ios --require-mobile-prebuilt-extensions - name: Upload iOS mobile build logs if: ${{ always() }} diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 0e54d955..2352dc0e 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,22 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Ported staged artifact validation from Python to Bun as + `tools/release/check-staged-artifacts.mjs`. CI mobile validation, SDK package + staging, release SDK validation, and mobile exact-extension package assembly + now call the pinned Bun launcher; the old Python entrypoint was removed from + the intentional Python inventory. Fresh parity/checks passed: the legacy + Python validator's `--inspect-present` mode before removal, + `tools/dev/bun.sh tools/release/check-staged-artifacts.mjs --inspect-present`, + `bash tools/policy/check-sdk-mobile-extension-surface.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`, `tools/dev/bun.sh + tools/policy/check-python-entrypoints.mjs --list`, `tools/release/release.py + check`, `bash tools/policy/check-tooling-stack.sh`, `bash + tools/policy/check-workflows.sh`, `bash tools/policy/check-repo-structure.sh`, + and `git diff --check --cached && git diff --check`. - 2026-06-27: Rechecked the root/tool crate split requested for PostgreSQL client tools. Native root runtime packages/crates are limited by `tools/release/native-runtime-payload-policy.json` to `initdb`, `pg_ctl`, and @@ -474,11 +490,11 @@ until the current-state gates here are checked with fresh local evidence. - 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 + per-SDK workflow booleans. `tools/dev/bun.sh tools/release/check-staged-artifacts.mjs` 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/release/check_artifact_targets.py`, `tools/dev/bun.sh + tools/release/check-staged-artifacts.mjs --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 diff --git a/docs/internal/IMPLEMENTATION_CHECKLIST.md b/docs/internal/IMPLEMENTATION_CHECKLIST.md index bead8288..fd99087c 100644 --- a/docs/internal/IMPLEMENTATION_CHECKLIST.md +++ b/docs/internal/IMPLEMENTATION_CHECKLIST.md @@ -191,7 +191,7 @@ or CI/build output proves the contract. Swift source archive for CocoaPods. - [x] Mobile build jobs inspect the produced app artifact for selected-extension correctness. Evidence: CI runs - `tools/release/check_staged_artifacts.py --require-mobile android + `tools/dev/bun.sh tools/release/check-staged-artifacts.mjs --require-mobile android --require-mobile-prebuilt-extensions` and the corresponding iOS command after app build, so the app package must contain only selected extension files and must have matching prebuilt exact-extension package inputs. @@ -202,7 +202,7 @@ or CI/build output proves the contract. unpacking exact-extension artifacts; `src/sdks/react-native/tools/expo-ios-runner.sh` stages generated registry C under compile-only `ios/generated/static-registry/`; and - `tools/release/check_staged_artifacts.py --require-mobile ios + `tools/dev/bun.sh tools/release/check-staged-artifacts.mjs --require-mobile ios --require-mobile-prebuilt-extensions` now requires Xcode link evidence for selected extension frameworks while rejecting build-only registry source or extension-framework inputs inside the final `.app` resource bundle. @@ -252,7 +252,7 @@ or CI/build output proves the contract. the package boundary. Evidence: `tools/release/build-sdk-ci-artifacts.sh` stages `target/sdk-artifacts/oliphaunt-kotlin/maven` only, React Native Android derives the Kotlin dependency from that staged Maven repo, and - `tools/release/check_staged_artifacts.py` now requires the Maven repository + `tools/dev/bun.sh tools/release/check-staged-artifacts.mjs` now requires the Maven repository instead of loose top-level AAR/JAR files. - [x] CI keeps build, check, test, and installed-app E2E phases separate. Evidence: `.github/workflows/ci.yml` has distinct `Checks`, `Tests`, `Builds`, @@ -279,7 +279,7 @@ or CI/build output proves the contract. `OLIPHAUNT_EXPO_ALLOW_NATIVE_BUILDS=0`, `OLIPHAUNT_EXPO_REQUIRE_SDK_ARTIFACTS=1`, and `OLIPHAUNT_EXPO_REQUIRE_PREBUILT_EXTENSIONS=1`; and run strict - `check_staged_artifacts.py --require-mobile-*-prebuilt-extensions` + `check-staged-artifacts.mjs --require-mobile-*-prebuilt-extensions` validation after app build. Android and iOS mobile builders now force release-mode app artifacts (`OLIPHAUNT_EXPO_ANDROID_BUILD_TYPE=release`, `OLIPHAUNT_EXPO_IOS_CONFIGURATION=Release`, and @@ -412,15 +412,15 @@ or CI/build output proves the contract. `oliphaunt-extension-vector-0.1.0-release-assets.sha256`. - [x] SDK package checks prove wrapper packages do not ship runtime or extension payloads. Evidence: - `tools/release/check_staged_artifacts.py --inspect-present` validates staged + `tools/dev/bun.sh tools/release/check-staged-artifacts.mjs --inspect-present` validates staged Swift, Kotlin, React Native, and TypeScript package artifacts, rejects runtime/share/static-registry payload leaks, and caught then removed a stale Kotlin debug AAR that embedded smoke runtime/vector assets. SDK staging now - runs `check_staged_artifacts.py --require-sdk-product "$product"` for every + runs `check-staged-artifacts.mjs --require-sdk-product "$product"` for every SDK product and stages only the Kotlin release AAR. - [x] Mobile app artifact checks prove unselected extension files do not enter app artifacts. Evidence: - `tools/release/check_staged_artifacts.py --require-mobile ios + `tools/dev/bun.sh tools/release/check-staged-artifacts.mjs --require-mobile ios --require-mobile-prebuilt-extensions` validates the fresh iOS `.app` built from staged React Native, Swift, liboliphaunt, and exact-extension artifacts; the checker binds the build report to the inspected app path, byte size, @@ -511,7 +511,7 @@ or CI/build output proves the contract. narrowed WASIX workspace package set so Cargo sees the same-release internal asset/AOT crates, stages only `oliphaunt-wasix-0.5.1.crate` plus package-file metadata under `target/sdk-artifacts/oliphaunt-wasix-rust`, and - `python3 tools/release/check_staged_artifacts.py --require-sdk-product + `tools/dev/bun.sh tools/release/check-staged-artifacts.mjs --require-sdk-product oliphaunt-wasix-rust` validates that the SDK artifact does not carry runtime payloads. @@ -609,7 +609,7 @@ Run before claiming this architecture complete: XCFramework zip has macOS, iOS device, and iOS simulator slices. This proves the Swift SDK package artifact path renders a checksum-pinned public `Package.swift.release`, stages `Oliphaunt-source.zip`, and passes - `python3 tools/release/check_staged_artifacts.py --require-sdk-product + `tools/dev/bun.sh tools/release/check-staged-artifacts.mjs --require-sdk-product 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 @@ -634,19 +634,19 @@ Run before claiming this architecture complete: `oliphaunt-extension-postgis` change with aggregate artifact/package tasks selects only `oliphaunt-extension-postgis`, emits 6 native rows, and emits 1 WASIX row. -- [x] `python3 tools/release/check_staged_artifacts.py +- [x] `tools/dev/bun.sh tools/release/check-staged-artifacts.mjs --require-sdk-product oliphaunt-rust` -- [x] `python3 tools/release/check_staged_artifacts.py +- [x] `tools/dev/bun.sh tools/release/check-staged-artifacts.mjs --require-sdk-product oliphaunt-kotlin` -- [x] `python3 tools/release/check_staged_artifacts.py +- [x] `tools/dev/bun.sh tools/release/check-staged-artifacts.mjs --require-sdk-product oliphaunt-swift` -- [x] `python3 tools/release/check_staged_artifacts.py +- [x] `tools/dev/bun.sh tools/release/check-staged-artifacts.mjs --require-sdk-product oliphaunt-react-native` -- [x] `python3 tools/release/check_staged_artifacts.py +- [x] `tools/dev/bun.sh tools/release/check-staged-artifacts.mjs --require-sdk-product oliphaunt-js` -- [x] `python3 tools/release/check_staged_artifacts.py +- [x] `tools/dev/bun.sh tools/release/check-staged-artifacts.mjs --require-sdk-product oliphaunt-wasix-rust` -- [x] `python3 tools/release/check_staged_artifacts.py --require-mobile ios +- [x] `tools/dev/bun.sh tools/release/check-staged-artifacts.mjs --require-mobile ios --require-mobile-prebuilt-extensions` passes after rebuilding `pnpm --dir src/sdks/react-native/examples/expo run mobile-build:ios` with staged SDK, native runtime, and exact-extension artifacts. The fresh app diff --git a/src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh b/src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh index 36905cae..f7be6f10 100755 --- a/src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh +++ b/src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh @@ -62,4 +62,4 @@ case " ${args[*]} " in esac tools/dev/bun.sh tools/release/build-extension-ci-artifacts.mjs "${args[@]}" -python3 tools/release/check_staged_artifacts.py "${validation_args[@]}" +tools/dev/bun.sh tools/release/check-staged-artifacts.mjs "${validation_args[@]}" diff --git a/tools/policy/check-release-policy.py b/tools/policy/check-release-policy.py index b64c7418..690bf770 100644 --- a/tools/policy/check-release-policy.py +++ b/tools/policy/check-release-policy.py @@ -770,8 +770,8 @@ def check_ci_policy() -> None: "React Native Android mobile build reports must include static-extension link evidence", ), ( - "tools/release/check_staged_artifacts.py", - "check_android_prebuilt_extension_linkage", + "tools/release/check-staged-artifacts.mjs", + "checkAndroidPrebuiltExtensionLinkage", "staged mobile artifact checks must validate Android static-extension link evidence", ), ): diff --git a/tools/policy/check-sdk-mobile-extension-surface.sh b/tools/policy/check-sdk-mobile-extension-surface.sh index 46a68d4b..0c641b7b 100755 --- a/tools/policy/check-sdk-mobile-extension-surface.sh +++ b/tools/policy/check-sdk-mobile-extension-surface.sh @@ -182,11 +182,11 @@ require_text src/sdks/react-native/tools/expo-ios-runner.sh "build-only static-r "React Native iOS build runner must reject build-only static-registry source in app resources" require_text src/sdks/react-native/tools/expo-ios-runner.sh "liboliphaunt_extension_[A-Za-z0-9_]+" \ "React Native iOS build runner must inspect selected extension framework link inputs" -require_text tools/release/check_staged_artifacts.py "check_ios_prebuilt_extension_linkage" \ +require_text tools/release/check-staged-artifacts.mjs "checkIosPrebuiltExtensionLinkage" \ "staged mobile artifact checks must verify iOS selected extension link evidence" -require_text tools/release/check_staged_artifacts.py "static-registry/oliphaunt_static_registry.c" \ +require_text tools/release/check-staged-artifacts.mjs "static-registry/oliphaunt_static_registry.c" \ "staged mobile artifact checks must reject build-only static-registry source in iOS app resources" -require_text tools/release/check_staged_artifacts.py "liboliphaunt_extension_[A-Za-z0-9_]+" \ +require_text tools/release/check-staged-artifacts.mjs "liboliphaunt_extension_[A-Za-z0-9_]+" \ "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" diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index 384cc4ea..0dfe9be5 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -6,7 +6,6 @@ tools/release/artifact_targets.py tools/release/check_artifact_targets.py tools/release/check_consumer_shape.py tools/release/check_release_metadata.py -tools/release/check_staged_artifacts.py tools/release/extension_artifact_targets.py tools/release/local_registry_publish.py tools/release/package_liboliphaunt_cargo_artifacts.py diff --git a/tools/release/build-sdk-ci-artifacts.sh b/tools/release/build-sdk-ci-artifacts.sh index 9ffd30e0..91c1b54d 100755 --- a/tools/release/build-sdk-ci-artifacts.sh +++ b/tools/release/build-sdk-ci-artifacts.sh @@ -190,5 +190,5 @@ esac find "$artifact_root" -mindepth 1 -maxdepth 1 \( -type f -o -type d \) -print | sort >"$artifact_root/artifacts.txt" [ -s "$artifact_root/artifacts.txt" ] || fail "no SDK artifacts were staged for $product" -python3 tools/release/check_staged_artifacts.py --require-sdk-product "$product" +tools/dev/bun.sh tools/release/check-staged-artifacts.mjs --require-sdk-product "$product" printf 'Staged %s SDK artifacts under %s\n' "$product" "$artifact_root" diff --git a/tools/release/check-staged-artifacts.mjs b/tools/release/check-staged-artifacts.mjs new file mode 100644 index 00000000..4bb48e30 --- /dev/null +++ b/tools/release/check-staged-artifacts.mjs @@ -0,0 +1,1678 @@ +#!/usr/bin/env bun +import { createHash } from "node:crypto"; +import { + existsSync, + readdirSync, + readFileSync, + statSync, +} from "node:fs"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { inflateRawSync } from "node:zlib"; + +import { + ROOT, + compareText, + currentProductVersion, + exactExtensionProducts, + extensionArtifactTargets, +} from "./release-artifact-targets.mjs"; +import { loadGraph } from "./release-graph.mjs"; + +const PREFIX = "check-staged-artifacts.mjs"; +const SDK_ROOT = path.join(ROOT, "target/sdk-artifacts"); +const EXTENSION_ROOT = path.join(ROOT, "target/extension-artifacts"); +const MOBILE_ROOT = path.join(ROOT, "target/mobile-build/react-native"); + +const WASIX_RUNTIME_PACKAGE = "liboliphaunt-wasix-portable"; +const WASIX_TOOLS_PACKAGE = "oliphaunt-wasix-tools"; +const ICU_PACKAGE = "oliphaunt-icu"; +const WASIX_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", +}; +const WASIX_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", +}; +const WASIX_AOT_TARGET_TRIPLES = { + "macos-arm64": "aarch64-apple-darwin", + "linux-arm64-gnu": "aarch64-unknown-linux-gnu", + "linux-x64-gnu": "x86_64-unknown-linux-gnu", + "windows-x64-msvc": "x86_64-pc-windows-msvc", +}; +const WASIX_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"))', +}; + +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", +]); +const PUBLIC_EXTENSION_RELEASE_ASSET_KEY_ORDER = [ + "name", + "family", + "target", + "kind", + "sha256", + "bytes", +]; +const SDK_RUNTIME_PAYLOAD_PATTERNS = [ + /(^|\/)assets\/oliphaunt\/runtime\//u, + /(^|\/)assets\/oliphaunt\/template-pgdata\//u, + /(^|\/)assets\/oliphaunt\/static-registry\/archives\//u, + /(^|\/)oliphaunt\/runtime\/files\//u, + /(^|\/)runtime\/files\/share\/postgresql\//u, + /(^|\/)share\/postgresql\/extension\/[^/]+\.(control|sql)$/u, + /(^|\/)release-assets\//u, + /(^|\/)extension-artifacts\.json$/u, + /(^|\/)liboliphaunt\.(so|dylib|dll|a|lib)$/u, + /(^|\/)liboliphaunt_extensions\.(so|dylib|dll|a|lib)$/u, + /(^|\/)liboliphaunt_extension_[^/]+\.(so|dylib|dll|a|lib)$/u, + /\.xcframework(\/|$)/u, +]; +const KOTLIN_ALLOWED_NATIVE_PAYLOADS = new Set(["liboliphaunt_kotlin_android.so"]); +const KOTLIN_RELEASE_ABIS = new Set(["arm64-v8a", "x86_64"]); +const BASELINE_POSTGRES_EXTENSIONS = new Set(["plpgsql"]); +const EXTENSION_VERSIONING_BY_CLASS = { + contrib: "postgres-bound", + external: "upstream-bound", + "first-party": "repo-bound", +}; +const EXTENSION_RUNTIME_CONTRACT_PATH = "src/shared/extension-runtime-contract/contract.toml"; +const POSTGRES18_SOURCE_PATH = "src/postgres/versions/18/source.toml"; + +function fail(message) { + console.error(`${PREFIX}: ${message}`); + process.exit(1); +} + +function rel(file) { + const relative = path.relative(ROOT, String(file)); + if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) { + return String(file).split(path.sep).join("/"); + } + return relative.split(path.sep).join("/"); +} + +function isFile(file) { + try { + return statSync(file).isFile(); + } catch { + return false; + } +} + +function isDirectory(file) { + try { + return statSync(file).isDirectory(); + } catch { + return false; + } +} + +function sha256File(file) { + return createHash("sha256").update(readFileSync(file)).digest("hex"); +} + +function readJson(file) { + let data; + try { + data = JSON.parse(readFileSync(file, "utf8")); + } catch (error) { + fail(`${rel(file)} is not valid JSON: ${error.message}`); + } + if (data === null || Array.isArray(data) || typeof data !== "object") { + fail(`${rel(file)} must contain a JSON object`); + } + return data; +} + +function readPropertiesText(text) { + const parsed = {}; + for (const raw of text.split(/\r?\n/u)) { + const line = raw.trim(); + if (!line || line.startsWith("#")) { + continue; + } + const equals = line.indexOf("="); + if (equals < 0) { + fail(`invalid properties line: ${JSON.stringify(raw)}`); + } + parsed[line.slice(0, equals)] = line.slice(equals + 1); + } + return parsed; +} + +function csvValues(value) { + if (!value) { + return []; + } + return String(value).split(",").map((item) => item.trim()).filter(Boolean); +} + +function runCapture(command, args, label) { + const result = spawnSync(command, args, { + cwd: ROOT, + encoding: "buffer", + maxBuffer: 100 * 1024 * 1024, + }); + if (result.status !== 0) { + const stderr = result.stderr.toString("utf8").trim(); + fail(`${label} failed${stderr ? `: ${stderr}` : ""}`); + } + return result.stdout; +} + +function archiveTarNames(file) { + const output = runCapture("tar", ["-tf", file], `${rel(file)} tar listing`).toString("utf8"); + return output.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line && !line.endsWith("/")).sort(compareText); +} + +function tarReadText(file, member) { + return runCapture("tar", ["-xOf", file, member], `${rel(file)} ${member}`).toString("utf8"); +} + +function cargoCrateManifest(file) { + const manifests = archiveTarNames(file).filter((name) => name.split("/").length === 2 && name.endsWith("/Cargo.toml")); + if (manifests.length !== 1) { + fail(`${rel(file)} must contain exactly one top-level Cargo.toml`); + } + let data; + try { + data = Bun.TOML.parse(tarReadText(file, manifests[0])); + } catch (error) { + fail(`${rel(file)} contains an invalid Cargo.toml: ${error.message}`); + } + if (data === null || Array.isArray(data) || typeof data !== "object") { + fail(`${rel(file)} Cargo.toml must contain a TOML table`); + } + return data; +} + +function checkedArchiveMember(name, archive) { + const normalized = name.replaceAll("\\", "/"); + const parts = normalized.split("/").filter((part) => part && part !== "."); + if (parts.length === 0) { + return null; + } + if (normalized.startsWith("/") || parts.includes("..")) { + fail(`${rel(archive)} contains unsafe archive member ${JSON.stringify(name)}`); + } + return parts.join("/"); +} + +function findEndOfCentralDirectory(buffer, file) { + for (let offset = buffer.length - 22; offset >= Math.max(0, buffer.length - 65557); offset -= 1) { + if (buffer.readUInt32LE(offset) === 0x06054b50) { + return offset; + } + } + fail(`${rel(file)} is missing zip end of central directory`); +} + +function zipEntryData(buffer, file, offset, compressedSize, method) { + if (buffer.readUInt32LE(offset) !== 0x04034b50) { + fail(`${rel(file)} has an invalid zip local file header`); + } + const nameLength = buffer.readUInt16LE(offset + 26); + const extraLength = buffer.readUInt16LE(offset + 28); + const dataStart = offset + 30 + nameLength + extraLength; + const compressed = buffer.subarray(dataStart, dataStart + compressedSize); + if (method === 0) { + return compressed; + } + if (method === 8) { + return inflateRawSync(compressed); + } + fail(`${rel(file)} contains unsupported zip compression method ${method}`); +} + +function readZipEntries(file) { + const buffer = readFileSync(file); + const eocd = findEndOfCentralDirectory(buffer, file); + 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(`${rel(file)} has an invalid zip central directory`); + } + const method = buffer.readUInt16LE(offset + 10); + const compressedSize = buffer.readUInt32LE(offset + 20); + 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 localOffset = buffer.readUInt32LE(offset + 42); + const rawName = buffer.subarray(offset + 46, offset + 46 + nameLength).toString("utf8"); + const name = checkedArchiveMember(rawName, file); + if (name) { + entries.set(name, { + size, + isFile: !rawName.endsWith("/") && (externalAttributes & 0x10) === 0, + isDirectory: rawName.endsWith("/") || (externalAttributes & 0x10) !== 0, + data: () => zipEntryData(buffer, file, localOffset, compressedSize, method), + }); + } + offset += 46 + nameLength + extraLength + commentLength; + } + return entries; +} + +function archiveZipNames(file) { + return [...readZipEntries(file)] + .filter(([, entry]) => entry.isFile) + .map(([name]) => name) + .sort(compareText); +} + +function zipReadText(file, name) { + const entry = readZipEntries(file).get(name); + if (!entry || !entry.isFile) { + fail(`${rel(file)} is missing ${name}`); + } + try { + return Buffer.from(entry.data()).toString("utf8"); + } catch (error) { + fail(`${rel(file)} member ${name} is not readable UTF-8: ${error.message}`); + } +} + +function validateZstdArchiveMagic(file) { + if (!readFileSync(file).subarray(0, 4).equals(Buffer.from([0x28, 0xb5, 0x2f, 0xfd]))) { + fail(`${rel(file)} is not a zstd archive`); + } +} + +function validateReleaseArchivePayload(file) { + if (file.endsWith(".tar.gz") || file.endsWith(".tgz") || file.endsWith(".crate")) { + if (archiveTarNames(file).length === 0) { + fail(`${rel(file)} must contain at least one file`); + } + return; + } + if (file.endsWith(".zip") || file.endsWith(".aar") || file.endsWith(".jar")) { + if (archiveZipNames(file).length === 0) { + fail(`${rel(file)} must contain at least one file`); + } + return; + } + if (file.endsWith(".tar.zst")) { + validateZstdArchiveMagic(file); + } +} + +function directoryNames(root) { + const result = []; + const visit = (dir) => { + if (!isDirectory(dir)) { + return; + } + for (const name of readdirSync(dir).sort(compareText)) { + const file = path.join(dir, name); + if (isDirectory(file)) { + visit(file); + } else if (isFile(file)) { + result.push(relFrom(root, file)); + } + } + }; + visit(root); + return result.sort(compareText); +} + +function relFrom(root, file) { + return path.relative(root, file).split(path.sep).join("/"); +} + +function pathBytes(file) { + if (isFile(file)) { + return statSync(file).size; + } + if (isDirectory(file)) { + let total = 0; + for (const name of directoryNames(file)) { + total += statSync(path.join(file, ...name.split("/"))).size; + } + return total; + } + fail(`missing path while measuring bytes: ${rel(file)}`); +} + +function dirReadText(root, name) { + const file = path.join(root, ...name.split("/")); + if (!isFile(file)) { + fail(`${rel(root)} is missing ${name}`); + } + return readFileSync(file, "utf8"); +} + +function graphProducts() { + return loadGraph(PREFIX).products; +} + +function productConfig(product) { + const config = graphProducts()[product]; + if (!config) { + fail(`unknown release product ${product}`); + } + return config; +} + +function sdkProducts() { + return Object.entries(graphProducts()) + .filter(([, config]) => config.kind === "sdk") + .map(([product]) => product) + .sort(compareText); +} + +const versionCache = new Map(); + +function currentProductVersionSync(product) { + if (!versionCache.has(product)) { + const versionFile = productConfig(product).version_files?.[0]; + if (typeof versionFile !== "string" || !versionFile) { + fail(`${product} does not declare a canonical version file`); + } + const file = path.join(ROOT, versionFile); + const text = readFileSync(file, "utf8"); + const name = path.basename(file); + let version = ""; + if (name === "Cargo.toml") { + 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; + } + const match = inPackage ? /^version\s*=\s*"([^"]+)"/u.exec(line) : null; + if (match) { + version = match[1]; + break; + } + } + } else if (name === "package.json" || name === "jsr.json") { + const data = JSON.parse(text); + version = typeof data.version === "string" ? data.version : ""; + } else if (name === "gradle.properties") { + for (const rawLine of text.split(/\r?\n/u)) { + const line = rawLine.trim(); + if (!line || line.startsWith("#") || !line.includes("=")) { + continue; + } + const [key, ...rest] = line.split("="); + if (key.trim() === "VERSION_NAME") { + version = rest.join("=").trim(); + break; + } + } + } else if (name === "VERSION" || name === "LIBOLIPHAUNT_VERSION") { + version = text.trim(); + } else { + fail(`${product}.version_files has unsupported version file type: ${versionFile}`); + } + if (!version) { + fail(`${versionFile} does not define a release version for ${product}`); + } + versionCache.set(product, version); + } + return versionCache.get(product); +} + +function nonEmptyString(value, context) { + if (typeof value === "string" && value.length > 0) { + return value; + } + fail(`${context} must be a non-empty string`); +} + +function releaseMetadataRelativePath(value, context) { + const candidate = path.normalize(value).split(path.sep).join("/"); + if (path.isAbsolute(value) || candidate.split("/").includes("..")) { + fail(`${context} must be a repository-relative path: ${JSON.stringify(value)}`); + } + if (!existsSync(path.join(ROOT, candidate))) { + fail(`${context} path does not exist: ${candidate}`); + } + return candidate; +} + +function packagePath(product) { + return releaseMetadataRelativePath(nonEmptyString(productConfig(product).path, `${product}.path`), `${product}.path`); +} + +function extensionMetadata(product) { + const config = productConfig(product); + if (config.kind !== "exact-extension-artifact") { + fail(`${product} is not an exact-extension artifact product`); + } + const topLevelSqlName = nonEmptyString(config.extension_sql_name, `${product}.extension_sql_name`); + const metadata = config.extension; + if (metadata === null || Array.isArray(metadata) || typeof metadata !== "object") { + fail(`${product} release metadata must declare [extension]`); + } + const sqlName = nonEmptyString(metadata.sql_name, `${product}.extension.sql_name`); + if (sqlName !== topLevelSqlName) { + fail(`${product}.extension.sql_name ${JSON.stringify(sqlName)} must match extension_sql_name ${JSON.stringify(topLevelSqlName)}`); + } + const extensionClass = nonEmptyString(metadata.class, `${product}.extension.class`); + if (!(extensionClass in EXTENSION_VERSIONING_BY_CLASS)) { + fail(`${product}.extension.class must be one of ${Object.keys(EXTENSION_VERSIONING_BY_CLASS).sort(compareText).join(", ")}`); + } + const versioning = nonEmptyString(metadata.versioning, `${product}.extension.versioning`); + const expectedVersioning = EXTENSION_VERSIONING_BY_CLASS[extensionClass]; + if (versioning !== expectedVersioning) { + fail(`${product}.extension.versioning must be ${JSON.stringify(expectedVersioning)} for class ${JSON.stringify(extensionClass)}, got ${JSON.stringify(versioning)}`); + } + const source = metadata.source; + if (source === null || Array.isArray(source) || typeof source !== "object") { + fail(`${product}.extension must declare [extension.source]`); + } + const sourcePath = releaseMetadataRelativePath(nonEmptyString(source.path, `${product}.extension.source.path`), `${product}.extension.source.path`); + const packageRoot = packagePath(product); + if (extensionClass === "contrib" && sourcePath !== POSTGRES18_SOURCE_PATH) { + fail(`${product}.extension.source.path must be ${JSON.stringify(POSTGRES18_SOURCE_PATH)} for contrib extensions`); + } + if (extensionClass === "external" && sourcePath !== `${packageRoot}/source.toml`) { + fail(`${product}.extension.source.path must be ${packageRoot}/source.toml for external extensions`); + } + if (extensionClass === "first-party" && !(sourcePath === packageRoot || sourcePath.startsWith(`${packageRoot}/`))) { + fail(`${product}.extension.source.path must stay inside ${packageRoot}/ for first-party extensions`); + } + const compatibility = metadata.compatibility; + if (compatibility === null || Array.isArray(compatibility) || typeof compatibility !== "object") { + fail(`${product}.extension must declare [extension.compatibility]`); + } + const postgresMajor = nonEmptyString(compatibility.postgres_major, `${product}.extension.compatibility.postgres_major`); + if (postgresMajor !== "18") { + fail(`${product}.extension.compatibility.postgres_major must be '18', got ${JSON.stringify(postgresMajor)}`); + } + const contractPath = releaseMetadataRelativePath( + nonEmptyString(compatibility.extension_runtime_contract, `${product}.extension.compatibility.extension_runtime_contract`), + `${product}.extension.compatibility.extension_runtime_contract`, + ); + if (contractPath !== EXTENSION_RUNTIME_CONTRACT_PATH) { + fail(`${product}.extension.compatibility.extension_runtime_contract must be ${JSON.stringify(EXTENSION_RUNTIME_CONTRACT_PATH)}`); + } + const nativeProduct = nonEmptyString(compatibility.native_runtime_product, `${product}.extension.compatibility.native_runtime_product`); + const wasixProduct = nonEmptyString(compatibility.wasix_runtime_product, `${product}.extension.compatibility.wasix_runtime_product`); + if (nativeProduct !== "liboliphaunt-native") { + fail(`${product}.extension.compatibility.native_runtime_product must be 'liboliphaunt-native'`); + } + if (wasixProduct !== "liboliphaunt-wasix") { + fail(`${product}.extension.compatibility.wasix_runtime_product must be 'liboliphaunt-wasix'`); + } + const nativeVersion = nonEmptyString(compatibility.native_runtime_version, `${product}.extension.compatibility.native_runtime_version`); + const wasixVersion = nonEmptyString(compatibility.wasix_runtime_version, `${product}.extension.compatibility.wasix_runtime_version`); + const expectedNativeVersion = currentProductVersionSync(nativeProduct); + const expectedWasixVersion = currentProductVersionSync(wasixProduct); + if (nativeVersion !== expectedNativeVersion) { + fail(`${product}.extension.compatibility.native_runtime_version must be ${JSON.stringify(expectedNativeVersion)}, got ${JSON.stringify(nativeVersion)}`); + } + if (wasixVersion !== expectedWasixVersion) { + fail(`${product}.extension.compatibility.wasix_runtime_version must be ${JSON.stringify(expectedWasixVersion)}, got ${JSON.stringify(wasixVersion)}`); + } + return { + sqlName, + class: extensionClass, + versioning, + sourcePath, + compatibility: { + postgresMajor, + extensionRuntimeContract: contractPath, + nativeRuntimeProduct: nativeProduct, + nativeRuntimeVersion: nativeVersion, + wasixRuntimeProduct: wasixProduct, + wasixRuntimeVersion: wasixVersion, + }, + }; +} + +function extensionSourceIdentity(product) { + const metadata = extensionMetadata(product); + const source = Bun.TOML.parse(readFileSync(path.join(ROOT, metadata.sourcePath), "utf8")); + if (metadata.class === "contrib") { + const postgresql = source.postgresql; + if (postgresql === null || Array.isArray(postgresql) || typeof postgresql !== "object") { + fail(`${metadata.sourcePath} must declare [postgresql] for contrib extension products`); + } + return { + kind: "postgres-contrib", + name: "postgresql", + version: nonEmptyString(postgresql.version, `${metadata.sourcePath}.postgresql.version`), + url: nonEmptyString(postgresql.url, `${metadata.sourcePath}.postgresql.url`), + sha256: nonEmptyString(postgresql.sha256, `${metadata.sourcePath}.postgresql.sha256`), + }; + } + if (metadata.class === "external") { + return { + kind: "external", + name: nonEmptyString(source.name, `${metadata.sourcePath}.name`), + url: nonEmptyString(source.url, `${metadata.sourcePath}.url`), + branch: nonEmptyString(source.branch, `${metadata.sourcePath}.branch`), + commit: nonEmptyString(source.commit, `${metadata.sourcePath}.commit`), + }; + } + if (metadata.class === "first-party") { + return { + kind: "repo", + name: metadata.sqlName, + path: metadata.sourcePath, + version: currentProductVersionSync(product), + }; + } + fail(`${product}.extension.class has unsupported source identity class ${JSON.stringify(metadata.class)}`); +} + +function publicAotCargoDependencies() { + return Object.fromEntries( + Object.entries(WASIX_AOT_PACKAGES).map(([target, name]) => [ + WASIX_AOT_TARGET_CFGS[WASIX_AOT_TARGET_TRIPLES[target]], + name, + ]), + ); +} + +function publicToolsAotCargoDependencies() { + return Object.fromEntries( + Object.entries(WASIX_TOOLS_AOT_PACKAGES).map(([target, name]) => [ + WASIX_AOT_TARGET_CFGS[WASIX_AOT_TARGET_TRIPLES[target]], + name, + ]), + ); +} + +async function validateWasixSdkCrate(crate) { + const manifest = cargoCrateManifest(crate); + const packageConfig = manifest.package; + if (packageConfig === null || Array.isArray(packageConfig) || typeof packageConfig !== "object" || packageConfig.name !== "oliphaunt-wasix") { + fail(`${rel(crate)} must package the oliphaunt-wasix crate`); + } + const runtimeVersion = await currentProductVersion("liboliphaunt-wasix", PREFIX); + const dependencies = manifest.dependencies; + if (dependencies === null || Array.isArray(dependencies) || typeof dependencies !== "object") { + fail(`${rel(crate)} must declare Cargo dependencies`); + } + for (const name of [WASIX_RUNTIME_PACKAGE, WASIX_TOOLS_PACKAGE, ICU_PACKAGE].sort(compareText)) { + const dependency = dependencies[name]; + if (dependency === null || Array.isArray(dependency) || typeof dependency !== "object" || dependency.version !== `=${runtimeVersion}` || "path" in dependency) { + fail(`${rel(crate)} dependency ${name} must use registry version =${runtimeVersion} without a path`); + } + } + const targetTables = manifest.target; + if (targetTables === null || Array.isArray(targetTables) || typeof targetTables !== "object") { + fail(`${rel(crate)} must declare target-specific WASIX AOT dependencies`); + } + const expectedTargets = new Map(); + for (const [cfg, name] of Object.entries(publicAotCargoDependencies())) { + if (!expectedTargets.has(cfg)) { + expectedTargets.set(cfg, []); + } + expectedTargets.get(cfg).push(name); + } + for (const [cfg, name] of Object.entries(publicToolsAotCargoDependencies())) { + if (!expectedTargets.has(cfg)) { + expectedTargets.set(cfg, []); + } + expectedTargets.get(cfg).push(name); + } + for (const [cfg, crates] of [...expectedTargets].sort(([left], [right]) => compareText(left, right))) { + const target = targetTables[cfg]; + const targetDependencies = target && typeof target === "object" && !Array.isArray(target) ? (target.dependencies ?? {}) : {}; + for (const name of crates.sort(compareText)) { + const dependency = targetDependencies[name]; + if (dependency === null || Array.isArray(dependency) || typeof dependency !== "object" || dependency.version !== `=${runtimeVersion}` || "path" in dependency) { + fail(`${rel(crate)} target dependency ${cfg}:${name} must use registry version =${runtimeVersion} without a path`); + } + } + } +} + +function generatedExtensionRows() { + const metadata = path.join(ROOT, "src/extensions/generated/sdk/react-native.json"); + const data = readJson(metadata); + const rows = data.extensions; + if (!Array.isArray(rows)) { + fail(`${rel(metadata)} must contain an extensions array`); + } + const result = new Map(); + for (const row of rows) { + if (row && typeof row === "object" && !Array.isArray(row)) { + const sqlName = row["sql-name"]; + if (typeof sqlName === "string" && sqlName) { + result.set(sqlName, row); + } + } + } + return result; +} + +function createsExtension(sqlName, rows) { + const row = rows.get(sqlName); + if (!row) { + fail(`selected extension ${JSON.stringify(sqlName)} is missing from generated extension metadata`); + } + return row["creates-extension"] !== false; +} + +function nativeModuleStem(sqlName, rows) { + const row = rows.get(sqlName); + if (!row) { + fail(`selected extension ${JSON.stringify(sqlName)} is missing from generated extension metadata`); + } + return typeof row["native-module-stem"] === "string" ? row["native-module-stem"] : ""; +} + +function nativeModuleExtensions(selected, rows) { + return selected + .filter((extension) => { + const stem = nativeModuleStem(extension, rows); + return stem && stem !== "-"; + }) + .sort(compareText); +} + +function extensionNameForAsset(pathName) { + const name = path.basename(pathName); + if (name.endsWith(".control")) { + return name.slice(0, -".control".length); + } + if (name.includes("--") && name.endsWith(".sql")) { + return name.split("--", 1)[0]; + } + return null; +} + +function rejectSdkRuntimePayload(product, artifact, names) { + for (const name of names) { + const basename = path.basename(name); + if (product === "oliphaunt-kotlin" && KOTLIN_ALLOWED_NATIVE_PAYLOADS.has(basename)) { + continue; + } + for (const pattern of SDK_RUNTIME_PAYLOAD_PATTERNS) { + if (pattern.test(name)) { + fail(`${product} SDK artifact ${rel(artifact)} must not include runtime/extension payload ${name}`); + } + } + } +} + +function validateKotlinAndroidAar(artifact, names) { + const presentAbis = new Set( + names + .map((name) => name.split("/")) + .filter((parts) => parts.length === 3 && parts[0] === "jni" && parts[2] === "liboliphaunt_kotlin_android.so") + .map((parts) => parts[1]), + ); + if (presentAbis.size !== KOTLIN_RELEASE_ABIS.size || [...presentAbis].some((abi) => !KOTLIN_RELEASE_ABIS.has(abi))) { + fail( + `Kotlin Android release AAR ${rel(artifact)} must contain JNI adapters for ` + + `${[...KOTLIN_RELEASE_ABIS].sort(compareText).join(", ")}; got ${[...presentAbis].sort(compareText).join(", ") || "(none)"}`, + ); + } +} + +async function checkSdkProduct(product, { require }) { + const root = path.join(SDK_ROOT, product); + if (!existsSync(root)) { + if (require) { + fail(`missing staged SDK artifacts for ${product} under ${rel(root)}`); + } + return false; + } + let checked = false; + if (["oliphaunt-js", "oliphaunt-react-native"].includes(product)) { + const tarballs = readdirSync(root).filter((name) => name.endsWith(".tgz")).map((name) => path.join(root, name)).sort(compareText); + if (tarballs.length === 0 && require) { + fail(`${product} must stage an npm tarball under ${rel(root)}`); + } + for (const tarball of tarballs) { + rejectSdkRuntimePayload(product, tarball, archiveTarNames(tarball)); + checked = true; + } + } else if (product === "oliphaunt-swift") { + const archives = readdirSync(root).filter((name) => name.endsWith(".zip")).map((name) => path.join(root, name)).sort(compareText); + if (archives.length === 0 && require) { + fail(`${product} must stage a source zip under ${rel(root)}`); + } + for (const archive of archives) { + rejectSdkRuntimePayload(product, archive, archiveZipNames(archive)); + checked = true; + } + const releaseManifest = path.join(root, "Package.swift.release"); + if (!existsSync(releaseManifest) && require) { + fail(`${product} must stage ${rel(releaseManifest)} for release installation`); + } + if (existsSync(releaseManifest)) { + const text = readFileSync(releaseManifest, "utf8"); + if (text.includes("file://")) { + fail(`${rel(releaseManifest)} must not contain local file URLs`); + } + if (!text.includes("liboliphaunt-native-v") || !text.includes("checksum:")) { + fail(`${rel(releaseManifest)} must reference checksummed public liboliphaunt assets`); + } + } + } else if (product === "oliphaunt-kotlin") { + const mavenRoot = path.join(root, "maven"); + if (!isDirectory(mavenRoot)) { + if (require) { + fail(`${product} must stage a Maven repository under ${rel(mavenRoot)}`); + } + return false; + } + for (const archive of walkFiles(root).filter((file) => file.endsWith(".aar") || file.endsWith(".jar")).sort(compareText)) { + const names = archiveZipNames(archive); + rejectSdkRuntimePayload(product, archive, names); + if (archive.endsWith(".aar")) { + validateKotlinAndroidAar(archive, names); + } + checked = true; + } + } else if (product === "oliphaunt-rust") { + const crates = readdirSync(root).filter((name) => name.endsWith(".crate")).map((name) => path.join(root, name)).sort(compareText); + if (crates.length === 0 && require) { + fail(`${product} must stage a Cargo crate under ${rel(root)}`); + } + for (const crate of crates) { + rejectSdkRuntimePayload(product, crate, archiveTarNames(crate)); + checked = true; + } + } else if (product === "oliphaunt-wasix-rust") { + const crates = readdirSync(root).filter((name) => name.endsWith(".crate")).map((name) => path.join(root, name)).sort(compareText); + if (crates.length === 0 && require) { + fail(`${product} must stage a Cargo crate under ${rel(root)}`); + } + for (const crate of crates) { + rejectSdkRuntimePayload(product, crate, archiveTarNames(crate)); + await validateWasixSdkCrate(crate); + checked = true; + } + const listing = path.join(root, "cargo-package-files.txt"); + if (!isFile(listing)) { + if (require) { + fail(`${product} must stage a Cargo package file list under ${rel(root)}`); + } + return false; + } + const entries = new Set(readFileSync(listing, "utf8").split(/\r?\n/u).map((line) => line.trim()).filter(Boolean)); + for (const requiredEntry of [ + "Cargo.toml", + "README.md", + "src/lib.rs", + "src/bin/oliphaunt_wasix_dump.rs", + "src/bin/oliphaunt_wasix_proxy.rs", + "src/oliphaunt/assets.rs", + ]) { + if (!entries.has(requiredEntry)) { + fail(`${product} package file list is missing ${requiredEntry}`); + } + } + for (const entry of entries) { + if (entry.startsWith("target/") || entry.startsWith("src/runtimes/") || entry.startsWith("src/extensions/generated/")) { + fail(`${product} package file list contains generated or external payload entry ${entry}`); + } + } + checked = true; + } else { + fail(`unsupported SDK product ${product}`); + } + if (require && !checked) { + fail(`${product} did not contain any inspectable staged package artifacts under ${rel(root)}`); + } + if (checked) { + console.log(`validated SDK artifact cleanliness: ${product}`); + } + return checked; +} + +function walkFiles(root) { + if (!isDirectory(root)) { + return []; + } + const result = []; + const visit = (dir) => { + for (const name of readdirSync(dir).sort(compareText)) { + const file = path.join(dir, name); + if (isDirectory(file)) { + visit(file); + } else if (isFile(file)) { + result.push(file); + } + } + }; + visit(root); + return result; +} + +function extensionArtifactKindAllowed(family, target, kind) { + if (family === "wasix") { + return target === "wasix-portable" && kind === "wasix-runtime"; + } + if (family !== "native") { + return false; + } + if (target === "ios-xcframework") { + return new Set(["runtime", "ios-xcframework"]).has(kind); + } + if (target.startsWith("android-")) { + return new Set(["runtime", "android-static-archive"]).has(kind); + } + return kind === "runtime"; +} + +function publicExtensionAsset(asset) { + const result = {}; + for (const key of PUBLIC_EXTENSION_RELEASE_ASSET_KEY_ORDER) { + if (Object.hasOwn(asset, key)) { + result[key] = asset[key]; + } + } + return result; +} + +async function checkExtensionProduct(product, { require, requireFullTargets }) { + const root = path.join(EXTENSION_ROOT, product); + const manifest = path.join(root, "extension-artifacts.json"); + if (!existsSync(manifest)) { + if (require) { + fail(`missing staged exact-extension package manifest for ${product} under ${rel(root)}`); + } + return false; + } + const data = readJson(manifest); + const expected = { + schema: "oliphaunt-extension-ci-artifacts-v1", + product, + version: await currentProductVersion(product, PREFIX), + }; + for (const [key, value] of Object.entries(expected)) { + if (data[key] !== value) { + fail(`${rel(manifest)} has ${key}=${JSON.stringify(data[key])}, expected ${JSON.stringify(value)}`); + } + } + const expectedSqlName = productConfig(product).extension_sql_name; + if (data.sqlName !== expectedSqlName) { + fail(`${rel(manifest)} has sqlName=${JSON.stringify(data.sqlName)}, expected ${JSON.stringify(expectedSqlName)}`); + } + const assets = data.assets; + if (!Array.isArray(assets) || assets.length === 0) { + fail(`${rel(manifest)} must declare at least one asset`); + } + const seenNames = new Set(); + const stagedTargets = new Set(); + const allowedTargets = new Set(extensionArtifactTargets({ product, publishedOnly: true }, PREFIX).map((target) => target.target)); + for (const asset of assets) { + if (asset === null || Array.isArray(asset) || typeof asset !== "object") { + fail(`${rel(manifest)} contains a non-object asset entry`); + } + const { family, target, kind, name, path: pathValue, sha256, bytes } = asset; + if (![family, target, kind, name, pathValue, sha256].every((value) => typeof value === "string" && value)) { + fail(`${rel(manifest)} contains an incomplete asset entry: ${JSON.stringify(asset)}`); + } + if (!Number.isInteger(bytes) || bytes <= 0) { + fail(`${rel(manifest)} asset ${name} must declare positive bytes`); + } + if (seenNames.has(name)) { + fail(`${rel(manifest)} declares duplicate asset name ${name}`); + } + seenNames.add(name); + stagedTargets.add(target); + if (!allowedTargets.has(target)) { + fail(`${rel(manifest)} stages undeclared target=${JSON.stringify(target)}`); + } + if (!extensionArtifactKindAllowed(family, target, kind)) { + fail(`${rel(manifest)} stages invalid artifact kind=${JSON.stringify(kind)} for family=${JSON.stringify(family)} target=${JSON.stringify(target)}`); + } + const assetPath = path.join(ROOT, pathValue); + if (path.dirname(assetPath) !== path.join(root, "release-assets") || path.basename(assetPath) !== name) { + fail(`${rel(manifest)} asset ${name} must live directly under ${rel(path.join(root, "release-assets"))}`); + } + if (!isFile(assetPath)) { + fail(`${rel(manifest)} references missing asset ${rel(assetPath)}`); + } + if (statSync(assetPath).size !== bytes) { + fail(`${rel(assetPath)} size does not match ${rel(manifest)}`); + } + if (sha256File(assetPath) !== sha256) { + fail(`${rel(assetPath)} checksum does not match ${rel(manifest)}`); + } + validateReleaseArchivePayload(assetPath); + } + const releaseManifest = path.join(root, "release-assets", `${product}-${expected.version}-manifest.json`); + if (!existsSync(releaseManifest)) { + fail(`${product} must stage release manifest ${rel(releaseManifest)}`); + } + const releaseData = readJson(releaseManifest); + const expectedRelease = { + schema: "oliphaunt-extension-release-manifest-v1", + product, + version: String(expected.version), + sqlName: String(expectedSqlName), + }; + for (const [key, value] of Object.entries(expectedRelease)) { + if (releaseData[key] !== value) { + fail(`${rel(releaseManifest)} has ${key}=${JSON.stringify(releaseData[key])}, expected ${JSON.stringify(value)}`); + } + } + if (!setEquals(new Set(Object.keys(releaseData)), PUBLIC_EXTENSION_RELEASE_MANIFEST_KEYS)) { + fail(`${rel(releaseManifest)} public manifest keys must be ${JSON.stringify([...PUBLIC_EXTENSION_RELEASE_MANIFEST_KEYS].sort(compareText))}, got ${JSON.stringify(Object.keys(releaseData).sort(compareText))}`); + } + const metadata = extensionMetadata(product); + if (releaseData.extensionClass !== metadata.class) { + fail(`${rel(releaseManifest)} has stale extensionClass`); + } + if (releaseData.versioning !== metadata.versioning) { + fail(`${rel(releaseManifest)} has stale versioning`); + } + if (!deepEqual(releaseData.sourceIdentity, extensionSourceIdentity(product))) { + fail(`${rel(releaseManifest)} has stale sourceIdentity`); + } + if (!deepEqual(releaseData.compatibility, metadata.compatibility)) { + fail(`${rel(releaseManifest)} has stale compatibility metadata`); + } + const publicAssets = releaseData.assets; + if (!Array.isArray(publicAssets) || publicAssets.length === 0) { + fail(`${rel(releaseManifest)} must declare release assets`); + } + const expectedPublicAssets = assets.map(publicExtensionAsset); + if (!deepEqual(publicAssets, expectedPublicAssets)) { + fail(`${rel(releaseManifest)} public assets must match staged CI manifest without local paths`); + } + for (const asset of publicAssets) { + if (asset === null || Array.isArray(asset) || typeof asset !== "object") { + fail(`${rel(releaseManifest)} contains a non-object public asset row`); + } + if (!setEquals(new Set(Object.keys(asset)), PUBLIC_EXTENSION_RELEASE_ASSET_KEYS)) { + fail(`${rel(releaseManifest)} public asset ${JSON.stringify(asset.name)} keys must be ${JSON.stringify([...PUBLIC_EXTENSION_RELEASE_ASSET_KEYS].sort(compareText))}, got ${JSON.stringify(Object.keys(asset).sort(compareText))}`); + } + } + const propertiesManifest = path.join(root, "release-assets", `${product}-${expected.version}-manifest.properties`); + if (!existsSync(propertiesManifest)) { + fail(`${product} must stage properties manifest ${rel(propertiesManifest)}`); + } + const properties = readPropertiesText(readFileSync(propertiesManifest, "utf8")); + const expectedProperties = { + schema: "oliphaunt-extension-release-manifest-v1", + product, + version: String(expected.version), + sqlName: String(expectedSqlName), + extensionClass: String(releaseData.extensionClass), + versioning: String(releaseData.versioning), + sourceKind: String(releaseData.sourceIdentity.kind), + }; + for (const [key, value] of Object.entries(expectedProperties)) { + if (properties[key] !== value) { + fail(`${rel(propertiesManifest)} has ${key}=${JSON.stringify(properties[key])}, expected ${JSON.stringify(value)}`); + } + } + const expectedPropertyAssets = Object.fromEntries( + assets.map((asset) => [`${asset.family}.${asset.target}.${asset.kind}`, asset.name]), + ); + const actualPropertyAssets = Object.fromEntries( + Object.entries(properties) + .filter(([key]) => key.startsWith("asset.")) + .map(([key, value]) => [key.slice("asset.".length), value]), + ); + if (JSON.stringify(sortObject(actualPropertyAssets)) !== JSON.stringify(sortObject(expectedPropertyAssets))) { + fail(`${rel(propertiesManifest)} asset rows must match ${rel(manifest)} exactly: ${JSON.stringify(actualPropertyAssets)} vs ${JSON.stringify(expectedPropertyAssets)}`); + } + const checksumManifest = path.join(root, "release-assets", `${product}-${expected.version}-release-assets.sha256`); + if (!existsSync(checksumManifest)) { + fail(`${product} must stage checksum manifest ${rel(checksumManifest)}`); + } + validateChecksumManifest(checksumManifest, path.join(root, "release-assets")); + if (requireFullTargets) { + const missing = [...allowedTargets].filter((target) => !stagedTargets.has(target)).sort(compareText); + if (missing.length > 0) { + fail(`${product} is missing published exact-extension targets: ${missing.join(", ")}`); + } + } + console.log(`validated exact-extension package artifacts: ${product}`); + return true; +} + +function setEquals(left, right) { + return left.size === right.size && [...left].every((item) => right.has(item)); +} + +function sortObject(value) { + return Object.fromEntries(Object.entries(value).sort(([left], [right]) => compareText(left, right))); +} + +function sortValue(value) { + if (Array.isArray(value)) { + return value.map(sortValue); + } + if (value !== null && typeof value === "object") { + return Object.fromEntries(Object.keys(value).sort(compareText).map((key) => [key, sortValue(value[key])])); + } + return value; +} + +function deepEqual(left, right) { + return JSON.stringify(sortValue(left)) === JSON.stringify(sortValue(right)); +} + +function validateChecksumManifest(file, assetDir) { + const declared = new Map(); + const lines = readFileSync(file, "utf8").split(/\r?\n/u); + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index].trim(); + if (!line) { + continue; + } + const parts = line.split(/\s+/u); + if (parts.length !== 2) { + fail(`${rel(file)}:${index + 1} must contain ' ./'`); + } + const [sha, name] = parts; + if (!/^[0-9a-f]{64}$/u.test(sha) || !name.startsWith("./") || name.slice(2).includes("/")) { + fail(`${rel(file)}:${index + 1} contains an invalid checksum entry`); + } + const assetName = name.slice(2); + if (declared.has(assetName)) { + fail(`${rel(file)} declares duplicate checksum entry for ${assetName}`); + } + declared.set(assetName, sha); + } + const expectedNames = readdirSync(assetDir) + .map((name) => path.join(assetDir, name)) + .filter((candidate) => isFile(candidate) && candidate !== file) + .map((candidate) => path.basename(candidate)) + .sort(compareText); + if (JSON.stringify([...declared.keys()].sort(compareText)) !== JSON.stringify(expectedNames)) { + fail(`${rel(file)} must cover release assets exactly`); + } + for (const [name, expectedSha] of declared) { + const actual = sha256File(path.join(assetDir, name)); + if (actual !== expectedSha) { + fail(`${rel(file)} checksum mismatch for ${name}`); + } + } +} + +function discoverMobileArtifacts(platform) { + if (platform === "android") { + const root = path.join(MOBILE_ROOT, "android"); + return existsSync(root) + ? readdirSync(root).filter((name) => name.endsWith(".apk")).map((name) => { + const file = path.join(root, name); + return { platform: "android", path: file, names: archiveZipNames(file), readText: (member) => zipReadText(file, member) }; + }).sort((left, right) => compareText(left.path, right.path)) + : []; + } + if (platform === "ios") { + const root = path.join(MOBILE_ROOT, "ios"); + return existsSync(root) + ? readdirSync(root).filter((name) => name.endsWith(".app") && isDirectory(path.join(root, name))).map((name) => { + const app = path.join(root, name); + return { platform: "ios", path: app, names: directoryNames(app), readText: (member) => dirReadText(app, member) }; + }).sort((left, right) => compareText(left.path, right.path)) + : []; + } + fail(`unsupported mobile platform ${platform}`); +} + +function mobilePrefix(platform) { + if (platform === "android") { + return "assets/oliphaunt/"; + } + if (platform === "ios") { + return "OliphauntReactNativeResources.bundle/oliphaunt/"; + } + fail(`unsupported mobile platform ${platform}`); +} + +function mobileTargetForArtifact(artifact) { + if (artifact.platform === "ios") { + return "ios-xcframework"; + } + const abis = artifact.names + .map((name) => name.split("/")) + .filter((parts) => parts.length === 3 && parts[0] === "lib" && parts[2] === "liboliphaunt.so") + .map((parts) => parts[1]) + .sort(compareText); + if (abis.length !== 1) { + fail(`${rel(artifact.path)} must contain exactly one Android liboliphaunt ABI, got ${JSON.stringify(abis)}`); + } + if (abis[0] === "arm64-v8a") { + return "android-arm64-v8a"; + } + if (abis[0] === "x86_64") { + return "android-x86_64"; + } + fail(`${rel(artifact.path)} contains unsupported Android ABI ${abis[0]}`); +} + +function mobileBuildReport(platform) { + const report = path.join(MOBILE_ROOT, platform, "build-report.json"); + if (!isFile(report)) { + return null; + } + const data = readJson(report); + if (data.schema !== "oliphaunt-react-native-mobile-build-v1") { + fail(`${rel(report)} has invalid mobile build report schema`); + } + if (data.platform !== platform) { + fail(`${rel(report)} has platform=${JSON.stringify(data.platform)}, expected ${JSON.stringify(platform)}`); + } + return data; +} + +function resolveReportPath(value, reportPath, field) { + if (typeof value !== "string" || !value) { + fail(`${rel(reportPath)} must declare ${field}`); + } + return path.isAbsolute(value) ? value : path.join(ROOT, value); +} + +function checkExtensionPackageHasMobileTarget(sqlName, target) { + for (const product of exactExtensionProducts(PREFIX)) { + const manifest = path.join(EXTENSION_ROOT, product, "extension-artifacts.json"); + if (!isFile(manifest)) { + continue; + } + const data = readJson(manifest); + if (data.sqlName !== sqlName) { + continue; + } + const assets = data.assets; + if (!Array.isArray(assets)) { + fail(`${rel(manifest)} must declare assets`); + } + const runtimeMatches = assets.filter((asset) => asset && asset.family === "native" && asset.target === target && asset.kind === "runtime"); + if (runtimeMatches.length !== 1) { + fail(`${sqlName} exact-extension package must contain one native runtime asset for ${target}`); + } + if (target === "ios-xcframework") { + const frameworkMatches = assets.filter((asset) => asset && asset.family === "native" && asset.target === target && asset.kind === "ios-xcframework"); + if (frameworkMatches.length !== 1) { + fail(`${sqlName} exact-extension package must contain one iOS XCFramework asset`); + } + } + return; + } + fail(`no exact-extension package found for selected mobile extension ${sqlName}`); +} + +function checkIosPrebuiltExtensionLinkage(artifact, stems) { + if (stems.length === 0) { + return; + } + const sourceLeaks = artifact.names + .filter((name) => name.includes("/static-registry/oliphaunt_static_registry.c") || name.includes("/extension-frameworks/") || name.endsWith(".xcframework")) + .sort(compareText); + if (sourceLeaks.length > 0) { + fail(`${rel(artifact.path)} includes build-only iOS static-extension inputs as app resources: ${sourceLeaks.slice(0, 10).join(", ")}`); + } + const report = mobileBuildReport("ios"); + if (report === null) { + fail(`${rel(artifact.path)} requires ${rel(path.join(MOBILE_ROOT, "ios/build-report.json"))} for iOS extension link evidence`); + } + const scratchRoot = report.scratchRoot; + if (typeof scratchRoot !== "string" || !scratchRoot) { + fail(`${rel(path.join(MOBILE_ROOT, "ios/build-report.json"))} must declare scratchRoot for iOS extension link evidence`); + } + const scratchPath = scratchRoot; + const xcodeLog = path.join(scratchPath, "xcodebuild.log"); + if (!isFile(xcodeLog)) { + fail(`iOS extension link evidence is missing xcodebuild log: ${rel(xcodeLog)}`); + } + const logText = readFileSync(xcodeLog, "utf8"); + if (!logText.includes("** BUILD SUCCEEDED **")) { + fail(`iOS extension link evidence requires a successful xcodebuild log: ${rel(xcodeLog)}`); + } + const podsSupport = path.join( + scratchPath, + "src/sdks/react-native/examples/expo/ios/Pods/Target Support Files/OliphauntReactNative", + ); + const inputFile = path.join(podsSupport, "OliphauntReactNative-xcframeworks-input-files.xcfilelist"); + const outputFile = path.join(podsSupport, "OliphauntReactNative-xcframeworks-output-files.xcfilelist"); + if (!isFile(inputFile)) { + fail(`iOS extension link evidence is missing CocoaPods XCFramework input file list: ${rel(inputFile)}`); + } + if (!isFile(outputFile)) { + fail(`iOS extension link evidence is missing CocoaPods XCFramework output file list: ${rel(outputFile)}`); + } + const expectedFrameworks = new Set(stems.map((stem) => `liboliphaunt_extension_${stem}`)); + const podText = `${readFileSync(inputFile, "utf8")}\n${readFileSync(outputFile, "utf8")}`; + const podFrameworks = new Set([...podText.matchAll(/liboliphaunt_extension_[A-Za-z0-9_]+/gu)].map((match) => match[0])); + const productsRoot = path.join(scratchPath, "DerivedData/Build/Products"); + if (!isDirectory(productsRoot)) { + fail(`iOS extension link evidence is missing Xcode build products: ${rel(productsRoot)}`); + } + const builtFrameworks = new Set( + walkFiles(productsRoot) + .map((file) => path.basename(file)) + .filter((name) => /^liboliphaunt_extension_.*(\.a|\.framework)$/u.test(name)) + .map((name) => name.replace(/\.a$/u, "").replace(/\.framework$/u, "")), + ); + const missingPods = [...expectedFrameworks].filter((item) => !podFrameworks.has(item)).sort(compareText); + if (missingPods.length > 0) { + fail(`CocoaPods file lists do not include selected iOS extension link input(s): ${missingPods.join(", ")}`); + } + const missingBuilt = [...expectedFrameworks].filter((item) => !builtFrameworks.has(item)).sort(compareText); + if (missingBuilt.length > 0) { + fail(`Xcode build products do not include selected iOS extension linked artifact(s): ${missingBuilt.join(", ")}`); + } + const unexpectedPods = [...podFrameworks].filter((item) => !expectedFrameworks.has(item)).sort(compareText); + if (unexpectedPods.length > 0) { + fail(`CocoaPods file lists include unselected iOS extension link input(s): ${unexpectedPods.join(", ")}`); + } + const unexpectedBuilt = [...builtFrameworks].filter((item) => !expectedFrameworks.has(item)).sort(compareText); + if (unexpectedBuilt.length > 0) { + fail(`Xcode build products include unselected iOS extension linked artifact(s): ${unexpectedBuilt.join(", ")}`); + } +} + +function checkAndroidPrebuiltExtensionLinkage(artifact, stems, report, reportPath, expectedAbi, staticRegistry, target) { + if (stems.length === 0) { + return; + } + const evidencePath = resolveReportPath(report.androidLinkEvidence, reportPath, "androidLinkEvidence"); + if (!isFile(evidencePath)) { + fail(`Android extension link evidence is missing: ${rel(evidencePath)}`); + } + const linkedStems = new Set(); + const linkedDependencies = new Set(); + let evidenceAbi = ""; + let runtimePath = ""; + let schemaRows = 0; + let abiRows = 0; + const requireExistingPath = (rawPath, lineNumber, rowKind) => { + const resolved = path.isAbsolute(rawPath) ? rawPath : path.join(path.dirname(evidencePath), rawPath); + if (!isFile(resolved)) { + fail(`${rel(evidencePath)}:${lineNumber} ${rowKind} path does not exist: ${resolved}`); + } + return resolved; + }; + const lines = readFileSync(evidencePath, "utf8").split(/\r?\n/u); + for (let index = 0; index < lines.length; index += 1) { + const parts = lines[index].split("\t"); + if (!parts.length || !parts[0]) { + continue; + } + const lineNumber = index + 1; + const kind = parts[0]; + if (kind === "schema") { + if (JSON.stringify(parts) !== JSON.stringify(["schema", "oliphaunt-android-static-extension-link-v1"])) { + fail(`${rel(evidencePath)}:${lineNumber} has invalid schema row`); + } + schemaRows += 1; + } else if (kind === "abi") { + if (parts.length !== 2) { + fail(`${rel(evidencePath)}:${lineNumber} has invalid abi row`); + } + evidenceAbi = parts[1]; + abiRows += 1; + } else if (kind === "runtime") { + if (parts.length !== 3 || parts[1] !== "liboliphaunt") { + fail(`${rel(evidencePath)}:${lineNumber} has invalid runtime row`); + } + const runtime = requireExistingPath(parts[2], lineNumber, "runtime"); + if (path.basename(runtime) !== "liboliphaunt.so") { + fail(`${rel(evidencePath)}:${lineNumber} runtime path must end in liboliphaunt.so`); + } + if (runtimePath) { + fail(`${rel(evidencePath)} contains duplicate runtime rows`); + } + runtimePath = runtime; + } else if (kind === "extension") { + if (parts.length !== 3) { + fail(`${rel(evidencePath)}:${lineNumber} has invalid extension row`); + } + const [stem, archive] = [parts[1], parts[2]]; + const expectedName = `liboliphaunt_extension_${stem}.a`; + const archivePath = requireExistingPath(archive, lineNumber, "extension"); + const expectedRelative = staticRegistry[`module.${stem}.archive.${target}`]; + if (!expectedRelative) { + fail(`${rel(artifact.path)} static registry manifest has no module.${stem}.archive.${target} entry`); + } + if (path.basename(archivePath) !== expectedName) { + fail(`${rel(evidencePath)}:${lineNumber} archive ${JSON.stringify(archive)} does not match stem ${JSON.stringify(stem)}`); + } + if (!archivePath.split(path.sep).join("/").endsWith(expectedRelative)) { + fail(`${rel(evidencePath)}:${lineNumber} archive ${JSON.stringify(archive)} does not match static-registry path ${JSON.stringify(expectedRelative)}`); + } + linkedStems.add(stem); + } else if (kind === "dependency") { + if (parts.length !== 3 || !parts[1]) { + fail(`${rel(evidencePath)}:${lineNumber} has invalid dependency row`); + } + const dependencyName = parts[1]; + const dependencyPath = requireExistingPath(parts[2], lineNumber, "dependency"); + const expectedRelative = staticRegistry[`dependency.${dependencyName}.archive.${target}`]; + if (!expectedRelative) { + fail(`${rel(evidencePath)}:${lineNumber} dependency ${JSON.stringify(dependencyName)} is not declared by the static-registry manifest for ${target}`); + } + if (!dependencyPath.split(path.sep).join("/").endsWith(expectedRelative)) { + fail(`${rel(evidencePath)}:${lineNumber} dependency path ${JSON.stringify(parts[2])} does not match static-registry path ${JSON.stringify(expectedRelative)}`); + } + linkedDependencies.add(dependencyName); + } else { + fail(`${rel(evidencePath)}:${lineNumber} has unknown row kind ${JSON.stringify(kind)}`); + } + } + if (schemaRows !== 1) { + fail(`${rel(evidencePath)} must contain exactly one schema row`); + } + if (abiRows !== 1) { + fail(`${rel(evidencePath)} must contain exactly one abi row`); + } + if (evidenceAbi !== expectedAbi) { + fail(`${rel(evidencePath)} declares abi=${JSON.stringify(evidenceAbi)}, expected ${JSON.stringify(expectedAbi)}`); + } + if (!runtimePath) { + fail(`${rel(evidencePath)} does not show liboliphaunt runtime link input`); + } + const expectedStems = new Set(stems); + const missing = [...expectedStems].filter((stem) => !linkedStems.has(stem)).sort(compareText); + if (missing.length > 0) { + fail(`${rel(evidencePath)} does not show selected Android extension archive link input(s): ${missing.join(", ")}`); + } + const unexpected = [...linkedStems].filter((stem) => !expectedStems.has(stem)).sort(compareText); + if (unexpected.length > 0) { + fail(`${rel(evidencePath)} shows unselected Android extension archive link input(s): ${unexpected.join(", ")}`); + } + const expectedDependencies = new Set(csvValues(staticRegistry.dependencyArchives)); + const missingDependencies = [...expectedDependencies].filter((dependency) => !linkedDependencies.has(dependency)).sort(compareText); + if (missingDependencies.length > 0) { + fail(`${rel(evidencePath)} does not show required Android extension dependency archive link input(s): ${missingDependencies.join(", ")}`); + } + const unexpectedDependencies = [...linkedDependencies].filter((dependency) => !expectedDependencies.has(dependency)).sort(compareText); + if (unexpectedDependencies.length > 0) { + fail(`${rel(evidencePath)} shows unselected Android extension dependency archive link input(s): ${unexpectedDependencies.join(", ")}`); + } +} + +function checkMobileArtifact(artifact, { requirePrebuiltExtensions }) { + const prefix = mobilePrefix(artifact.platform); + const runtimeManifestName = `${prefix}runtime/manifest.properties`; + const staticRegistryManifestName = `${prefix}static-registry/manifest.properties`; + const packageSizeName = `${prefix}package-size.tsv`; + const runtime = readPropertiesText(artifact.readText(runtimeManifestName)); + if (runtime.schema !== "oliphaunt-runtime-resources-v1") { + fail(`${rel(artifact.path)} has invalid runtime resource manifest schema`); + } + const selected = csvValues(runtime.extensions); + const selectedSet = new Set(selected); + const rows = generatedExtensionRows(); + const target = mobileTargetForArtifact(artifact); + const reportPath = path.join(MOBILE_ROOT, artifact.platform, "build-report.json"); + const report = mobileBuildReport(artifact.platform); + if (report === null) { + fail(`${rel(artifact.path)} requires mobile build report ${rel(reportPath)}`); + } + const reportArtifact = resolveReportPath(report.appArtifact, reportPath, "appArtifact"); + if (path.resolve(reportArtifact) !== path.resolve(artifact.path)) { + fail(`${rel(reportPath)} appArtifact=${reportArtifact} does not match inspected artifact ${artifact.path}`); + } + if (report.appArtifactBytes !== pathBytes(artifact.path)) { + fail(`${rel(reportPath)} appArtifactBytes does not match inspected artifact size`); + } + if (!Array.isArray(report.selectedExtensions)) { + fail(`${rel(reportPath)} selectedExtensions must be an array`); + } + const reportSelected = report.selectedExtensions.map((value) => String(value)).filter(Boolean).sort(compareText); + if (JSON.stringify(reportSelected) !== JSON.stringify([...selected].sort(compareText))) { + fail(`${rel(reportPath)} selectedExtensions=${JSON.stringify(reportSelected)} must match runtime manifest ${JSON.stringify([...selected].sort(compareText))}`); + } + let expectedAbi = ""; + if (artifact.platform === "android") { + expectedAbi = target === "android-arm64-v8a" ? "arm64-v8a" : "x86_64"; + if (report.abi !== expectedAbi) { + fail(`${rel(reportPath)} abi=${JSON.stringify(report.abi)}, expected ${JSON.stringify(expectedAbi)}`); + } + } + const extensionAssetNames = artifact.names.filter( + (name) => + name.includes(`${prefix}runtime/files/share/postgresql/extension/`) && + (name.endsWith(".control") || name.endsWith(".sql")), + ); + const presentExtensions = new Set(extensionAssetNames.map(extensionNameForAsset).filter(Boolean)); + const unexpected = [...presentExtensions].filter((extension) => !selectedSet.has(extension) && !BASELINE_POSTGRES_EXTENSIONS.has(extension)).sort(compareText); + if (unexpected.length > 0) { + fail(`${rel(artifact.path)} includes unselected extension assets: ${unexpected.join(", ")}`); + } + for (const extension of selected) { + if (createsExtension(extension, rows)) { + const hasControl = extensionAssetNames.some((name) => name.endsWith(`/${extension}.control`)); + const hasSql = extensionAssetNames.some((name) => name.includes(`/${extension}--`) && name.endsWith(".sql")); + if (!hasControl || !hasSql) { + fail(`${rel(artifact.path)} is missing selected ${extension} control/SQL assets`); + } + } + if (requirePrebuiltExtensions) { + checkExtensionPackageHasMobileTarget(extension, target); + } + } + const stems = selected.map((extension) => nativeModuleStem(extension, rows)).filter((stem) => stem && stem !== "-").sort(compareText); + const staticRegistry = readPropertiesText(artifact.readText(staticRegistryManifestName)); + const registered = csvValues(staticRegistry.registeredExtensions).sort(compareText); + const nativeSelected = nativeModuleExtensions(selected, rows); + if (stems.length > 0) { + if (runtime.mobileStaticRegistryState !== "complete") { + fail(`${rel(artifact.path)} must mark mobile static registry complete for native-module extensions`); + } + if (JSON.stringify(registered) !== JSON.stringify(nativeSelected)) { + fail(`${rel(artifact.path)} static registry registeredExtensions=${JSON.stringify(registered)}, expected ${JSON.stringify(nativeSelected)}`); + } + if (artifact.platform === "android" && !artifact.names.some((name) => name.endsWith("/liboliphaunt_extensions.so"))) { + fail(`${rel(artifact.path)} Android app is missing liboliphaunt_extensions.so`); + } + if (artifact.platform === "android" && requirePrebuiltExtensions) { + checkAndroidPrebuiltExtensionLinkage(artifact, stems, report, reportPath, expectedAbi, staticRegistry, target); + } + if (artifact.platform === "ios" && requirePrebuiltExtensions) { + checkIosPrebuiltExtensionLinkage(artifact, stems); + } + if (artifact.names.some((name) => name.includes("static-registry/archives/"))) { + fail(`${rel(artifact.path)} must not ship build-only static-registry archives`); + } + } else if (![undefined, "", "not-required"].includes(runtime.mobileStaticRegistryState)) { + fail(`${rel(artifact.path)} must not claim a static registry for SQL-only extensions`); + } + const packageSize = artifact.readText(packageSizeName); + const packageSizeExtensions = packageSize + .split(/\r?\n/u) + .filter((line) => line.startsWith("extension\t")) + .map((line) => line.split("\t")[1]) + .filter(Boolean) + .sort(compareText); + if (JSON.stringify(packageSizeExtensions) !== JSON.stringify([...selected].sort(compareText))) { + fail(`${rel(artifact.path)} package-size extension rows ${JSON.stringify(packageSizeExtensions)} must exactly match selected extensions ${JSON.stringify([...selected].sort(compareText))}`); + } + console.log(`validated mobile app extension contents: ${artifact.platform} ${rel(artifact.path)}`); +} + +function checkMobilePlatform(platform, { require, requirePrebuiltExtensions }) { + const artifacts = discoverMobileArtifacts(platform); + if (artifacts.length === 0) { + if (require) { + fail(`missing staged React Native ${platform} mobile app artifacts under ${rel(path.join(MOBILE_ROOT, platform))}`); + } + return false; + } + for (const artifact of artifacts) { + checkMobileArtifact(artifact, { requirePrebuiltExtensions }); + } + return true; +} + +function expandProducts(values, { allProducts, label }) { + const expanded = []; + for (const value of values) { + if (value === "all") { + expanded.push(...[...allProducts].sort(compareText)); + } else if (!allProducts.has(value)) { + fail(`unknown ${label} ${value}; expected one of: all, ${[...allProducts].sort(compareText).join(", ")}`); + } else { + expanded.push(value); + } + } + return [...new Set(expanded)].sort(compareText); +} + +function usage() { + return `usage: tools/release/check-staged-artifacts.mjs [options] + +Options: + --require-sdk-product PRODUCT SDK product to require, or all + --require-extension-product PRODUCT exact-extension product to require, or all + --require-full-extension-targets require every published exact-extension target + --require-mobile android|ios|all mobile app artifact platform to require + --require-mobile-prebuilt-extensions require matching exact-extension package inputs + --inspect-present also inspect any present staged artifacts + -h, --help show this help +`; +} + +function parseArgs(argv) { + const args = { + requireSdkProduct: [], + requireExtensionProduct: [], + requireFullExtensionTargets: false, + requireMobile: [], + requireMobilePrebuiltExtensions: false, + inspectPresent: false, + }; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === "--require-sdk-product") { + const value = argv[index + 1]; + if (!value) { + fail("--require-sdk-product requires a value"); + } + args.requireSdkProduct.push(value); + index += 1; + } else if (arg === "--require-extension-product") { + const value = argv[index + 1]; + if (!value) { + fail("--require-extension-product requires a value"); + } + args.requireExtensionProduct.push(value); + index += 1; + } else if (arg === "--require-full-extension-targets") { + args.requireFullExtensionTargets = true; + } else if (arg === "--require-mobile") { + const value = argv[index + 1]; + if (!["android", "ios", "all"].includes(value)) { + fail("--require-mobile requires one of: android, ios, all"); + } + args.requireMobile.push(value); + index += 1; + } else if (arg === "--require-mobile-prebuilt-extensions") { + args.requireMobilePrebuiltExtensions = true; + } else if (arg === "--inspect-present") { + args.inspectPresent = true; + } else if (arg === "--help" || arg === "-h") { + process.stdout.write(usage()); + process.exit(0); + } else { + fail(`unknown argument ${arg}`); + } + } + return args; +} + +async function main(argv) { + const args = parseArgs(argv); + let checked = 0; + + const sdkProductSet = new Set(sdkProducts()); + const requiredSdkProducts = expandProducts(args.requireSdkProduct, { + allProducts: sdkProductSet, + label: "SDK product", + }); + for (const product of requiredSdkProducts) { + checked += Number(await checkSdkProduct(product, { require: true })); + } + if (args.inspectPresent) { + for (const product of [...sdkProductSet].filter((product) => !requiredSdkProducts.includes(product)).sort(compareText)) { + checked += Number(await checkSdkProduct(product, { require: false })); + } + } + + const extensionProductSet = new Set(exactExtensionProducts(PREFIX)); + const requiredExtensionProducts = expandProducts(args.requireExtensionProduct, { + allProducts: extensionProductSet, + label: "exact-extension product", + }); + for (const product of requiredExtensionProducts) { + checked += Number(await checkExtensionProduct(product, { + require: true, + requireFullTargets: args.requireFullExtensionTargets, + })); + } + if (args.inspectPresent) { + for (const product of [...extensionProductSet].filter((product) => !requiredExtensionProducts.includes(product)).sort(compareText)) { + checked += Number(await checkExtensionProduct(product, { + require: false, + requireFullTargets: false, + })); + } + } + + const requiredMobile = new Set(); + for (const value of args.requireMobile) { + if (value === "all") { + requiredMobile.add("android"); + requiredMobile.add("ios"); + } else { + requiredMobile.add(value); + } + } + for (const platform of [...requiredMobile].sort(compareText)) { + checked += Number(checkMobilePlatform(platform, { + require: true, + requirePrebuiltExtensions: args.requireMobilePrebuiltExtensions, + })); + } + if (args.inspectPresent) { + for (const platform of ["android", "ios"].filter((value) => !requiredMobile.has(value))) { + checked += Number(checkMobilePlatform(platform, { + require: false, + requirePrebuiltExtensions: args.requireMobilePrebuiltExtensions, + })); + } + } + + if (checked === 0) { + fail("no staged artifacts were checked; pass --require-* or --inspect-present"); + } +} + +await main(Bun.argv.slice(2)); diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py index d34aaf41..7c485c3f 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -394,10 +394,10 @@ def validate_ci_release_artifacts() -> None: "OLIPHAUNT_EXPO_EXTENSION_ARTIFACT_ROOT": "Mobile build jobs must resolve exact-extension artifacts from the staged package artifact root", "Validate Android mobile app artifacts": "Android mobile build jobs must inspect the built app for exact selected-extension contents", "Validate iOS mobile app artifacts": "iOS mobile build jobs must inspect the built app for exact selected-extension contents", - "check_staged_artifacts.py --require-mobile android --require-mobile-prebuilt-extensions": ( + "check-staged-artifacts.mjs --require-mobile android --require-mobile-prebuilt-extensions": ( "Android mobile artifact validation must require prebuilt exact-extension package inputs" ), - "check_staged_artifacts.py --require-mobile ios --require-mobile-prebuilt-extensions": ( + "check-staged-artifacts.mjs --require-mobile ios --require-mobile-prebuilt-extensions": ( "iOS mobile artifact validation must require prebuilt exact-extension package inputs" ), "OLIPHAUNT_EXPO_IOS_OLIPHAUNT_XCFRAMEWORK": "iOS mobile build jobs must consume the linked liboliphaunt XCFramework artifact", @@ -609,7 +609,7 @@ def validate_ci_release_artifacts() -> None: ) require_text( "tools/release/build-sdk-ci-artifacts.sh", - 'check_staged_artifacts.py --require-sdk-product "$product"', + 'check-staged-artifacts.mjs --require-sdk-product "$product"', "SDK package builders must validate staged package artifacts for runtime/extension payload leaks", ) reject_text( @@ -624,7 +624,7 @@ def validate_ci_release_artifacts() -> None: ) require_text( "src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh", - "check_staged_artifacts.py \"${validation_args[@]}\"", + "check-staged-artifacts.mjs \"${validation_args[@]}\"", "mobile exact-extension package assembly must validate the staged package manifests and checksums it selected", ) require_text( @@ -643,8 +643,8 @@ def validate_ci_release_artifacts() -> None: "liboliphaunt native aggregate assets must have one Moon-modeled packager/checker entrypoint", ) require_text( - "tools/release/check_staged_artifacts.py", - "validate_release_archive_payload(path)", + "tools/release/check-staged-artifacts.mjs", + "validateReleaseArchivePayload(assetPath)", "staged exact-extension artifact checks must reject placeholder files that are not readable release archives", ) require_text( diff --git a/tools/release/check_staged_artifacts.py b/tools/release/check_staged_artifacts.py deleted file mode 100755 index b5b9ee49..00000000 --- a/tools/release/check_staged_artifacts.py +++ /dev/null @@ -1,1163 +0,0 @@ -#!/usr/bin/env python3 -"""Validate staged release/build artifacts without rebuilding them. - -This checker enforces the packaging boundary: - -* SDK packages are wrappers and must not accidentally embed runtime or extension - payloads. -* Exact-extension packages must contain only declared artifact targets, with - checksums matching their manifests. -* Mobile app artifacts must contain only the extensions selected for that app. -""" - -from __future__ import annotations - -import argparse -import hashlib -import json -import re -import sys -import tarfile -import tomllib -import zipfile -from collections.abc import Iterable -from dataclasses import dataclass -from pathlib import Path -from typing import NoReturn - -import artifact_targets -import extension_artifact_targets -import package_liboliphaunt_wasix_cargo_artifacts -import product_metadata - - -ROOT = Path(__file__).resolve().parents[2] -SDK_ROOT = ROOT / "target" / "sdk-artifacts" -EXTENSION_ROOT = ROOT / "target" / "extension-artifacts" -MOBILE_ROOT = ROOT / "target" / "mobile-build" / "react-native" - -SDK_PRODUCTS = frozenset(artifact_targets.sdk_package_products()) - -SDK_RUNTIME_PAYLOAD_PATTERNS = [ - re.compile(pattern) - for pattern in ( - r"(^|/)assets/oliphaunt/runtime/", - r"(^|/)assets/oliphaunt/template-pgdata/", - r"(^|/)assets/oliphaunt/static-registry/archives/", - r"(^|/)oliphaunt/runtime/files/", - r"(^|/)runtime/files/share/postgresql/", - r"(^|/)share/postgresql/extension/[^/]+\.(control|sql)$", - r"(^|/)release-assets/", - r"(^|/)extension-artifacts\.json$", - r"(^|/)liboliphaunt\.(so|dylib|dll|a|lib)$", - r"(^|/)liboliphaunt_extensions\.(so|dylib|dll|a|lib)$", - r"(^|/)liboliphaunt_extension_[^/]+\.(so|dylib|dll|a|lib)$", - r"\.xcframework(/|$)", - ) -] - -KOTLIN_ALLOWED_NATIVE_PAYLOADS = { - "liboliphaunt_kotlin_android.so", -} -KOTLIN_RELEASE_ABIS = {"arm64-v8a", "x86_64"} -BASELINE_POSTGRES_EXTENSIONS = {"plpgsql"} - - -def fail(message: str) -> NoReturn: - print(f"check_staged_artifacts.py: {message}", file=sys.stderr) - raise SystemExit(1) - - -def rel(path: Path) -> str: - try: - return str(path.relative_to(ROOT)) - except ValueError: - return str(path) - - -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 read_json(path: Path) -> dict[str, object]: - try: - data = json.loads(path.read_text(encoding="utf-8")) - except json.JSONDecodeError as error: - fail(f"{rel(path)} is not valid JSON: {error}") - if not isinstance(data, dict): - fail(f"{rel(path)} must contain a JSON object") - return data - - -def read_properties_text(text: str) -> dict[str, str]: - parsed: dict[str, str] = {} - for raw in text.splitlines(): - line = raw.strip() - if not line or line.startswith("#"): - continue - if "=" not in line: - fail(f"invalid properties line: {raw!r}") - key, value = line.split("=", 1) - parsed[key] = value - return parsed - - -def csv_values(value: str | None) -> list[str]: - if not value: - return [] - return [item.strip() for item in value.split(",") if item.strip()] - - -def archive_tar_names(path: Path) -> list[str]: - try: - with tarfile.open(path, "r:*") as archive: - return sorted(member.name for member in archive.getmembers() if member.isfile()) - except tarfile.TarError as error: - 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: - return sorted(name for name in archive.namelist() if not name.endswith("/")) - except zipfile.BadZipFile as error: - 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 = { - 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) - 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: 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 sorted(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) - if magic != b"\x28\xb5\x2f\xfd": - fail(f"{rel(path)} is not a zstd archive") - - -def validate_release_archive_payload(path: Path) -> None: - if path.name.endswith(".tar.gz") or path.name.endswith(".tgz") or path.name.endswith(".crate"): - names = archive_tar_names(path) - if not names: - fail(f"{rel(path)} must contain at least one file") - return - if path.name.endswith(".zip") or path.name.endswith(".aar") or path.name.endswith(".jar"): - names = archive_zip_names(path) - if not names: - fail(f"{rel(path)} must contain at least one file") - return - if path.name.endswith(".tar.zst"): - validate_zstd_archive_magic(path) - - -def directory_names(root: Path) -> list[str]: - return sorted(str(path.relative_to(root)) for path in root.rglob("*") if path.is_file()) - - -def path_bytes(path: Path) -> int: - if path.is_file(): - return path.stat().st_size - if path.is_dir(): - return sum(item.stat().st_size for item in path.rglob("*") if item.is_file()) - fail(f"missing path while measuring bytes: {rel(path)}") - - -def zip_read_text(path: Path, name: str) -> str: - try: - with zipfile.ZipFile(path) as archive: - with archive.open(name) as handle: - return handle.read().decode("utf-8") - except KeyError: - fail(f"{rel(path)} is missing {name}") - except zipfile.BadZipFile as error: - fail(f"{rel(path)} is not a readable zip archive: {error}") - - -def dir_read_text(root: Path, name: str) -> str: - path = root / name - if not path.is_file(): - fail(f"{rel(root)} is missing {name}") - return path.read_text(encoding="utf-8") - - -def generated_extension_rows() -> dict[str, dict[str, object]]: - metadata = ROOT / "src" / "extensions" / "generated" / "sdk" / "react-native.json" - data = read_json(metadata) - rows = data.get("extensions") - if not isinstance(rows, list): - fail(f"{rel(metadata)} must contain an extensions array") - result: dict[str, dict[str, object]] = {} - for row in rows: - if not isinstance(row, dict): - continue - sql_name = row.get("sql-name") - if isinstance(sql_name, str) and sql_name: - result[sql_name] = row - return result - - -def creates_extension(sql_name: str, rows: dict[str, dict[str, object]]) -> bool: - row = rows.get(sql_name) - if row is None: - fail(f"selected extension {sql_name!r} is missing from generated extension metadata") - return row.get("creates-extension") is not False - - -def native_module_stem(sql_name: str, rows: dict[str, dict[str, object]]) -> str: - row = rows.get(sql_name) - if row is None: - fail(f"selected extension {sql_name!r} is missing from generated extension metadata") - stem = row.get("native-module-stem") - return stem if isinstance(stem, str) else "" - - -def native_module_extensions(selected: list[str], rows: dict[str, dict[str, object]]) -> list[str]: - return sorted( - extension - for extension in selected - if (stem := native_module_stem(extension, rows)) and stem != "-" - ) - - -def extension_name_for_asset(path_name: str) -> str | None: - name = Path(path_name).name - if name.endswith(".control"): - return name.removesuffix(".control") - if "--" in name and name.endswith(".sql"): - return name.split("--", 1)[0] - return None - - -def reject_sdk_runtime_payload(product: str, artifact: Path, names: Iterable[str]) -> None: - for name in names: - basename = Path(name).name - if product == "oliphaunt-kotlin" and basename in KOTLIN_ALLOWED_NATIVE_PAYLOADS: - continue - for pattern in SDK_RUNTIME_PAYLOAD_PATTERNS: - if pattern.search(name): - fail(f"{product} SDK artifact {rel(artifact)} must not include runtime/extension payload {name}") - - -def validate_kotlin_android_aar(artifact: Path, names: Iterable[str]) -> None: - name_set = set(names) - present_abis = { - parts[1] - for name in name_set - if (parts := name.split("/")) and len(parts) == 3 and parts[0] == "jni" and parts[2] == "liboliphaunt_kotlin_android.so" - } - if present_abis != KOTLIN_RELEASE_ABIS: - fail( - f"Kotlin Android release AAR {rel(artifact)} must contain JNI adapters for " - f"{', '.join(sorted(KOTLIN_RELEASE_ABIS))}; got {', '.join(sorted(present_abis)) or '(none)'}" - ) - - -def check_sdk_product(product: str, *, require: bool) -> bool: - root = SDK_ROOT / product - if not root.exists(): - if require: - fail(f"missing staged SDK artifacts for {product} under {rel(root)}") - return False - - checked = False - if product in {"oliphaunt-js", "oliphaunt-react-native"}: - tarballs = sorted(root.glob("*.tgz")) - if not tarballs and require: - fail(f"{product} must stage an npm tarball under {rel(root)}") - for tarball in tarballs: - reject_sdk_runtime_payload(product, tarball, archive_tar_names(tarball)) - checked = True - elif product == "oliphaunt-swift": - archives = sorted(root.glob("*.zip")) - if not archives and require: - fail(f"{product} must stage a source zip under {rel(root)}") - for archive in archives: - reject_sdk_runtime_payload(product, archive, archive_zip_names(archive)) - checked = True - release_manifest = root / "Package.swift.release" - if not release_manifest.exists() and require: - fail(f"{product} must stage {rel(release_manifest)} for release installation") - if release_manifest.exists(): - text = release_manifest.read_text(encoding="utf-8") - if "file://" in text: - fail(f"{rel(release_manifest)} must not contain local file URLs") - if "liboliphaunt-native-v" not in text or "checksum:" not in text: - fail(f"{rel(release_manifest)} must reference checksummed public liboliphaunt assets") - elif product == "oliphaunt-kotlin": - maven_root = root / "maven" - if not maven_root.is_dir(): - if require: - fail(f"{product} must stage a Maven repository under {rel(maven_root)}") - return False - archives = sorted([*root.glob("*.aar"), *root.glob("*.jar")]) - for archive in archives: - names = archive_zip_names(archive) - reject_sdk_runtime_payload(product, archive, names) - if archive.suffix == ".aar": - validate_kotlin_android_aar(archive, names) - checked = True - maven_artifacts = sorted(maven_root.rglob("*")) - for artifact in (path for path in maven_artifacts if path.suffix in {".aar", ".jar"}): - names = archive_zip_names(artifact) - reject_sdk_runtime_payload(product, artifact, names) - if artifact.suffix == ".aar": - validate_kotlin_android_aar(artifact, names) - checked = True - elif product == "oliphaunt-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)) - 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: - fail(f"{product} must stage a Cargo package file list under {rel(root)}") - return False - entries = { - line.strip() - for line in listing.read_text(encoding="utf-8").splitlines() - if line.strip() - } - for required_entry in { - "Cargo.toml", - "README.md", - "src/lib.rs", - "src/bin/oliphaunt_wasix_dump.rs", - "src/bin/oliphaunt_wasix_proxy.rs", - "src/oliphaunt/assets.rs", - }: - if required_entry not in entries: - fail(f"{product} package file list is missing {required_entry}") - for entry in entries: - if entry.startswith(("target/", "src/runtimes/", "src/extensions/generated/")): - fail( - f"{product} package file list contains generated or external payload entry {entry}" - ) - checked = True - else: - fail(f"unsupported SDK product {product}") - - if require and not checked: - fail(f"{product} did not contain any inspectable staged package artifacts under {rel(root)}") - if checked: - print(f"validated SDK artifact cleanliness: {product}") - return checked - - -def exact_extension_products() -> list[str]: - products: list[str] = [] - for product in product_metadata.product_ids(): - if product_metadata.product_config(product).get("kind") == "exact-extension-artifact": - products.append(product) - return sorted(products) - - -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 public_extension_asset(asset: dict) -> dict: - return { - key: asset[key] - for key in product_metadata.PUBLIC_EXTENSION_RELEASE_ASSET_KEYS - if key in asset - } - - -def check_extension_product(product: str, *, require: bool, require_full_targets: bool) -> bool: - root = EXTENSION_ROOT / product - manifest = root / "extension-artifacts.json" - if not manifest.exists(): - if require: - fail(f"missing staged exact-extension package manifest for {product} under {rel(root)}") - return False - data = read_json(manifest) - expected = { - "schema": "oliphaunt-extension-ci-artifacts-v1", - "product": product, - "version": product_metadata.read_current_version(product), - } - for key, value in expected.items(): - if data.get(key) != value: - fail(f"{rel(manifest)} has {key}={data.get(key)!r}, expected {value!r}") - sql_name = data.get("sqlName") - expected_sql_name = product_metadata.product_config(product).get("extension_sql_name") - if sql_name != expected_sql_name: - fail(f"{rel(manifest)} has sqlName={sql_name!r}, expected {expected_sql_name!r}") - - assets = data.get("assets") - if not isinstance(assets, list) or not assets: - fail(f"{rel(manifest)} must declare at least one asset") - - seen_names: set[str] = set() - staged_targets: set[str] = set() - allowed_targets = { - target.target for target in extension_artifact_targets.artifact_targets(product=product, published_only=True) - } - for asset in assets: - if not isinstance(asset, dict): - fail(f"{rel(manifest)} contains a non-object asset entry") - family = asset.get("family") - target = asset.get("target") - kind = asset.get("kind") - name = asset.get("name") - path_value = asset.get("path") - sha = asset.get("sha256") - bytes_value = asset.get("bytes") - if not all(isinstance(value, str) and value for value in (family, target, kind, name, path_value, sha)): - fail(f"{rel(manifest)} contains an incomplete asset entry: {asset!r}") - if not isinstance(bytes_value, int) or bytes_value <= 0: - fail(f"{rel(manifest)} asset {name} must declare positive bytes") - if name in seen_names: - fail(f"{rel(manifest)} declares duplicate asset name {name}") - seen_names.add(name) - staged_targets.add(target) - if target not in allowed_targets: - fail(f"{rel(manifest)} stages undeclared target={target!r}") - if not extension_artifact_kind_allowed(family, target, kind): - fail(f"{rel(manifest)} stages invalid artifact kind={kind!r} for family={family!r} target={target!r}") - path = ROOT / path_value - if path.parent != root / "release-assets" or path.name != name: - fail(f"{rel(manifest)} asset {name} must live directly under {rel(root / 'release-assets')}") - if not path.is_file(): - fail(f"{rel(manifest)} references missing asset {rel(path)}") - if path.stat().st_size != bytes_value: - fail(f"{rel(path)} size does not match {rel(manifest)}") - if sha256_file(path) != sha: - fail(f"{rel(path)} checksum does not match {rel(manifest)}") - validate_release_archive_payload(path) - - release_manifest = root / "release-assets" / f"{product}-{expected['version']}-manifest.json" - if not release_manifest.exists(): - fail(f"{product} must stage release manifest {rel(release_manifest)}") - release_data = read_json(release_manifest) - expected_release = { - "schema": "oliphaunt-extension-release-manifest-v1", - "product": product, - "version": str(expected["version"]), - "sqlName": str(expected_sql_name), - } - for key, value in expected_release.items(): - if release_data.get(key) != value: - fail(f"{rel(release_manifest)} has {key}={release_data.get(key)!r}, expected {value!r}") - actual_release_keys = set(release_data) - expected_release_keys = product_metadata.PUBLIC_EXTENSION_RELEASE_MANIFEST_KEYS - if actual_release_keys != expected_release_keys: - fail( - f"{rel(release_manifest)} public manifest keys must be " - f"{sorted(expected_release_keys)}, got {sorted(actual_release_keys)}" - ) - extension_metadata = product_metadata.extension_metadata(product) - if release_data.get("extensionClass") != extension_metadata["class"]: - fail(f"{rel(release_manifest)} has stale extensionClass") - if release_data.get("versioning") != extension_metadata["versioning"]: - fail(f"{rel(release_manifest)} has stale versioning") - if release_data.get("sourceIdentity") != product_metadata.extension_source_identity(product): - fail(f"{rel(release_manifest)} has stale sourceIdentity") - if release_data.get("compatibility") != extension_metadata["compatibility"]: - fail(f"{rel(release_manifest)} has stale compatibility metadata") - public_assets = release_data.get("assets") - if not isinstance(public_assets, list) or not public_assets: - fail(f"{rel(release_manifest)} must declare release assets") - expected_public_assets = [public_extension_asset(asset) for asset in assets] - if public_assets != expected_public_assets: - fail(f"{rel(release_manifest)} public assets must match staged CI manifest without local paths") - for asset in public_assets: - if not isinstance(asset, dict): - fail(f"{rel(release_manifest)} contains a non-object public asset row") - actual_asset_keys = set(asset) - expected_asset_keys = product_metadata.PUBLIC_EXTENSION_RELEASE_ASSET_KEYS - if actual_asset_keys != expected_asset_keys: - fail( - f"{rel(release_manifest)} public asset {asset.get('name')!r} keys must be " - f"{sorted(expected_asset_keys)}, got {sorted(actual_asset_keys)}" - ) - properties_manifest = root / "release-assets" / f"{product}-{expected['version']}-manifest.properties" - if not properties_manifest.exists(): - fail(f"{product} must stage properties manifest {rel(properties_manifest)}") - properties = read_properties_text(properties_manifest.read_text(encoding="utf-8")) - expected_properties = { - "schema": "oliphaunt-extension-release-manifest-v1", - "product": product, - "version": str(expected["version"]), - "sqlName": str(expected_sql_name), - "extensionClass": str(release_data["extensionClass"]), - "versioning": str(release_data["versioning"]), - "sourceKind": str(release_data["sourceIdentity"]["kind"]), - } - for key, value in expected_properties.items(): - if properties.get(key) != value: - fail(f"{rel(properties_manifest)} has {key}={properties.get(key)!r}, expected {value!r}") - expected_property_assets = { - f"{asset['family']}.{asset['target']}.{asset['kind']}": asset["name"] - for asset in assets - if isinstance(asset, dict) - } - actual_property_assets = { - key.removeprefix("asset."): value - for key, value in properties.items() - if key.startswith("asset.") - } - if actual_property_assets != expected_property_assets: - fail( - f"{rel(properties_manifest)} asset rows must match {rel(manifest)} exactly: " - f"{actual_property_assets!r} vs {expected_property_assets!r}" - ) - checksum_manifest = root / "release-assets" / f"{product}-{expected['version']}-release-assets.sha256" - if not checksum_manifest.exists(): - fail(f"{product} must stage checksum manifest {rel(checksum_manifest)}") - validate_checksum_manifest(checksum_manifest, root / "release-assets") - - if require_full_targets: - missing = allowed_targets - staged_targets - if missing: - rendered = ", ".join(sorted(missing)) - fail(f"{product} is missing published exact-extension targets: {rendered}") - print(f"validated exact-extension package artifacts: {product}") - return True - - -def validate_checksum_manifest(path: Path, asset_dir: Path) -> None: - declared: dict[str, str] = {} - for line_number, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1): - line = raw.strip() - if not line: - continue - parts = line.split(None, 1) - if len(parts) != 2: - fail(f"{rel(path)}:{line_number} must contain ' ./'") - sha, name = parts - if not re.fullmatch(r"[0-9a-f]{64}", sha) or not name.startswith("./") or "/" in name[2:]: - fail(f"{rel(path)}:{line_number} contains an invalid checksum entry") - asset_name = name[2:] - if asset_name in declared: - fail(f"{rel(path)} declares duplicate checksum entry for {asset_name}") - declared[asset_name] = sha - expected_names = sorted(item.name for item in asset_dir.iterdir() if item.is_file() and item != path) - if sorted(declared) != expected_names: - fail(f"{rel(path)} must cover release assets exactly") - for name, expected_sha in declared.items(): - actual = sha256_file(asset_dir / name) - if actual != expected_sha: - fail(f"{rel(path)} checksum mismatch for {name}") - - -@dataclass(frozen=True) -class MobileArtifact: - platform: str - path: Path - names: list[str] - - def read_text(self, name: str) -> str: - if self.path.is_dir(): - return dir_read_text(self.path, name) - return zip_read_text(self.path, name) - - -def discover_mobile_artifacts(platform: str) -> list[MobileArtifact]: - if platform == "android": - return [ - MobileArtifact("android", apk, archive_zip_names(apk)) - for apk in sorted((MOBILE_ROOT / "android").glob("*.apk")) - ] - if platform == "ios": - ios_root = MOBILE_ROOT / "ios" - apps = sorted(ios_root.glob("*.app")) - return [MobileArtifact("ios", app, directory_names(app)) for app in apps] - fail(f"unsupported mobile platform {platform}") - - -def mobile_prefix(platform: str) -> str: - if platform == "android": - return "assets/oliphaunt/" - if platform == "ios": - return "OliphauntReactNativeResources.bundle/oliphaunt/" - fail(f"unsupported mobile platform {platform}") - - -def mobile_target_for_artifact(artifact: MobileArtifact) -> str: - if artifact.platform == "ios": - return "ios-xcframework" - abis = sorted( - name.split("/", 2)[1] - for name in artifact.names - if name.startswith("lib/") and name.endswith("/liboliphaunt.so") - ) - if len(abis) != 1: - fail(f"{rel(artifact.path)} must contain exactly one Android liboliphaunt ABI, got {abis}") - abi = abis[0] - if abi == "arm64-v8a": - return "android-arm64-v8a" - if abi == "x86_64": - return "android-x86_64" - fail(f"{rel(artifact.path)} contains unsupported Android ABI {abi}") - - -def mobile_build_report(platform: str) -> dict[str, object] | None: - report = MOBILE_ROOT / platform / "build-report.json" - if not report.is_file(): - return None - data = read_json(report) - if data.get("schema") != "oliphaunt-react-native-mobile-build-v1": - fail(f"{rel(report)} has invalid mobile build report schema") - if data.get("platform") != platform: - fail(f"{rel(report)} has platform={data.get('platform')!r}, expected {platform!r}") - return data - - -def resolve_report_path(value: object, report_path: Path, field: str) -> Path: - if not isinstance(value, str) or not value: - fail(f"{rel(report_path)} must declare {field}") - path = Path(value) - if not path.is_absolute(): - path = ROOT / path - return path - - -def check_extension_package_has_mobile_target(sql_name: str, target: str) -> None: - for product in exact_extension_products(): - manifest = EXTENSION_ROOT / product / "extension-artifacts.json" - if not manifest.is_file(): - continue - data = read_json(manifest) - if data.get("sqlName") != sql_name: - continue - assets = data.get("assets") - if not isinstance(assets, list): - fail(f"{rel(manifest)} must declare assets") - runtime_matches = [ - asset - for asset in assets - if isinstance(asset, dict) - and asset.get("family") == "native" - and asset.get("target") == target - and asset.get("kind") == "runtime" - ] - if len(runtime_matches) != 1: - fail(f"{sql_name} exact-extension package must contain one native runtime asset for {target}") - if target == "ios-xcframework": - framework_matches = [ - asset - for asset in assets - if isinstance(asset, dict) - and asset.get("family") == "native" - and asset.get("target") == target - and asset.get("kind") == "ios-xcframework" - ] - if len(framework_matches) != 1: - fail(f"{sql_name} exact-extension package must contain one iOS XCFramework asset") - return - fail(f"no exact-extension package found for selected mobile extension {sql_name}") - - -def check_ios_prebuilt_extension_linkage(artifact: MobileArtifact, stems: list[str]) -> None: - if not stems: - return - - source_leaks = sorted( - name - for name in artifact.names - if "/static-registry/oliphaunt_static_registry.c" in name - or "/extension-frameworks/" in name - or name.endswith(".xcframework") - ) - if source_leaks: - fail( - f"{rel(artifact.path)} includes build-only iOS static-extension inputs as app resources: " - f"{', '.join(source_leaks[:10])}" - ) - - report = mobile_build_report("ios") - if report is None: - fail(f"{rel(artifact.path)} requires {rel(MOBILE_ROOT / 'ios' / 'build-report.json')} for iOS extension link evidence") - scratch_root = report.get("scratchRoot") - if not isinstance(scratch_root, str) or not scratch_root: - fail(f"{rel(MOBILE_ROOT / 'ios' / 'build-report.json')} must declare scratchRoot for iOS extension link evidence") - scratch_path = Path(scratch_root) - xcode_log = scratch_path / "xcodebuild.log" - if not xcode_log.is_file(): - fail(f"iOS extension link evidence is missing xcodebuild log: {rel(xcode_log)}") - log_text = xcode_log.read_text(encoding="utf-8", errors="replace") - if "** BUILD SUCCEEDED **" not in log_text: - fail(f"iOS extension link evidence requires a successful xcodebuild log: {rel(xcode_log)}") - - pods_support = ( - scratch_path - / "src" - / "sdks" - / "react-native" - / "examples" - / "expo" - / "ios" - / "Pods" - / "Target Support Files" - / "OliphauntReactNative" - ) - input_file = pods_support / "OliphauntReactNative-xcframeworks-input-files.xcfilelist" - output_file = pods_support / "OliphauntReactNative-xcframeworks-output-files.xcfilelist" - if not input_file.is_file(): - fail(f"iOS extension link evidence is missing CocoaPods XCFramework input file list: {rel(input_file)}") - if not output_file.is_file(): - fail(f"iOS extension link evidence is missing CocoaPods XCFramework output file list: {rel(output_file)}") - - expected_frameworks = {f"liboliphaunt_extension_{stem}" for stem in stems} - pod_text = input_file.read_text(encoding="utf-8", errors="replace") + "\n" + output_file.read_text( - encoding="utf-8", errors="replace" - ) - pod_frameworks = set(re.findall(r"liboliphaunt_extension_[A-Za-z0-9_]+", pod_text)) - products_root = scratch_path / "DerivedData" / "Build" / "Products" - if not products_root.is_dir(): - fail(f"iOS extension link evidence is missing Xcode build products: {rel(products_root)}") - built_frameworks = { - path.name.removesuffix(".a").removesuffix(".framework") - for path in products_root.rglob("liboliphaunt_extension_*") - if path.name.endswith((".a", ".framework")) - } - - missing_pods = sorted(expected_frameworks - pod_frameworks) - if missing_pods: - fail( - f"CocoaPods file lists do not include selected iOS extension link input(s): " - f"{', '.join(missing_pods)}" - ) - missing_built = sorted(expected_frameworks - built_frameworks) - if missing_built: - fail( - f"Xcode build products do not include selected iOS extension linked artifact(s): " - f"{', '.join(missing_built)}" - ) - unexpected_pods = sorted(pod_frameworks - expected_frameworks) - if unexpected_pods: - fail( - f"CocoaPods file lists include unselected iOS extension link input(s): " - f"{', '.join(unexpected_pods)}" - ) - unexpected_built = sorted(built_frameworks - expected_frameworks) - if unexpected_built: - fail( - f"Xcode build products include unselected iOS extension linked artifact(s): " - f"{', '.join(unexpected_built)}" - ) - - -def check_android_prebuilt_extension_linkage( - artifact: MobileArtifact, - stems: list[str], - report: dict[str, object], - report_path: Path, - expected_abi: str, - static_registry: dict[str, str], - target: str, -) -> None: - if not stems: - return - - evidence_path = resolve_report_path(report.get("androidLinkEvidence"), report_path, "androidLinkEvidence") - if not evidence_path.is_file(): - fail(f"Android extension link evidence is missing: {rel(evidence_path)}") - linked_stems: set[str] = set() - linked_dependencies: set[str] = set() - evidence_abi = "" - runtime_path = "" - schema_rows = 0 - abi_rows = 0 - - def require_existing_path(raw_path: str, line_number: int, row_kind: str) -> Path: - path = Path(raw_path) - if not path.is_absolute(): - path = evidence_path.parent / path - if not path.is_file(): - fail(f"{rel(evidence_path)}:{line_number} {row_kind} path does not exist: {path}") - return path - - for line_number, raw in enumerate(evidence_path.read_text(encoding="utf-8").splitlines(), start=1): - parts = raw.split("\t") - if not parts or not parts[0]: - continue - kind = parts[0] - if kind == "schema": - if parts != ["schema", "oliphaunt-android-static-extension-link-v1"]: - fail(f"{rel(evidence_path)}:{line_number} has invalid schema row") - schema_rows += 1 - elif kind == "abi": - if len(parts) != 2: - fail(f"{rel(evidence_path)}:{line_number} has invalid abi row") - evidence_abi = parts[1] - abi_rows += 1 - elif kind == "runtime": - if len(parts) != 3 or parts[1] != "liboliphaunt": - fail(f"{rel(evidence_path)}:{line_number} has invalid runtime row") - path = require_existing_path(parts[2], line_number, "runtime") - if path.name != "liboliphaunt.so": - fail(f"{rel(evidence_path)}:{line_number} runtime path must end in liboliphaunt.so") - if runtime_path: - fail(f"{rel(evidence_path)} contains duplicate runtime rows") - runtime_path = str(path) - elif kind == "extension": - if len(parts) != 3: - fail(f"{rel(evidence_path)}:{line_number} has invalid extension row") - stem, archive = parts[1], parts[2] - expected_name = f"liboliphaunt_extension_{stem}.a" - path = require_existing_path(archive, line_number, "extension") - expected_relative = static_registry.get(f"module.{stem}.archive.{target}") - if not expected_relative: - fail(f"{rel(artifact.path)} static registry manifest has no module.{stem}.archive.{target} entry") - if path.name != expected_name: - fail(f"{rel(evidence_path)}:{line_number} archive {archive!r} does not match stem {stem!r}") - if not path.as_posix().endswith(expected_relative): - fail( - f"{rel(evidence_path)}:{line_number} archive {archive!r} does not match " - f"static-registry path {expected_relative!r}" - ) - linked_stems.add(stem) - elif kind == "dependency": - if len(parts) != 3 or not parts[1]: - fail(f"{rel(evidence_path)}:{line_number} has invalid dependency row") - dependency_name = parts[1] - path = require_existing_path(parts[2], line_number, "dependency") - expected_relative = static_registry.get(f"dependency.{dependency_name}.archive.{target}") - if not expected_relative: - fail( - f"{rel(evidence_path)}:{line_number} dependency {dependency_name!r} is not declared " - f"by the static-registry manifest for {target}" - ) - if not path.as_posix().endswith(expected_relative): - fail( - f"{rel(evidence_path)}:{line_number} dependency path {parts[2]!r} does not match " - f"static-registry path {expected_relative!r}" - ) - linked_dependencies.add(dependency_name) - else: - fail(f"{rel(evidence_path)}:{line_number} has unknown row kind {kind!r}") - if schema_rows != 1: - fail(f"{rel(evidence_path)} must contain exactly one schema row") - if abi_rows != 1: - fail(f"{rel(evidence_path)} must contain exactly one abi row") - if evidence_abi != expected_abi: - fail(f"{rel(evidence_path)} declares abi={evidence_abi!r}, expected {expected_abi!r}") - if not runtime_path: - fail(f"{rel(evidence_path)} does not show liboliphaunt runtime link input") - expected_stems = set(stems) - missing = sorted(expected_stems - linked_stems) - if missing: - fail( - f"{rel(evidence_path)} does not show selected Android extension archive link input(s): " - f"{', '.join(missing)}" - ) - unexpected = sorted(linked_stems - expected_stems) - if unexpected: - fail( - f"{rel(evidence_path)} shows unselected Android extension archive link input(s): " - f"{', '.join(unexpected)}" - ) - expected_dependencies = set(csv_values(static_registry.get("dependencyArchives"))) - missing_dependencies = sorted(expected_dependencies - linked_dependencies) - if missing_dependencies: - fail( - f"{rel(evidence_path)} does not show required Android extension dependency archive link input(s): " - f"{', '.join(missing_dependencies)}" - ) - unexpected_dependencies = sorted(linked_dependencies - expected_dependencies) - if unexpected_dependencies: - fail( - f"{rel(evidence_path)} shows unselected Android extension dependency archive link input(s): " - f"{', '.join(unexpected_dependencies)}" - ) - - -def check_mobile_artifact(artifact: MobileArtifact, *, require_prebuilt_extensions: bool) -> None: - prefix = mobile_prefix(artifact.platform) - runtime_manifest_name = f"{prefix}runtime/manifest.properties" - static_registry_manifest_name = f"{prefix}static-registry/manifest.properties" - package_size_name = f"{prefix}package-size.tsv" - - runtime = read_properties_text(artifact.read_text(runtime_manifest_name)) - if runtime.get("schema") != "oliphaunt-runtime-resources-v1": - fail(f"{rel(artifact.path)} has invalid runtime resource manifest schema") - selected = csv_values(runtime.get("extensions")) - selected_set = set(selected) - rows = generated_extension_rows() - target = mobile_target_for_artifact(artifact) - - report_path = MOBILE_ROOT / artifact.platform / "build-report.json" - report = mobile_build_report(artifact.platform) - if report is None: - fail(f"{rel(artifact.path)} requires mobile build report {rel(report_path)}") - report_artifact = resolve_report_path(report.get("appArtifact"), report_path, "appArtifact") - if report_artifact.resolve() != artifact.path.resolve(): - fail(f"{rel(report_path)} appArtifact={report_artifact} does not match inspected artifact {artifact.path}") - if report.get("appArtifactBytes") != path_bytes(artifact.path): - fail(f"{rel(report_path)} appArtifactBytes does not match inspected artifact size") - selected_from_report = report.get("selectedExtensions") - if not isinstance(selected_from_report, list): - fail(f"{rel(report_path)} selectedExtensions must be an array") - report_selected = sorted(str(value) for value in selected_from_report if str(value)) - if report_selected != sorted(selected): - fail(f"{rel(report_path)} selectedExtensions={report_selected} must match runtime manifest {sorted(selected)}") - if artifact.platform == "android": - expected_abi = "arm64-v8a" if target == "android-arm64-v8a" else "x86_64" - if report.get("abi") != expected_abi: - fail(f"{rel(report_path)} abi={report.get('abi')!r}, expected {expected_abi!r}") - else: - expected_abi = "" - - extension_asset_names = [ - name - for name in artifact.names - if f"{prefix}runtime/files/share/postgresql/extension/" in name - and (name.endswith(".control") or name.endswith(".sql")) - ] - present_extensions = {extension for name in extension_asset_names if (extension := extension_name_for_asset(name))} - unexpected = sorted(present_extensions - selected_set - BASELINE_POSTGRES_EXTENSIONS) - if unexpected: - fail(f"{rel(artifact.path)} includes unselected extension assets: {', '.join(unexpected)}") - for extension in selected: - if creates_extension(extension, rows): - has_control = any(name.endswith(f"/{extension}.control") for name in extension_asset_names) - has_sql = any(f"/{extension}--" in name and name.endswith(".sql") for name in extension_asset_names) - if not has_control or not has_sql: - fail(f"{rel(artifact.path)} is missing selected {extension} control/SQL assets") - if require_prebuilt_extensions: - check_extension_package_has_mobile_target(extension, target) - - stems = sorted(stem for extension in selected if (stem := native_module_stem(extension, rows)) and stem != "-") - static_registry = read_properties_text(artifact.read_text(static_registry_manifest_name)) - registered = sorted(csv_values(static_registry.get("registeredExtensions"))) - native_selected = native_module_extensions(selected, rows) - if stems: - if runtime.get("mobileStaticRegistryState") != "complete": - fail(f"{rel(artifact.path)} must mark mobile static registry complete for native-module extensions") - if registered != native_selected: - fail(f"{rel(artifact.path)} static registry registeredExtensions={registered}, expected {native_selected}") - if artifact.platform == "android" and not any(name.endswith("/liboliphaunt_extensions.so") for name in artifact.names): - fail(f"{rel(artifact.path)} Android app is missing liboliphaunt_extensions.so") - if artifact.platform == "android" and require_prebuilt_extensions: - check_android_prebuilt_extension_linkage(artifact, stems, report, report_path, expected_abi, static_registry, target) - if artifact.platform == "ios" and require_prebuilt_extensions: - check_ios_prebuilt_extension_linkage(artifact, stems) - if any("static-registry/archives/" in name for name in artifact.names): - fail(f"{rel(artifact.path)} must not ship build-only static-registry archives") - else: - if runtime.get("mobileStaticRegistryState") not in {"", "not-required"}: - fail(f"{rel(artifact.path)} must not claim a static registry for SQL-only extensions") - - package_size = artifact.read_text(package_size_name) - extension_rows = [ - line.split("\t") - for line in package_size.splitlines() - if line.startswith("extension\t") - ] - package_size_extensions = sorted(parts[1] for parts in extension_rows if len(parts) >= 2) - if package_size_extensions != sorted(selected): - fail( - f"{rel(artifact.path)} package-size extension rows {package_size_extensions} " - f"must exactly match selected extensions {sorted(selected)}" - ) - print(f"validated mobile app extension contents: {artifact.platform} {rel(artifact.path)}") - - -def check_mobile_platform(platform: str, *, require: bool, require_prebuilt_extensions: bool) -> bool: - artifacts = discover_mobile_artifacts(platform) - if not artifacts: - if require: - fail(f"missing staged React Native {platform} mobile app artifacts under {rel(MOBILE_ROOT / platform)}") - return False - for artifact in artifacts: - check_mobile_artifact(artifact, require_prebuilt_extensions=require_prebuilt_extensions) - return True - - -def expand_products(values: list[str], *, all_products: set[str], label: str) -> list[str]: - expanded: list[str] = [] - for value in values: - if value == "all": - expanded.extend(sorted(all_products)) - else: - if value not in all_products: - fail(f"unknown {label} {value}; expected one of: all, {', '.join(sorted(all_products))}") - expanded.append(value) - return sorted(set(expanded)) - - -def parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--require-sdk-product", action="append", default=[], help="SDK product to require, or all") - parser.add_argument( - "--require-extension-product", - action="append", - default=[], - help="exact-extension product to require, or all", - ) - parser.add_argument( - "--require-full-extension-targets", - action="store_true", - help="require exact-extension packages to contain every published target", - ) - parser.add_argument( - "--require-mobile", - action="append", - default=[], - choices=["android", "ios", "all"], - help="mobile app artifact platform to require", - ) - parser.add_argument( - "--require-mobile-prebuilt-extensions", - action="store_true", - help="mobile artifacts must have matching staged exact-extension packages for their selected extensions", - ) - parser.add_argument( - "--inspect-present", - action="store_true", - help="also inspect any present staged SDK, extension, and mobile artifacts", - ) - return parser.parse_args(argv) - - -def main(argv: list[str]) -> int: - args = parse_args(argv) - checked = 0 - - required_sdk_products = expand_products( - args.require_sdk_product, - all_products=SDK_PRODUCTS, - label="SDK product", - ) - for product in required_sdk_products: - checked += int(check_sdk_product(product, require=True)) - if args.inspect_present: - for product in sorted(SDK_PRODUCTS - set(required_sdk_products)): - checked += int(check_sdk_product(product, require=False)) - - extension_products = set(exact_extension_products()) - required_extension_products = expand_products( - args.require_extension_product, - all_products=extension_products, - label="exact-extension product", - ) - for product in required_extension_products: - checked += int( - check_extension_product( - product, - require=True, - require_full_targets=args.require_full_extension_targets, - ) - ) - if args.inspect_present: - for product in sorted(extension_products - set(required_extension_products)): - checked += int(check_extension_product(product, require=False, require_full_targets=False)) - - required_mobile = set() - for value in args.require_mobile: - if value == "all": - required_mobile.update({"android", "ios"}) - else: - required_mobile.add(value) - for platform in sorted(required_mobile): - checked += int( - check_mobile_platform( - platform, - require=True, - require_prebuilt_extensions=args.require_mobile_prebuilt_extensions, - ) - ) - if args.inspect_present: - for platform in sorted({"android", "ios"} - required_mobile): - checked += int( - check_mobile_platform( - platform, - require=False, - require_prebuilt_extensions=args.require_mobile_prebuilt_extensions, - ) - ) - - if checked == 0: - fail("no staged artifacts were checked; pass --require-* or --inspect-present") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) diff --git a/tools/release/release.py b/tools/release/release.py index f7437052..b6674a45 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -1687,7 +1687,7 @@ def run_extension_maven_artifact_dry_run(product: str) -> None: def validate_staged_sdk_package(product: str) -> None: - run(["python3", "tools/release/check_staged_artifacts.py", "--require-sdk-product", product]) + run(["tools/dev/bun.sh", "tools/release/check-staged-artifacts.mjs", "--require-sdk-product", product]) def command_prepare_rust_release_source(passthrough: list[str]) -> None: From aa76bfb790e00922c8cca28c6e9ed6f307d5ce27 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 03:22:27 +0000 Subject: [PATCH 144/308] chore: port native cargo artifact packager to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 20 + examples/README.md | 2 +- src/sdks/rust/tools/check-sdk.sh | 2 +- tools/policy/python-entrypoints.allowlist | 1 - tools/release/check_artifact_targets.py | 6 +- tools/release/check_consumer_shape.py | 22 +- tools/release/check_release_metadata.py | 18 +- tools/release/local_registry_publish.py | 4 +- .../package-liboliphaunt-cargo-artifacts.mjs | 1021 +++++++++++++++++ .../package_liboliphaunt_cargo_artifacts.py | 989 ---------------- tools/release/release.py | 31 +- 11 files changed, 1086 insertions(+), 1030 deletions(-) create mode 100644 tools/release/package-liboliphaunt-cargo-artifacts.mjs delete mode 100644 tools/release/package_liboliphaunt_cargo_artifacts.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 2352dc0e..190ce41a 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,26 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Ported native liboliphaunt Cargo artifact crate packaging from + Python to Bun as `tools/release/package-liboliphaunt-cargo-artifacts.mjs`. + Release publishing, local-registry Cargo package synthesis, the Rust SDK + package-shape fixture, and example staging docs now use the pinned Bun + launcher. `release.py` no longer imports the packager module and keeps only + the trivial native/tool crate-name helper it needs for release-source + rendering. Fresh parity/checks passed: old Python and new Bun Linux + `linux-x64-gnu` fixture package generation with matching normalized + `packages.json`, matching generated crate member lists, and equal crate byte + sizes; `python3 tools/release/check_artifact_targets.py`, `python3 + tools/release/check_release_metadata.py`, `python3 + tools/release/check_consumer_shape.py --products-json + '["liboliphaunt-native","oliphaunt-rust"]'`, and `python3 -m py_compile` for + touched Python release/policy callers. Follow-up validation passed: + `tools/dev/bun.sh tools/policy/check-python-entrypoints.mjs --list`, `bash + tools/policy/check-tooling-stack.sh`, `bash tools/policy/check-repo-structure.sh`, + `python3 tools/policy/check-release-policy.py`, full `python3 + tools/release/check_consumer_shape.py`, `tools/release/release.py check`, `bash + src/sdks/rust/tools/check-sdk.sh package-shape`, and `git diff --check + --cached && git diff --check`. - 2026-06-27: Ported staged artifact validation from Python to Bun as `tools/release/check-staged-artifacts.mjs`. CI mobile validation, SDK package staging, release SDK validation, and mobile exact-extension package assembly diff --git a/examples/README.md b/examples/README.md index e4bc95c8..36dda8b2 100644 --- a/examples/README.md +++ b/examples/README.md @@ -21,7 +21,7 @@ staged with: ```sh python3 tools/release/local_registry_publish.py download --run-id 28049923289 --preset local-publish -python3 tools/release/package_liboliphaunt_cargo_artifacts.py \ +tools/dev/bun.sh tools/release/package-liboliphaunt-cargo-artifacts.mjs \ --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 diff --git a/src/sdks/rust/tools/check-sdk.sh b/src/sdks/rust/tools/check-sdk.sh index cd7ae46f..a2cfcee5 100755 --- a/src/sdks/rust/tools/check-sdk.sh +++ b/src/sdks/rust/tools/check-sdk.sh @@ -166,7 +166,7 @@ check_broker_cargo_relay_fixture() { 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 \ + run tools/dev/bun.sh tools/release/package-liboliphaunt-cargo-artifacts.mjs \ --asset-dir "$liboliphaunt_fixture_assets" \ --output-dir "$liboliphaunt_cargo_artifacts" \ --version "$liboliphaunt_version" \ diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index 0dfe9be5..f58be08d 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -8,7 +8,6 @@ tools/release/check_consumer_shape.py tools/release/check_release_metadata.py tools/release/extension_artifact_targets.py tools/release/local_registry_publish.py -tools/release/package_liboliphaunt_cargo_artifacts.py tools/release/package_liboliphaunt_wasix_cargo_artifacts.py tools/release/product_metadata.py tools/release/release.py diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py index 7c485c3f..7e70b5de 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -907,7 +907,7 @@ def validate_ci_release_artifacts() -> None: ) require_text( "tools/release/release.py", - "package_liboliphaunt_cargo_artifacts.py", + "package-liboliphaunt-cargo-artifacts.mjs", "liboliphaunt native Cargo artifact packages must be generated from staged native release assets", ) require_text( @@ -971,8 +971,8 @@ def validate_ci_release_artifacts() -> None: "liboliphaunt npm artifact packages must include the selected platform runtime tree", ) require_text( - "tools/release/package_liboliphaunt_cargo_artifacts.py", - "optimize_native_payload(", + "tools/release/package-liboliphaunt-cargo-artifacts.mjs", + "optimizeNativePayload(", "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 b69fff8d..62a06949 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -469,7 +469,7 @@ 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_packager = read_text("tools/release/package-liboliphaunt-cargo-artifacts.mjs") 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") @@ -488,13 +488,13 @@ def check_liboliphaunt(findings: list[Finding]) -> None: 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 - 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 - and 'tool_set="tools"' in native_packager + and "extractArchive(toolsArchive, toolsRoot)" in native_packager + and "validateToolsTargetPair" in native_packager + and "writeToolsFacadeCrate" in native_packager + and "packageBase: TOOLS_PRODUCT" in native_packager + and "artifactProduct: TOOLS_PRODUCT" in native_packager + and 'toolSet: "runtime"' in native_packager + and 'toolSet: "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 @@ -507,7 +507,7 @@ def check_liboliphaunt(findings: list[Finding]) -> None: "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.mjs", - "tools/release/package_liboliphaunt_cargo_artifacts.py", + "tools/release/package-liboliphaunt-cargo-artifacts.mjs", "tools/release/release.py", *native_runtime_package_split_failures, *native_tools_package_split_failures, @@ -565,10 +565,10 @@ def check_liboliphaunt(findings: list[Finding]) -> None: severity="P0", ) for required in [ - "package_liboliphaunt_cargo_artifacts.py", + "package-liboliphaunt-cargo-artifacts.mjs", "publish_liboliphaunt_cargo_artifacts", "liboliphaunt_cargo_artifact_crates", - "package_liboliphaunt_cargo_artifacts.cargo_package_name", + "liboliphaunt_cargo_package_name", ]: require( findings, diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 50e6c602..335312c6 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -395,7 +395,7 @@ def validate_local_registry_publisher() -> None: 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-liboliphaunt-cargo-artifacts.mjs" 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 @@ -1498,20 +1498,20 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None 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") + native_packager_source = read_text("tools/release/package-liboliphaunt-cargo-artifacts.mjs") native_optimizer_source = read_text("tools/release/optimize_native_runtime_payload.mjs") if ( 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 - 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 - or 'artifact_product=TOOLS_PRODUCT' not in native_packager_source + or "extractArchive(toolsArchive, toolsRoot)" not in native_packager_source + or "validateToolsTargetPair" not in native_packager_source + or "writeToolsFacadeCrate" not in native_packager_source + or 'toolSet: "runtime"' not in native_packager_source + or 'toolSet: "tools"' not in native_packager_source + or "packageBase: TOOLS_PRODUCT" not in native_packager_source + or "artifactProduct: 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") diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index d83e3da0..76b0cb0a 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -2463,8 +2463,8 @@ def stage_release_asset_cargo_packages( ) run( [ - "python3", - "tools/release/package_liboliphaunt_cargo_artifacts.py", + "tools/dev/bun.sh", + "tools/release/package-liboliphaunt-cargo-artifacts.mjs", "--version", lib_version, "--output-dir", diff --git a/tools/release/package-liboliphaunt-cargo-artifacts.mjs b/tools/release/package-liboliphaunt-cargo-artifacts.mjs new file mode 100644 index 00000000..3804c7e4 --- /dev/null +++ b/tools/release/package-liboliphaunt-cargo-artifacts.mjs @@ -0,0 +1,1021 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; +import { + closeSync, + copyFileSync, + cpSync, + existsSync, + mkdirSync, + openSync, + readdirSync, + readFileSync, + readSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; +import path from "node:path"; + +import { + ROOT, + allArtifactTargets, + compareText, + currentProductVersion, +} from "./release-artifact-targets.mjs"; + +const PREFIX = "package-liboliphaunt-cargo-artifacts.mjs"; +const PRODUCT = "liboliphaunt-native"; +const KIND = "native-runtime"; +const TOOLS_PRODUCT = "oliphaunt-tools"; +const TOOLS_KIND = "native-tools"; +const TOOLS_FACADE_TEMPLATE = path.join(ROOT, "src/runtimes/liboliphaunt/native/crates/tools"); +const SURFACE = "rust-native-direct"; +const CRATES_IO_MAX_BYTES = 10 * 1024 * 1024; +const DEFAULT_PART_BYTES = 7 * 1024 * 1024; + +const AGGREGATOR_BUILD_RS = String.raw`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 = __VERSION__; +const KIND: &str = __KIND__; +const TARGET: &str = __TARGET__; +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 liboliphaunt native payload"); + } + fs::create_dir_all(&payload).expect("create liboliphaunt native 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 liboliphaunt native 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 payload files"); + collect_chunks(&root.join("chunks"), &root.join("chunks"), &mut chunk_files) + .expect("collect 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 liboliphaunt chunk indexes for {relative}"); + } + } + let output = payload.join(&relative); + if let Some(parent) = output.parent() { + fs::create_dir_all(parent).expect("create reconstructed file parent"); + } + let mut writer = fs::File::create(&output).expect("create reconstructed payload file"); + for (_, path) in chunks { + let mut reader = fs::File::open(&path).expect("open payload chunk"); + io::copy(&mut reader, &mut writer).expect("append payload chunk"); + } + } + + let files = collect_files(&payload).expect("collect reconstructed liboliphaunt payload files"); + if files.is_empty() { + panic!("liboliphaunt native 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:?}\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 liboliphaunt payload file"); + text.push_str(&format!( + "\n[[files]]\nsource = {:?}\nrelative = {:?}\nsha256 = {:?}\nexecutable = {}\n", + file.display().to_string(), + relative, + sha256, + is_executable_relative(&relative), + )); + } + fs::write(&manifest, text).expect("write liboliphaunt native 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 liboliphaunt 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) +} + +fn is_executable_relative(relative: &str) -> bool { + relative.starts_with("runtime/bin/") || relative.starts_with("bin/") +} +`; + +function fail(message) { + console.error(`${PREFIX}: ${message}`); + process.exit(1); +} + +function rel(file) { + const relative = path.relative(ROOT, String(file)); + if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) { + return String(file).split(path.sep).join("/"); + } + return relative.split(path.sep).join("/"); +} + +function repoPath(value) { + return path.isAbsolute(value) ? value : path.join(ROOT, value); +} + +function run(args, { env = process.env, capture = false } = {}) { + console.log(`\n==> ${args.join(" ")}`); + const result = spawnSync(args[0], args.slice(1), { + cwd: ROOT, + env, + encoding: capture ? "utf8" : "buffer", + stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit", + maxBuffer: 256 * 1024 * 1024, + }); + if (result.error !== undefined) { + fail(`${args[0]} failed to start: ${result.error.message}`); + } + if (result.status !== 0) { + if (capture) { + process.stderr.write(result.stderr ?? ""); + } + process.exit(result.status ?? 1); + } + return capture ? result.stdout : ""; +} + +function isFile(file) { + try { + return statSync(file).isFile(); + } catch { + return false; + } +} + +function isDirectory(file) { + try { + return statSync(file).isDirectory(); + } catch { + return false; + } +} + +function cargoPackageName(targetId, { packageBase = PRODUCT } = {}) { + return `${packageBase}-${targetId}`; +} + +function cargoLinksName(targetId, { artifactProduct = PRODUCT } = {}) { + return `oliphaunt_artifact_${artifactProduct.replaceAll("-", "_")}_${targetId.replaceAll("-", "_")}`; +} + +function partPackageName(targetId, index, { packageBase = PRODUCT } = {}) { + return `${cargoPackageName(targetId, { packageBase })}-part-${String(index).padStart(3, "0")}`; +} + +function partLinksName(targetId, index, { artifactProduct = PRODUCT } = {}) { + return `oliphaunt_artifact_part_${artifactProduct.replaceAll("-", "_")}_${targetId.replaceAll("-", "_")}_${String(index).padStart(3, "0")}`; +} + +function rustCrateIdent(crateName) { + return crateName.replaceAll("-", "_"); +} + +function tomlString(value) { + return JSON.stringify(value); +} + +function artifactAssetName(target, version) { + return target.asset.replaceAll("{version}", version); +} + +function checkedMemberPath(name, archive) { + const normalized = name.replaceAll("\\", "/"); + if (!normalized || normalized === "." || normalized === "./" || normalized.startsWith("/") || normalized.includes("\0")) { + fail(`${rel(archive)} contains unsafe archive member ${JSON.stringify(name)}`); + } + const parts = normalized.split("/").filter((part) => part && part !== "."); + if (parts.length === 0 || parts.includes("..")) { + fail(`${rel(archive)} contains unsafe archive member ${JSON.stringify(name)}`); + } + return parts.join("/"); +} + +function archiveNames(archive) { + const command = archive.endsWith(".zip") ? ["unzip", "-Z1", archive] : ["tar", "-tf", archive]; + const output = run(command, { capture: true }); + return output.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean); +} + +function extractArchive(archive, destination) { + rmSync(destination, { recursive: true, force: true }); + mkdirSync(destination, { recursive: true }); + for (const name of archiveNames(archive)) { + if (name === "." || name === "./" || name.endsWith("/")) { + continue; + } + checkedMemberPath(name, archive); + } + const command = archive.endsWith(".zip") + ? ["unzip", "-qq", archive, "-d", destination] + : ["tar", "-xf", archive, "-C", destination]; + run(command); +} + +function optimizeNativePayload(payloadRoot, target, { toolSet }) { + run([ + "tools/dev/bun.sh", + "tools/release/optimize_native_runtime_payload.mjs", + payloadRoot, + "--target", + target, + "--tool-set", + toolSet, + ]); +} + +function writePartCrate( + crateDir, + { + targetId, + index, + version, + packageBase, + artifactProduct, + artifactLabel, + }, +) { + rmSync(crateDir, { recursive: true, force: true }); + const name = partPackageName(targetId, index, { packageBase }); + const links = partLinksName(targetId, index, { artifactProduct }); + mkdirSync(path.join(crateDir, "src"), { recursive: true }); + writeFileSync( + path.join(crateDir, "Cargo.toml"), + `[package] +name = "${name}" +version = "${version}" +edition = "2024" +rust-version = "1.93" +description = "Cargo payload part ${String(index).padStart(3, "0")} for the ${targetId} ${artifactLabel}." +readme = "README.md" +repository = "https://github.com/f0rr0/oliphaunt" +homepage = "https://oliphaunt.dev" +license = "MIT AND Apache-2.0 AND PostgreSQL" +links = "${links}" +build = "build.rs" +include = ["Cargo.toml", "README.md", "build.rs", "src/**", "payload/**"] + +[lib] +path = "src/lib.rs" + +[workspace] +`, + ); + writeFileSync( + path.join(crateDir, "README.md"), + `# ${name} + +Cargo payload part for the \`${targetId}\` ${artifactLabel}. +Applications do not depend on this crate directly. +`, + ); + writeFileSync( + path.join(crateDir, "src/lib.rs"), + `pub const RELEASE_TARGET: &str = "${targetId}"; +pub const PART_INDEX: usize = ${index}; +pub const PAYLOAD_ROOT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/payload"); +`, + ); + writeFileSync( + path.join(crateDir, "build.rs"), + `use std::env; +use std::path::PathBuf; + +fn main() { + let manifest_dir = + PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set")); + let root = manifest_dir.join("payload"); + 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 Oliphaunt artifact payload under {}", root.display()); + } + return; + } + println!("cargo::metadata=root={}", root.display()); +} +`, + ); +} + +function writeAggregatorCrate( + crateDir, + { + target, + version, + partCount, + packageBase, + artifactProduct, + artifactKind, + artifactLabel, + }, +) { + rmSync(crateDir, { recursive: true, force: true }); + if (typeof target.triple !== "string" || !target.triple) { + fail(`${target.id} must declare Cargo target triple`); + } + const name = cargoPackageName(target.target, { packageBase }); + const links = cargoLinksName(target.target, { artifactProduct }); + mkdirSync(path.join(crateDir, "src"), { recursive: true }); + const dependencyLines = []; + const partRoots = []; + for (let index = 0; index < partCount; index += 1) { + const partName = partPackageName(target.target, index, { packageBase }); + dependencyLines.push(`${partName} = { version = "=${version}" }`); + partRoots.push(` ${rustCrateIdent(partName)}::PAYLOAD_ROOT,`); + } + const libraryRelativePath = target.libraryRelativePath ?? ""; + writeFileSync( + path.join(crateDir, "Cargo.toml"), + `[package] +name = "${name}" +version = "${version}" +edition = "2024" +rust-version = "1.93" +description = "Cargo artifact crate for the ${target.target} ${artifactLabel}." +readme = "README.md" +repository = "https://github.com/f0rr0/oliphaunt" +homepage = "https://oliphaunt.dev" +license = "MIT AND Apache-2.0 AND PostgreSQL" +links = "${links}" +build = "build.rs" +include = ["Cargo.toml", "README.md", "build.rs", "src/**"] + +[lib] +path = "src/lib.rs" + +[build-dependencies] +sha2 = "0.10" +${dependencyLines.join("\n")} + +[workspace] +`, + ); + writeFileSync( + path.join(crateDir, "README.md"), + `# ${name} + +Cargo artifact crate for the \`${target.target}\` ${artifactLabel}. +Applications do not depend on this crate directly; \`oliphaunt\` selects it for +matching Cargo targets. +`, + ); + writeFileSync( + path.join(crateDir, "src/lib.rs"), + `pub const PRODUCT: &str = "${artifactProduct}"; +pub const KIND: &str = "${artifactKind}"; +pub const RELEASE_TARGET: &str = "${target.target}"; +pub const CARGO_TARGET: &str = "${target.triple}"; +pub const LIBRARY_RELATIVE_PATH: &str = "${libraryRelativePath}"; +`, + ); + writeFileSync( + path.join(crateDir, "build.rs"), + AGGREGATOR_BUILD_RS + .replace("__SCHEMA__", tomlString("oliphaunt-artifact-manifest-v1")) + .replace("__PRODUCT__", tomlString(artifactProduct)) + .replace("__VERSION__", tomlString(version)) + .replace("__KIND__", tomlString(artifactKind)) + .replace("__TARGET__", tomlString(target.triple)) + .replace("__PART_ROOTS__", partRoots.join("\n")), + ); +} + +function walkFiles(root) { + const files = []; + const visit = (current) => { + if (!existsSync(current)) { + return; + } + for (const entry of readdirSync(current, { withFileTypes: true })) { + const file = path.join(current, entry.name); + if (entry.isDirectory()) { + visit(file); + } else if (entry.isFile()) { + files.push(file); + } + } + }; + visit(root); + return files.sort(compareText); +} + +function nextPartDir( + sourceRoot, + targetId, + index, + version, + { + packageBase, + artifactProduct, + artifactLabel, + }, +) { + const crateDir = path.join(sourceRoot, partPackageName(targetId, index, { packageBase })); + writePartCrate(crateDir, { + targetId, + index, + version, + packageBase, + artifactProduct, + artifactLabel, + }); + return crateDir; +} + +function writeChunk(file, data) { + mkdirSync(path.dirname(file), { recursive: true }); + writeFileSync(file, data); +} + +function copyPayloadFile(source, destination) { + mkdirSync(path.dirname(destination), { recursive: true }); + copyFileSync(source, destination); +} + +function buildPartCrates( + extractedRoot, + sourceRoot, + { + targetId, + version, + partBytes, + packageBase, + artifactProduct, + artifactLabel, + }, +) { + const partDirs = []; + let currentDir; + let currentSize = 0; + const startPart = () => { + const partDir = nextPartDir(sourceRoot, targetId, partDirs.length, version, { + packageBase, + artifactProduct, + artifactLabel, + }); + partDirs.push(partDir); + return partDir; + }; + + for (const source of walkFiles(extractedRoot)) { + const relative = path.relative(extractedRoot, source).split(path.sep).join("/"); + const size = statSync(source).size; + if (size > partBytes) { + currentDir = undefined; + currentSize = 0; + const fd = openSync(source, "r"); + try { + let partIndex = 0; + let offset = 0; + while (offset < size) { + const length = Math.min(partBytes, size - offset); + const buffer = Buffer.allocUnsafe(length); + const bytesRead = readSync(fd, buffer, 0, length, offset); + if (bytesRead <= 0) { + break; + } + const partDir = startPart(); + writeChunk( + path.join(partDir, "payload/chunks", `${relative}.part${String(partIndex).padStart(3, "0")}`), + buffer.subarray(0, bytesRead), + ); + offset += bytesRead; + partIndex += 1; + } + } finally { + closeSync(fd); + } + continue; + } + if (currentDir === undefined || currentSize + size > partBytes) { + currentDir = startPart(); + currentSize = 0; + } + copyPayloadFile(source, path.join(currentDir, "payload/files", relative)); + currentSize += size; + } + if (partDirs.length === 0) { + fail(`${targetId} generated no ${artifactLabel} part crates`); + } + return partDirs; +} + +function cargoPackage(crateDir, targetDir, { noVerify = false } = {}) { + const manifest = path.join(crateDir, "Cargo.toml"); + const metadata = Bun.TOML.parse(readFileSync(manifest, "utf8")); + const name = metadata?.package?.name; + const version = metadata?.package?.version; + if (typeof name !== "string" || typeof version !== "string") { + fail(`${rel(manifest)} must declare package.name and package.version`); + } + const command = [ + "cargo", + "package", + "--manifest-path", + manifest, + "--target-dir", + targetDir, + "--allow-dirty", + ]; + if (noVerify) { + command.push("--no-verify"); + } + run(command, { env: { ...process.env, OLIPHAUNT_ARTIFACT_CRATE_REQUIRE_PAYLOAD: "1" } }); + const cratePath = path.join(targetDir, "package", `${name}-${version}.crate`); + if (!isFile(cratePath)) { + fail(`cargo package did not create ${rel(cratePath)}`); + } + return cratePath; +} + +function validateCrateSize(cratePath) { + const size = statSync(cratePath).size; + if (size > CRATES_IO_MAX_BYTES) { + fail(`${rel(cratePath)} is ${size} bytes, above the crates.io 10 MiB package limit`); + } +} + +function validateToolsTargetPair(runtimeTarget, toolsTarget) { + if (toolsTarget.target !== runtimeTarget.target) { + fail(`${toolsTarget.id} must use target ${runtimeTarget.target}`); + } + if (toolsTarget.triple !== runtimeTarget.triple) { + fail(`${toolsTarget.id} must use Cargo target triple ${runtimeTarget.triple}`); + } +} + +function rustArtifactCargoTargetCfg(target) { + 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(`unsupported Cargo target cfg for ${target.id}`); +} + +function writeToolsFacadeCrate(sourceRoot, { version, toolsTargets }) { + const crateDir = path.join(sourceRoot, TOOLS_PRODUCT); + if (existsSync(crateDir)) { + fail(`duplicate generated ${TOOLS_PRODUCT} source crate: ${rel(crateDir)}`); + } + cpSync(TOOLS_FACADE_TEMPLATE, crateDir, { + recursive: true, + filter: (source) => path.basename(source) !== "target", + }); + const cargoToml = path.join(crateDir, "Cargo.toml"); + let text = readFileSync(cargoToml, "utf8"); + text = text + .replace("repository.workspace = true", 'repository = "https://github.com/f0rr0/oliphaunt"') + .replace("homepage.workspace = true", 'homepage = "https://oliphaunt.dev"'); + const versionMatches = text.match(/^version = "[^"]+"$/gm) ?? []; + if (versionMatches.length !== 1) { + fail(`${rel(cargoToml)} must declare exactly one package version`); + } + text = text.replace(/^version = "[^"]+"$/m, `version = "${version}"`); + const dependencyBlocks = []; + for (const target of [...toolsTargets].sort((left, right) => compareText(left.target, right.target))) { + const packageName = cargoPackageName(target.target, { packageBase: TOOLS_PRODUCT }); + dependencyBlocks.push( + [ + "", + `[target.'cfg(${rustArtifactCargoTargetCfg(target)})'.dependencies]`, + `${packageName} = { version = "=${version}", path = "../${packageName}" }`, + ].join("\n"), + ); + } + if (!text.includes("\n[workspace]")) { + text = `${text.trimEnd()}\n\n[workspace]\n`; + } + writeFileSync(cargoToml, `${text.trimEnd()}\n${dependencyBlocks.join("\n")}\n`); + return { + name: TOOLS_PRODUCT, + manifestPath: cargoToml, + cratePath: null, + target: "portable", + product: TOOLS_PRODUCT, + kind: TOOLS_KIND, + role: "facade", + index: null, + }; +} + +function packagePayload( + payloadRoot, + sourceRoot, + outputDir, + cargoTargetDir, + { + target, + version, + partBytes, + packageBase, + artifactProduct, + artifactKind, + artifactLabel, + }, +) { + const partDirs = buildPartCrates(payloadRoot, sourceRoot, { + targetId: target.target, + version, + partBytes, + packageBase, + artifactProduct, + artifactLabel, + }); + const aggregatorDir = path.join(sourceRoot, cargoPackageName(target.target, { packageBase })); + writeAggregatorCrate(aggregatorDir, { + target, + version, + partCount: partDirs.length, + packageBase, + artifactProduct, + artifactKind, + artifactLabel, + }); + + const packages = []; + for (let index = 0; index < partDirs.length; index += 1) { + const partDir = partDirs[index]; + const cratePath = cargoPackage(partDir, cargoTargetDir); + validateCrateSize(cratePath); + const output = path.join(outputDir, path.basename(cratePath)); + copyFileSync(cratePath, output); + packages.push({ + name: partPackageName(target.target, index, { packageBase }), + manifestPath: path.join(partDir, "Cargo.toml"), + cratePath: output, + target: target.target, + product: artifactProduct, + kind: artifactKind, + role: "part", + index, + }); + } + packages.push({ + name: cargoPackageName(target.target, { packageBase }), + manifestPath: path.join(aggregatorDir, "Cargo.toml"), + cratePath: null, + target: target.target, + product: artifactProduct, + kind: artifactKind, + role: "aggregator", + index: null, + }); + return packages; +} + +function packageTarget( + target, + { + toolsTarget, + version, + assetDir, + sourceRoot, + outputDir, + cargoTargetDir, + partBytes, + }, +) { + validateToolsTargetPair(target, toolsTarget); + const archive = path.join(assetDir, artifactAssetName(target, version)); + if (!isFile(archive)) { + fail(`missing liboliphaunt native release asset: ${rel(archive)}`); + } + const toolsArchive = path.join(assetDir, artifactAssetName(toolsTarget, version)); + if (!isFile(toolsArchive)) { + fail(`missing oliphaunt-tools native release asset: ${rel(toolsArchive)}`); + } + const extractedRoot = path.join(sourceRoot, `${target.target}-extracted`); + extractArchive(archive, extractedRoot); + const toolsRoot = path.join(sourceRoot, `${target.target}-tools-extracted`); + extractArchive(toolsArchive, toolsRoot); + optimizeNativePayload(extractedRoot, target.target, { toolSet: "runtime" }); + optimizeNativePayload(toolsRoot, target.target, { toolSet: "tools" }); + return [ + ...packagePayload(extractedRoot, sourceRoot, outputDir, cargoTargetDir, { + target, + version, + partBytes, + packageBase: PRODUCT, + artifactProduct: PRODUCT, + artifactKind: KIND, + artifactLabel: "liboliphaunt native runtime", + }), + ...packagePayload(toolsRoot, sourceRoot, outputDir, cargoTargetDir, { + target: toolsTarget, + version, + partBytes, + packageBase: TOOLS_PRODUCT, + artifactProduct: TOOLS_PRODUCT, + artifactKind: TOOLS_KIND, + artifactLabel: "Oliphaunt native tools", + }), + ]; +} + +function writePackagesManifest(packages, outputDir) { + const data = { + schema: "oliphaunt-liboliphaunt-cargo-artifacts-v1", + product: PRODUCT, + packages: packages.map((item) => ({ + name: item.name, + target: item.target, + product: item.product, + kind: item.kind, + role: item.role, + index: item.index, + manifestPath: rel(item.manifestPath), + cratePath: item.cratePath === null ? null : rel(item.cratePath), + })), + }; + writeFileSync(path.join(outputDir, "packages.json"), `${JSON.stringify(data, null, 2)}\n`); +} + +function usage() { + fail( + "usage: tools/release/package-liboliphaunt-cargo-artifacts.mjs [--asset-dir DIR] [--output-dir DIR] [--version VERSION] [--target TARGET]... [--part-bytes BYTES]", + ); +} + +function help() { + console.log(`usage: tools/release/package-liboliphaunt-cargo-artifacts.mjs [options] + +Options: + --asset-dir DIR directory containing checked liboliphaunt native release assets + --output-dir DIR directory where generated .crate files are written + --version VERSION release version to package + --target TARGET release target id to package; may be repeated + --part-bytes BYTES maximum raw payload bytes per generated part crate + -h, --help show this help +`); +} + +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/liboliphaunt/release-assets", + outputDir: "target/liboliphaunt/cargo-artifacts", + version: undefined, + targets: [], + partBytes: DEFAULT_PART_BYTES, + }; + for (let index = 0; 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 === "--version") { + args.version = optionValue(argv, index); + index += 2; + } else if (arg === "--target") { + args.targets.push(optionValue(argv, index)); + index += 2; + } else if (arg === "--part-bytes") { + const parsed = Number.parseInt(optionValue(argv, index), 10); + if (!Number.isInteger(parsed)) { + usage(); + } + args.partBytes = parsed; + index += 2; + } else if (arg === "-h" || arg === "--help") { + help(); + process.exit(0); + } else { + usage(); + } + } + return { + assetDir: repoPath(args.assetDir), + outputDir: repoPath(args.outputDir), + version: args.version ?? await currentProductVersion(PRODUCT, PREFIX), + targets: args.targets, + partBytes: args.partBytes, + }; +} + +async function main(argv) { + const args = await parseArgs(argv); + if (!isDirectory(args.assetDir)) { + fail(`liboliphaunt release asset directory does not exist: ${rel(args.assetDir)}`); + } + if (args.partBytes <= 0 || args.partBytes > DEFAULT_PART_BYTES) { + fail(`--part-bytes must be between 1 and ${DEFAULT_PART_BYTES}`); + } + const selected = new Set(args.targets); + const sourceRoot = path.join(ROOT, "target/liboliphaunt/cargo-package-sources"); + const cargoTargetDir = path.join(ROOT, "target/liboliphaunt/cargo-package-target"); + rmSync(sourceRoot, { recursive: true, force: true }); + rmSync(args.outputDir, { recursive: true, force: true }); + rmSync(cargoTargetDir, { recursive: true, force: true }); + mkdirSync(sourceRoot, { recursive: true }); + mkdirSync(args.outputDir, { recursive: true }); + + let targets = allArtifactTargets( + { product: PRODUCT, kind: KIND, surface: SURFACE, publishedOnly: true }, + PREFIX, + ); + const toolsTargets = new Map( + allArtifactTargets( + { product: PRODUCT, kind: TOOLS_KIND, surface: SURFACE, publishedOnly: true }, + PREFIX, + ).map((target) => [target.target, target]), + ); + if (selected.size > 0) { + const known = new Set(targets.map((target) => target.target)); + const unknown = [...selected].filter((target) => !known.has(target)).sort(compareText); + if (unknown.length > 0) { + fail(`unknown liboliphaunt native Rust target(s): ${unknown.join(", ")}`); + } + targets = targets.filter((target) => selected.has(target.target)); + } + + const packages = []; + const selectedToolsTargets = []; + for (const target of targets) { + const toolsTarget = toolsTargets.get(target.target); + if (toolsTarget === undefined) { + fail(`missing oliphaunt-tools Cargo artifact target for ${target.target}`); + } + selectedToolsTargets.push(toolsTarget); + packages.push(...packageTarget(target, { + toolsTarget, + version: args.version, + assetDir: args.assetDir, + sourceRoot, + outputDir: args.outputDir, + cargoTargetDir, + partBytes: args.partBytes, + })); + } + packages.push(writeToolsFacadeCrate(sourceRoot, { + version: args.version, + toolsTargets: selectedToolsTargets, + })); + writePackagesManifest(packages, args.outputDir); + console.log("generated liboliphaunt native Cargo artifact crates:"); + for (const item of packages) { + console.log(`${item.name} ${item.role} ${item.cratePath === null ? "" : rel(item.cratePath)}`); + } +} + +await main(Bun.argv.slice(2)); diff --git a/tools/release/package_liboliphaunt_cargo_artifacts.py b/tools/release/package_liboliphaunt_cargo_artifacts.py deleted file mode 100644 index 2da8b284..00000000 --- a/tools/release/package_liboliphaunt_cargo_artifacts.py +++ /dev/null @@ -1,989 +0,0 @@ -#!/usr/bin/env python3 -"""Package liboliphaunt native runtime archives as Cargo artifact crates.""" - -from __future__ import annotations - -import argparse -import hashlib -import json -import os -import re -import shutil -import subprocess -import sys -import tarfile -import zipfile -from dataclasses import dataclass -from pathlib import Path, PurePosixPath -from typing import NoReturn - -import artifact_targets -import product_metadata - - -ROOT = Path(__file__).resolve().parents[2] -PRODUCT = "liboliphaunt-native" -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 - - -@dataclass(frozen=True) -class GeneratedPackage: - name: str - manifest_path: Path - crate_path: Path | None - target: str - product: str - kind: str - role: str - index: int | None = None - - -def fail(message: str) -> NoReturn: - print(f"package_liboliphaunt_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, *, 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('-', '_')}" - - -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, *, 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: - return crate_name.replace("-", "_") - - -def checked_member_path(name: str, archive: Path) -> PurePosixPath: - path = PurePosixPath(name) - parts = tuple(part for part in path.parts if part not in {"", "."}) - if not parts or any(part == ".." for part in parts) or path.is_absolute(): - fail(f"{rel(archive)} contains unsafe archive member {name!r}") - return PurePosixPath(*parts) - - -def extract_archive(archive_path: Path, destination: Path) -> None: - shutil.rmtree(destination, ignore_errors=True) - destination.mkdir(parents=True, exist_ok=True) - if archive_path.name.endswith(".zip"): - try: - with zipfile.ZipFile(archive_path) as archive: - for info in archive.infolist(): - if info.is_dir() or info.filename.rstrip("/") in {"", ".", "./"}: - continue - member = checked_member_path(info.filename, archive_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"{rel(archive_path)} is not a readable zip archive: {error}") - return - - try: - with tarfile.open(archive_path, "r:*") as archive: - for info in archive.getmembers(): - if info.isdir() or info.name.rstrip("/") in {"", ".", "./"}: - continue - if not info.isfile(): - fail(f"{rel(archive_path)} member {info.name} must be a regular file") - member = checked_member_path(info.name, archive_path) - extracted = archive.extractfile(info) - if extracted is None: - fail(f"{rel(archive_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"{rel(archive_path)} is not a readable tar archive: {error}") - - -def write_part_crate( - crate_dir: Path, - *, - target_id: str, - index: int, - version: str, - package_base: str, - 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) - (crate_dir / "Cargo.toml").write_text( - f"""[package] -name = "{name}" -version = "{version}" -edition = "2024" -rust-version = "1.93" -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" -license = "MIT AND Apache-2.0 AND PostgreSQL" -links = "{links}" -build = "build.rs" -include = ["Cargo.toml", "README.md", "build.rs", "src/**", "payload/**"] - -[lib] -path = "src/lib.rs" - -[workspace] -""", - encoding="utf-8", - ) - (crate_dir / "README.md").write_text( - f"""# {name} - -Cargo payload part for the `{target_id}` {artifact_label}. -Applications do not depend on this crate directly. -""", - encoding="utf-8", - ) - (crate_dir / "src" / "lib.rs").write_text( - f"""pub const RELEASE_TARGET: &str = "{target_id}"; -pub const PART_INDEX: usize = {index}; -pub const PAYLOAD_ROOT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/payload"); -""", - encoding="utf-8", - ) - (crate_dir / "build.rs").write_text( - """use std::env; -use std::path::PathBuf; - -fn main() { - let manifest_dir = - PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set")); - let root = manifest_dir.join("payload"); - 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 Oliphaunt artifact payload under {}", root.display()); - } - return; - } - println!("cargo::metadata=root={}", root.display()); -} -""", - encoding="utf-8", - ) - - -def toml_string(value: str) -> str: - return json.dumps(value) - - -def write_aggregator_crate( - crate_dir: Path, - *, - target: artifact_targets.ArtifactTarget, - version: str, - part_count: int, - package_base: str, - artifact_product: str, - 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) - 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, package_base=package_base)} = {{ version = "={version}" }}' - for index in range(part_count) - ] - part_roots = [ - 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} {artifact_label}." -readme = "README.md" -repository = "https://github.com/f0rr0/oliphaunt" -homepage = "https://oliphaunt.dev" -license = "MIT AND Apache-2.0 AND PostgreSQL" -links = "{links}" -build = "build.rs" -include = ["Cargo.toml", "README.md", "build.rs", "src/**"] - -[lib] -path = "src/lib.rs" - -[build-dependencies] -sha2 = "0.10" -{chr(10).join(dependency_lines)} - -[workspace] -""", - encoding="utf-8", - ) - (crate_dir / "README.md").write_text( - f"""# {name} - -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 = "{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 = "{library_relative_path}"; -""", - encoding="utf-8", - ) - build_rs = ( - AGGREGATOR_BUILD_RS - .replace("__SCHEMA__", toml_string("oliphaunt-artifact-manifest-v1")) - .replace("__PRODUCT__", toml_string(artifact_product)) - .replace("__VERSION__", toml_string(version)) - .replace("__KIND__", toml_string(artifact_kind)) - .replace("__TARGET__", toml_string(target.triple)) - .replace("__PART_ROOTS__", "\n".join(part_roots)) - ) - (crate_dir / "build.rs").write_text(build_rs, encoding="utf-8") - - -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 = __VERSION__; -const KIND: &str = __KIND__; -const TARGET: &str = __TARGET__; -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 liboliphaunt native payload"); - } - fs::create_dir_all(&payload).expect("create liboliphaunt native 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 liboliphaunt native 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 payload files"); - collect_chunks(&root.join("chunks"), &root.join("chunks"), &mut chunk_files) - .expect("collect 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 liboliphaunt chunk indexes for {relative}"); - } - } - let output = payload.join(&relative); - if let Some(parent) = output.parent() { - fs::create_dir_all(parent).expect("create reconstructed file parent"); - } - let mut writer = fs::File::create(&output).expect("create reconstructed payload file"); - for (_, path) in chunks { - let mut reader = fs::File::open(&path).expect("open payload chunk"); - io::copy(&mut reader, &mut writer).expect("append payload chunk"); - } - } - - let files = collect_files(&payload).expect("collect reconstructed liboliphaunt payload files"); - if files.is_empty() { - panic!("liboliphaunt native 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:?}\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 liboliphaunt payload file"); - text.push_str(&format!( - "\n[[files]]\nsource = {:?}\nrelative = {:?}\nsha256 = {:?}\nexecutable = {}\n", - file.display().to_string(), - relative, - sha256, - is_executable_relative(&relative), - )); - } - fs::write(&manifest, text).expect("write liboliphaunt native 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 liboliphaunt 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) -} - -fn is_executable_relative(relative: &str) -> bool { - relative.starts_with("runtime/bin/") || relative.starts_with("bin/") -} -''' - - -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, - *, - 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 - - -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 build_part_crates( - extracted_root: Path, - source_root: Path, - *, - 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 - current_size = 0 - - def start_part() -> Path: - index = len(part_dirs) - 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 - - for source in payload_files(extracted_root): - relative = source.relative_to(extracted_root).as_posix() - size = source.stat().st_size - if size > part_bytes: - current_dir = None - current_size = 0 - with source.open("rb") as handle: - part_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{part_index:03d}", - data, - ) - part_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: - fail(f"{target_id} generated no {artifact_label} part crates") - return part_dirs - - -def cargo_package(crate_dir: Path, target_dir: Path, *, no_verify: bool = False) -> Path: - manifest = crate_dir / "Cargo.toml" - package = json.loads( - subprocess.check_output( - ["cargo", "metadata", "--no-deps", "--format-version", "1", "--manifest-path", str(manifest)], - cwd=ROOT, - text=True, - ) - )["packages"][0] - name = package["name"] - version = package["version"] - command = [ - "cargo", - "package", - "--manifest-path", - str(manifest), - "--target-dir", - 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" - if not crate_path.is_file(): - fail(f"cargo 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: - fail(f"{rel(crate_path)} is {size} bytes, above the crates.io 10 MiB package limit") - - -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 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, - 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]: - part_dirs = build_part_crates( - 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, 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] = [] - for index, part_dir in enumerate(part_dirs): - crate_path = cargo_package(part_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_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, - ) - ) - - packages.append( - GeneratedPackage( - 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, - *, - tools_target: artifact_targets.ArtifactTarget, - version: str, - asset_dir: Path, - source_root: Path, - output_dir: Path, - 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" - extract_archive(tools_archive, tools_root) - optimize_native_payload( - extracted_root, - target.target, - tool_set="runtime", - ) - optimize_native_payload( - tools_root, - target.target, - tool_set="tools", - ) - 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=tools_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", - "product": PRODUCT, - "packages": [ - { - "name": package.name, - "target": package.target, - "product": package.product, - "kind": package.kind, - "role": package.role, - "index": package.index, - "manifestPath": rel(package.manifest_path), - "cratePath": rel(package.crate_path) if package.crate_path is not None else None, - } - for package in packages - ], - } - (output_dir / "packages.json").write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") - - -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 checked liboliphaunt native release assets", - ) - parser.add_argument( - "--output-dir", - default="target/liboliphaunt/cargo-artifacts", - help="directory where generated .crate files are written", - ) - parser.add_argument("--version", default=product_metadata.read_current_version(PRODUCT)) - parser.add_argument( - "--target", - action="append", - default=[], - help="release target id to package; defaults to every Rust native-direct target", - ) - parser.add_argument( - "--part-bytes", - type=int, - default=DEFAULT_PART_BYTES, - help="maximum raw payload bytes per generated part crate", - ) - 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"liboliphaunt release asset directory does not exist: {rel(asset_dir)}") - if args.part_bytes <= 0 or args.part_bytes > DEFAULT_PART_BYTES: - fail(f"--part-bytes must be between 1 and {DEFAULT_PART_BYTES}") - - selected = set(args.target) - source_root = ROOT / "target" / "liboliphaunt" / "cargo-package-sources" - cargo_target_dir = ROOT / "target" / "liboliphaunt" / "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) - - targets = artifact_targets.artifact_targets( - product=PRODUCT, - kind=KIND, - 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) - if unknown: - fail("unknown liboliphaunt native Rust target(s): " + ", ".join(unknown)) - 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, - tools_target=tools_target, - version=args.version, - asset_dir=asset_dir, - source_root=source_root, - output_dir=output_dir, - cargo_target_dir=cargo_target_dir, - 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: - crate_path = rel(package.crate_path) if package.crate_path is not None else "" - print(f"{package.name} {package.role} {crate_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 b6674a45..1788ca89 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -19,7 +19,6 @@ import artifact_targets import extension_artifact_targets -import package_liboliphaunt_cargo_artifacts import package_liboliphaunt_wasix_cargo_artifacts import product_metadata @@ -37,6 +36,12 @@ 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) +LIBOLIPHAUNT_NATIVE_CARGO_PRODUCT = "liboliphaunt-native" +LIBOLIPHAUNT_TOOLS_PRODUCT = "oliphaunt-tools" + + +def liboliphaunt_cargo_package_name(target_id: str, package_base: str = LIBOLIPHAUNT_NATIVE_CARGO_PRODUCT) -> str: + return f"{package_base}-{target_id}" def fail(message: str) -> NoReturn: @@ -743,8 +748,8 @@ def render_oliphaunt_release_cargo_toml(source: str, native_version: str, broker surface="rust-native-direct", published_only=True, ): - crate = package_liboliphaunt_cargo_artifacts.cargo_package_name(target.target) - tools_facade = package_liboliphaunt_cargo_artifacts.TOOLS_PRODUCT + crate = liboliphaunt_cargo_package_name(target.target) + tools_facade = LIBOLIPHAUNT_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_facade} = {{ version = "={native_version}" }}') @@ -786,7 +791,7 @@ def validate_generated_oliphaunt_release_artifact_coverage(manifest_path: Path) published_only=True, ) native_runtime_crates = { - package_liboliphaunt_cargo_artifacts.cargo_package_name(target.target) + liboliphaunt_cargo_package_name(target.target) for target in native_targets } native_crates = set(cargo_registry_packages("liboliphaunt-native")) @@ -799,7 +804,7 @@ 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." ) - tools_facade = package_liboliphaunt_cargo_artifacts.TOOLS_PRODUCT + tools_facade = LIBOLIPHAUNT_TOOLS_PRODUCT missing_native = sorted( crate for crate in native_runtime_crates if f'{crate} = {{ version = "={native_version}" }}' not in manifest ) @@ -911,10 +916,10 @@ def prepare_oliphaunt_release_source(version: str) -> Path: surface="rust-native-direct", published_only=True, ): - crate = package_liboliphaunt_cargo_artifacts.cargo_package_name(target.target) + crate = liboliphaunt_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_facade = package_liboliphaunt_cargo_artifacts.TOOLS_PRODUCT + tools_facade = LIBOLIPHAUNT_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( @@ -2817,8 +2822,8 @@ def liboliphaunt_cargo_artifact_crates(version: str) -> list[tuple[str, Path | N output_dir = ROOT / "target" / "liboliphaunt" / "cargo-artifacts" run( [ - "python3", - "tools/release/package_liboliphaunt_cargo_artifacts.py", + "tools/dev/bun.sh", + "tools/release/package-liboliphaunt-cargo-artifacts.mjs", "--version", version, "--output-dir", @@ -2841,16 +2846,16 @@ def liboliphaunt_cargo_artifact_crates(version: str) -> list[tuple[str, Path | N published_only=True, ) expected_aggregators = { - package_liboliphaunt_cargo_artifacts.cargo_package_name(target.target) + liboliphaunt_cargo_package_name(target.target) for target in native_targets } | { - package_liboliphaunt_cargo_artifacts.cargo_package_name( + liboliphaunt_cargo_package_name( target.target, - package_base=package_liboliphaunt_cargo_artifacts.TOOLS_PRODUCT, + package_base=LIBOLIPHAUNT_TOOLS_PRODUCT, ) for target in native_targets } - expected_facades = {package_liboliphaunt_cargo_artifacts.TOOLS_PRODUCT} + expected_facades = {LIBOLIPHAUNT_TOOLS_PRODUCT} expected_registry_crates = expected_aggregators | expected_facades configured_crates = set(cratesio_product_crates("liboliphaunt-native")) if configured_crates != expected_registry_crates: From cf41a7b3d925052e1d8d843be775bb62e5b1aa18 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 03:40:35 +0000 Subject: [PATCH 145/308] chore: route extension targets through bun graph --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 22 ++ tools/policy/python-entrypoints.allowlist | 1 - tools/release/check_artifact_targets.py | 7 +- tools/release/check_consumer_shape.py | 5 +- tools/release/check_release_metadata.py | 7 +- tools/release/extension_artifact_targets.py | 252 ------------------ tools/release/local_registry_publish.py | 6 +- tools/release/product_metadata.py | 76 ++++++ tools/release/release-artifact-targets.mjs | 6 + tools/release/release.py | 5 +- tools/release/release_graph_query.mjs | 40 +++ 11 files changed, 157 insertions(+), 270 deletions(-) delete mode 100644 tools/release/extension_artifact_targets.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 190ce41a..eadb2b92 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,28 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Removed the duplicate Python exact-extension artifact target + helper. Python release checks now query `tools/release/release_graph_query.mjs + extension-targets`, which delegates to the canonical Bun + `release-artifact-targets.mjs` metadata used by CI matrices and staged + artifact validation. The Bun target rows now preserve the stricter unpublished + `unsupported_reason` invariant and expose `source_file` for parity with the + retired helper. Fresh checks passed: `tools/dev/bun.sh + tools/release/release_graph_query.mjs extension-targets --family native + --published-only`, `tools/dev/bun.sh tools/release/release_graph_query.mjs + extension-targets --family wasix --published-only`, `python3 -m py_compile` + for touched Python release callers, `python3 + tools/release/check_artifact_targets.py`, `python3 + tools/release/check_release_metadata.py`, focused `python3 + tools/release/check_consumer_shape.py --products-json + '["liboliphaunt-native","liboliphaunt-wasix","oliphaunt-extension-postgis","oliphaunt-rust"]'`, + and a `local_registry_publish.local_publish_aggregate_artifacts()` smoke. + Follow-up validation passed: `tools/dev/bun.sh + tools/policy/check-python-entrypoints.mjs --list`, `python3 + tools/policy/check-release-policy.py`, `bash + tools/policy/check-tooling-stack.sh`, `bash tools/policy/check-repo-structure.sh`, + `tools/release/release.py check`, and `git diff --check --cached && git diff + --check`. - 2026-06-27: Ported native liboliphaunt Cargo artifact crate packaging from Python to Bun as `tools/release/package-liboliphaunt-cargo-artifacts.mjs`. Release publishing, local-registry Cargo package synthesis, the Rust SDK diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index f58be08d..702ba2f9 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -6,7 +6,6 @@ tools/release/artifact_targets.py tools/release/check_artifact_targets.py tools/release/check_consumer_shape.py tools/release/check_release_metadata.py -tools/release/extension_artifact_targets.py tools/release/local_registry_publish.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 7e70b5de..c05b80c2 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -11,7 +11,6 @@ from typing import NoReturn import artifact_targets -import extension_artifact_targets import product_metadata @@ -227,7 +226,7 @@ def validate_extension_artifact_targets() -> None: fail("published WASIX runtime targets are required before extension artifacts can be published") for product in extension_products: - rows = extension_artifact_targets.artifact_targets(product=product) + rows = product_metadata.extension_artifact_targets(product=product) published_native_targets = { target.target for target in rows if target.family == "native" and target.published } @@ -1202,7 +1201,7 @@ def validate_target_matrices() -> None: } expected_extension_native_pairs = { (target.product, target.target) - for target in extension_artifact_targets.artifact_targets(family="native", published_only=True) + for target in product_metadata.extension_artifact_targets(family="native", published_only=True) } if extension_native_pairs != expected_extension_native_pairs: fail( @@ -1251,7 +1250,7 @@ def validate_target_matrices() -> None: } expected_extension_wasix_pairs = { (target.product, target.target) - for target in extension_artifact_targets.artifact_targets(family="wasix", published_only=True) + for target in product_metadata.extension_artifact_targets(family="wasix", published_only=True) } if extension_wasix_pairs != expected_extension_wasix_pairs: fail( diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 62a06949..3270f837 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -19,7 +19,6 @@ import artifact_targets import product_metadata -import extension_artifact_targets import package_liboliphaunt_wasix_cargo_artifacts @@ -2023,7 +2022,7 @@ def check_exact_extension(findings: list[Finding], product: str) -> None: sql_name = config.get("extension_sql_name") expected_registry_packages = { f"maven:dev.oliphaunt.extensions:{product}-{target.target}" - for target in extension_artifact_targets.published_android_maven_targets(product) + for target in product_metadata.published_android_maven_targets(product) } version_path = f"{package_path}/VERSION" version = read_text(version_path).strip() @@ -2050,7 +2049,7 @@ def check_exact_extension(findings: list[Finding], product: str) -> None: f"{package_path}/release.toml registry_packages={sorted(product_registry_packages(product))!r}", severity="P0", ) - targets = extension_artifact_targets.artifact_targets(product=product, published_only=True) + targets = product_metadata.extension_artifact_targets(product=product, published_only=True) native_targets = {target.target for target in targets if target.family == "native"} wasix_targets = {target.target for target in targets if target.family == "wasix"} require( diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 335312c6..f9d315b0 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -11,7 +11,6 @@ from typing import NoReturn import artifact_targets -import extension_artifact_targets import package_liboliphaunt_wasix_cargo_artifacts import product_metadata import release @@ -265,7 +264,7 @@ def validate_exact_extension_registry_shape(graph: dict) -> None: ) expected_registry_packages = { f"maven:dev.oliphaunt.extensions:{product}-{target.target}" - for target in extension_artifact_targets.published_android_maven_targets(product) + for target in product_metadata.published_android_maven_targets(product) } if set(registry_packages) != expected_registry_packages: fail( @@ -274,11 +273,11 @@ def validate_exact_extension_registry_shape(graph: dict) -> None: ) android_targets = { target.target - for target in extension_artifact_targets.published_android_maven_targets(product) + for target in product_metadata.published_android_maven_targets(product) } 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): + for target in product_metadata.extension_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-"): diff --git a/tools/release/extension_artifact_targets.py b/tools/release/extension_artifact_targets.py deleted file mode 100644 index 23ee8ffe..00000000 --- a/tools/release/extension_artifact_targets.py +++ /dev/null @@ -1,252 +0,0 @@ -#!/usr/bin/env python3 -"""Exact-extension release artifact target metadata.""" - -from __future__ import annotations - -import tomllib -from dataclasses import dataclass -from pathlib import Path - -import artifact_targets as runtime_artifact_targets -import product_metadata - - -ROOT = Path(__file__).resolve().parents[2] -SCHEMA = "oliphaunt-extension-artifact-targets-v1" -FAMILIES = {"native", "wasix"} -KINDS = { - "native-dynamic", - "native-static-registry", - "wasix-runtime", -} -STATUSES = {"supported", "planned", "unsupported"} - - -@dataclass(frozen=True) -class ExtensionArtifactTarget: - product: str - sql_name: str - target: str - family: str - kind: str - published: bool - status: str - source_file: Path - unsupported_reason: str | None = None - - -def _read_toml(path: Path) -> dict: - try: - data = tomllib.loads(path.read_text(encoding="utf-8")) - except tomllib.TOMLDecodeError as error: - product_metadata.fail(f"{path.relative_to(ROOT)} is invalid TOML: {error}") - if not isinstance(data, dict): - product_metadata.fail(f"{path.relative_to(ROOT)} must contain a TOML table") - return data - - -def _exact_extension_products() -> list[str]: - products: list[str] = [] - for product in product_metadata.product_ids(): - if product_metadata.product_config(product).get("kind") == "exact-extension-artifact": - products.append(product) - return sorted(products) - - -def _extension_sql_name(product: str) -> str: - value = product_metadata.product_config(product).get("extension_sql_name") - if not isinstance(value, str) or not value: - product_metadata.fail(f"{product} release.toml must declare extension_sql_name") - return value - - -def _bool(value: object, label: str) -> bool: - if isinstance(value, bool): - return value - product_metadata.fail(f"{label} must be true or false") - - -def _string(value: object, label: str) -> str: - if isinstance(value, str) and value: - return value - product_metadata.fail(f"{label} must be a non-empty string") - - -def artifact_target_file(product: str) -> Path: - return ROOT / product_metadata.package_path(product) / "targets" / "artifacts.toml" - - -def _default_source_file(product: str) -> Path: - return ROOT / product_metadata.package_path(product) / "release.toml" - - -def _default_native_kind(target: str) -> str: - if target == "ios-xcframework" or target.startswith("android-"): - return "native-static-registry" - return "native-dynamic" - - -def _wasix_extension_target_id(runtime_target: str) -> str: - if runtime_target == "portable": - return "wasix-portable" - return runtime_target - - -def _default_target_rows(product: str) -> list[dict]: - source_file = str(_default_source_file(product).relative_to(ROOT)) - rows: list[dict] = [] - for target in runtime_artifact_targets.artifact_targets( - product="liboliphaunt-native", - kind="native-runtime", - published_only=True, - ): - if not target.extension_artifacts: - continue - rows.append( - { - "target": target.target, - "family": "native", - "kind": _default_native_kind(target.target), - "status": "supported", - "published": True, - "_source_file": source_file, - } - ) - for target in runtime_artifact_targets.artifact_targets( - product="liboliphaunt-wasix", - kind="wasix-runtime", - published_only=True, - ): - rows.append( - { - "target": _wasix_extension_target_id(target.target), - "family": "wasix", - "kind": "wasix-runtime", - "status": "supported", - "published": True, - "_source_file": source_file, - } - ) - if not rows: - product_metadata.fail(f"{product} could not derive any exact-extension artifact targets") - return rows - - -def artifact_targets( - *, - product: str | None = None, - family: str | None = None, - published_only: bool = False, -) -> list[ExtensionArtifactTarget]: - products = [product] if product is not None else _exact_extension_products() - parsed: list[ExtensionArtifactTarget] = [] - for product_id in products: - if product_id not in product_metadata.product_ids(): - product_metadata.fail(f"unknown exact-extension product {product_id}") - if product_metadata.product_config(product_id).get("kind") != "exact-extension-artifact": - product_metadata.fail(f"{product_id} is not an exact-extension artifact product") - path = artifact_target_file(product_id) - if path.is_file(): - source_file = path - data = _read_toml(path) - if data.get("schema") != SCHEMA: - product_metadata.fail(f"{path.relative_to(ROOT)} must use schema = {SCHEMA!r}") - rows = data.get("targets") - if not isinstance(rows, list) or not rows: - product_metadata.fail(f"{path.relative_to(ROOT)} must define [[targets]] rows") - else: - source_file = _default_source_file(product_id) - rows = _default_target_rows(product_id) - source_label = source_file.relative_to(ROOT) - allowed_override_keys = { - (str(row["target"]), str(row["family"]), str(row["kind"])) - for row in _default_target_rows(product_id) - } - sql_name = _extension_sql_name(product_id) - seen: set[tuple[str, str, str]] = set() - for index, row in enumerate(rows): - if not isinstance(row, dict): - product_metadata.fail(f"{source_label} targets[{index}] must be a table") - target = _string(row.get("target"), f"{source_label} targets[{index}].target") - target_family = _string(row.get("family"), f"{source_label} targets[{index}].family") - kind = _string(row.get("kind"), f"{source_label} targets[{index}].kind") - status = _string(row.get("status"), f"{source_label} targets[{index}].status") - published = _bool(row.get("published"), f"{source_label} targets[{index}].published") - if target_family not in FAMILIES: - product_metadata.fail(f"{source_label} target {target} has invalid family {target_family!r}") - if kind not in KINDS: - product_metadata.fail(f"{source_label} target {target} has invalid kind {kind!r}") - if status not in STATUSES: - product_metadata.fail(f"{source_label} target {target} has invalid status {status!r}") - if target_family == "wasix" and kind != "wasix-runtime": - product_metadata.fail(f"{source_label} target {target} must use kind wasix-runtime for wasix family") - if target_family == "native" and kind == "wasix-runtime": - product_metadata.fail(f"{source_label} target {target} cannot use wasix-runtime for native family") - reason = row.get("unsupported_reason") - if published and status != "supported": - product_metadata.fail(f"{source_label} target {target} cannot be published with status {status}") - if not published and (not isinstance(reason, str) or not reason): - product_metadata.fail(f"{source_label} unpublished target {target} must explain unsupported_reason") - key = (target, target_family, kind) - if key in seen: - product_metadata.fail(f"{source_label} has duplicate target row {key}") - if path.is_file() and key not in allowed_override_keys: - product_metadata.fail( - f"{source_label} target row {key} is not backed by runtime artifact metadata" - ) - seen.add(key) - if family is not None and target_family != family: - continue - if published_only and not published: - continue - parsed.append( - ExtensionArtifactTarget( - product=product_id, - sql_name=sql_name, - target=target, - family=target_family, - kind=kind, - published=published, - status=status, - source_file=source_file, - unsupported_reason=reason if isinstance(reason, str) else None, - ) - ) - return parsed - - -def published_target_ids(*, family: str) -> list[str]: - return sorted({target.target for target in artifact_targets(family=family, published_only=True)}) - - -def published_android_maven_targets(product: str) -> list[ExtensionArtifactTarget]: - return sorted( - ( - target - for target in artifact_targets(product=product, family="native", published_only=True) - if target.kind == "native-static-registry" and target.target.startswith("android-") - ), - 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 76b0cb0a..5384befb 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -35,7 +35,7 @@ from typing import Any, Iterable import artifact_targets -import extension_artifact_targets +import product_metadata ROOT = Path(__file__).resolve().parents[2] @@ -61,8 +61,8 @@ def local_publish_aggregate_artifacts() -> list[str]: 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(), + *product_metadata.ci_wasix_extension_artifact_names(), + *product_metadata.ci_extension_package_artifact_names(), ] diff --git a/tools/release/product_metadata.py b/tools/release/product_metadata.py index 8b534a4c..f835b579 100644 --- a/tools/release/product_metadata.py +++ b/tools/release/product_metadata.py @@ -16,6 +16,7 @@ import tomllib from functools import lru_cache from pathlib import Path +from types import SimpleNamespace from typing import Any, NoReturn @@ -306,6 +307,81 @@ def extension_product_ids(graph: dict | None = None) -> list[str]: ) +@lru_cache(maxsize=None) +def extension_artifact_targets( + *, + product: str | None = None, + family: str | None = None, + published_only: bool = False, +) -> tuple[SimpleNamespace, ...]: + args = ["tools/dev/bun.sh", "tools/release/release_graph_query.mjs", "extension-targets"] + if product is not None: + args.extend(["--product", product]) + if family is not None: + args.extend(["--family", family]) + if published_only: + args.append("--published-only") + try: + output = subprocess.check_output(args, cwd=ROOT, text=True, stderr=subprocess.PIPE) + except subprocess.CalledProcessError as error: + detail = (error.stderr or "").strip() + if detail: + fail(f"release graph extension target query failed: {detail}") + fail(f"release graph extension target query failed with exit code {error.returncode}") + rows = json.loads(output) + if not isinstance(rows, list) or not all(isinstance(row, dict) for row in rows): + fail("release graph extension-targets query must return a JSON object list") + return tuple(SimpleNamespace(**row) for row in rows) + + +def published_android_maven_targets(product: str) -> tuple[SimpleNamespace, ...]: + return tuple( + sorted( + ( + target + for target in extension_artifact_targets( + product=product, + family="native", + published_only=True, + ) + if target.kind == "native-static-registry" and target.target.startswith("android-") + ), + key=lambda target: target.target, + ) + ) + + +def published_extension_target_ids(*, family: str) -> list[str]: + return sorted( + { + target.target + for target in extension_artifact_targets(family=family, published_only=True) + } + ) + + +def ci_wasix_extension_artifact_names() -> list[str]: + names = [ + f"liboliphaunt-wasix-extension-artifacts-{target_id}" + for target_id in published_extension_target_ids(family="wasix") + ] + if not names: + 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 extension_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 + + def string_list(config: dict, key: str, product: str) -> list[str]: value = config.get(key, []) if not isinstance(value, list) or not all(isinstance(item, str) for item in value): diff --git a/tools/release/release-artifact-targets.mjs b/tools/release/release-artifact-targets.mjs index d481b472..09ec15e0 100644 --- a/tools/release/release-artifact-targets.mjs +++ b/tools/release/release-artifact-targets.mjs @@ -818,6 +818,10 @@ export function extensionArtifactTargets( if (published && status !== "supported") { fail(prefix, `${source} target ${target} cannot be published with status ${status}`); } + const unsupportedReason = row.unsupported_reason; + if (!published && (typeof unsupportedReason !== "string" || unsupportedReason.length === 0)) { + fail(prefix, `${source} unpublished target ${target} must explain unsupported_reason`); + } const key = `${target}\0${targetFamily}\0${kind}`; if (seen.has(key)) { fail(prefix, `${source} has duplicate target row ${target}/${targetFamily}/${kind}`); @@ -838,6 +842,8 @@ export function extensionArtifactTargets( kind, published, status, + source_file: source, + unsupported_reason: typeof unsupportedReason === "string" ? unsupportedReason : null, }); } } diff --git a/tools/release/release.py b/tools/release/release.py index 1788ca89..785641d1 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -18,7 +18,6 @@ from typing import NoReturn import artifact_targets -import extension_artifact_targets import package_liboliphaunt_wasix_cargo_artifacts import product_metadata @@ -1532,7 +1531,7 @@ def validate_extension_release_package(product: str) -> None: declared_native_targets = { target.target - for target in extension_artifact_targets.artifact_targets( + for target in product_metadata.extension_artifact_targets( product=product, family="native", published_only=True, @@ -1540,7 +1539,7 @@ def validate_extension_release_package(product: str) -> None: } declared_wasix_targets = { target.target - for target in extension_artifact_targets.artifact_targets( + for target in product_metadata.extension_artifact_targets( product=product, family="wasix", published_only=True, diff --git a/tools/release/release_graph_query.mjs b/tools/release/release_graph_query.mjs index f1f5194b..719774ff 100644 --- a/tools/release/release_graph_query.mjs +++ b/tools/release/release_graph_query.mjs @@ -1,4 +1,7 @@ #!/usr/bin/env bun +import { + extensionArtifactTargets, +} from "./release-artifact-targets.mjs"; import { buildPlan, compareText, @@ -146,6 +149,40 @@ function runPlansForPaths(argv) { ); } +function runExtensionTargets(argv) { + let product; + let family; + let publishedOnly = false; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === "--product") { + if (index + 1 >= argv.length) { + fail("--product requires a value"); + } + product = argv[index + 1]; + index += 1; + } else if (value.startsWith("--product=")) { + product = value.slice("--product=".length); + } else if (value === "--family") { + if (index + 1 >= argv.length) { + fail("--family requires a value"); + } + family = argv[index + 1]; + index += 1; + } else if (value.startsWith("--family=")) { + family = value.slice("--family=".length); + } else if (value === "--published-only") { + publishedOnly = true; + } else { + fail(`unknown argument ${value}`); + } + } + if (family !== undefined && !["native", "wasix"].includes(family)) { + fail("--family must be native or wasix"); + } + printJson(extensionArtifactTargets({ product, family, publishedOnly }, TOOL)); +} + function usage() { return `usage: tools/release/release_graph_query.mjs [options] @@ -155,6 +192,7 @@ Commands: release-order --products-json JSON plan [--changed-file PATH...] plans-for-paths --paths-json JSON + extension-targets [--product PRODUCT] [--family native|wasix] [--published-only] `; } @@ -170,6 +208,8 @@ function main(argv) { runPlan(rest); } else if (command === "plans-for-paths") { runPlansForPaths(rest); + } else if (command === "extension-targets") { + runExtensionTargets(rest); } else if (command === "--help" || command === "-h") { console.log(usage()); } else { From 56b9037cd18117f523cc0ab42d28a952025e1fe3 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 04:00:17 +0000 Subject: [PATCH 146/308] chore: route artifact targets through bun graph --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 19 + src/runtimes/liboliphaunt/native/moon.yml | 2 +- src/runtimes/node-direct/moon.yml | 6 +- tools/policy/check-release-policy.py | 5 +- tools/policy/python-entrypoints.allowlist | 1 - tools/release/artifact_targets.py | 763 ------------------ tools/release/check_artifact_targets.py | 33 +- tools/release/check_consumer_shape.py | 19 +- tools/release/check_release_metadata.py | 5 +- tools/release/local_registry_publish.py | 19 +- tools/release/product_metadata.py | 291 ++++++- tools/release/release-artifact-targets.mjs | 40 +- tools/release/release.py | 41 +- tools/release/release_graph_query.mjs | 70 ++ 14 files changed, 474 insertions(+), 840 deletions(-) delete mode 100644 tools/release/artifact_targets.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index eadb2b92..5fd45597 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,25 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Removed the duplicate Python runtime/helper artifact target + model in `tools/release/artifact_targets.py`. Python release callers now use + `product_metadata.artifact_targets()` compatibility wrappers backed by the + canonical Bun `release-artifact-targets.mjs` graph through + `release_graph_query.mjs artifact-targets` and `raw-artifact-targets`. + Moon inputs for native and Node-direct release tasks now track + `product_metadata.py` plus the Bun query entrypoint, and the intentional + Python inventory is down to 9 tracked files after staging. Fresh checks + passed: `tools/dev/bun.sh tools/release/release_graph_query.mjs + artifact-targets --product liboliphaunt-native --kind native-runtime + --published-only`, `tools/dev/bun.sh tools/release/release_graph_query.mjs + raw-artifact-targets --product liboliphaunt-native`, `python3 -m + py_compile` for touched Python release/policy callers, `python3 + tools/release/check_artifact_targets.py`, `python3 + tools/release/check_release_metadata.py`, focused `python3 + tools/release/check_consumer_shape.py --products-json + '["liboliphaunt-native","liboliphaunt-wasix","oliphaunt-broker","oliphaunt-node-direct","oliphaunt-js","oliphaunt-rust"]'`, + `python3 tools/policy/check-release-policy.py`, and + `tools/release/release.py check`. - 2026-06-27: Removed the duplicate Python exact-extension artifact target helper. Python release checks now query `tools/release/release_graph_query.mjs extension-targets`, which delegates to the canonical Bun diff --git a/src/runtimes/liboliphaunt/native/moon.yml b/src/runtimes/liboliphaunt/native/moon.yml index f05b57e1..a0578585 100644 --- a/src/runtimes/liboliphaunt/native/moon.yml +++ b/src/runtimes/liboliphaunt/native/moon.yml @@ -169,12 +169,12 @@ tasks: - "/release-please-config.json" - "/src/extensions/generated/sdk/rust.json" - "/src/runtimes/liboliphaunt/native/moon.yml" - - "/tools/release/artifact_targets.py" - "/tools/release/check-liboliphaunt-release-assets.mjs" - "/tools/release/package-liboliphaunt-aggregate-assets.sh" - "/tools/release/product_metadata.py" - "/tools/release/release-artifact-targets.mjs" - "/tools/release/release-graph.mjs" + - "/tools/release/release_graph_query.mjs" - "/target/liboliphaunt/release-assets/**/*" outputs: - "/target/liboliphaunt/release-assets/**/*" diff --git a/src/runtimes/node-direct/moon.yml b/src/runtimes/node-direct/moon.yml index 1b27a61b..aaedf308 100644 --- a/src/runtimes/node-direct/moon.yml +++ b/src/runtimes/node-direct/moon.yml @@ -38,11 +38,12 @@ tasks: - "liboliphaunt-native:check" inputs: - "/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/product_metadata.py" - "/tools/release/release-asset-validation.mjs" - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release_graph_query.mjs" - "/tools/policy/moon.mjs" - "/release-please-config.json" - "/.release-please-manifest.json" @@ -80,10 +81,11 @@ tasks: inputs: - "/src/runtimes/node-direct/**/*" - "/tools/release/artifact_target_matrix.mjs" - - "/tools/release/artifact_targets.py" - "/tools/release/check-node-direct-release-assets.mjs" + - "/tools/release/product_metadata.py" - "/tools/release/release-asset-validation.mjs" - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release_graph_query.mjs" - "/tools/policy/moon.mjs" - "/release-please-config.json" - "/.release-please-manifest.json" diff --git a/tools/policy/check-release-policy.py b/tools/policy/check-release-policy.py index 690bf770..a569aa79 100644 --- a/tools/policy/check-release-policy.py +++ b/tools/policy/check-release-policy.py @@ -13,7 +13,6 @@ ROOT = pathlib.Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "tools/release")) -import artifact_targets # noqa: E402 import product_metadata # noqa: E402 @@ -1026,7 +1025,7 @@ def check_release_workflow_policy() -> None: ) for snippet in ( "validate_wasix_release_assets", - "artifact_targets.expected_assets(product, version, surface=\"github-release\")", + "product_metadata.expected_assets(product, version, surface=\"github-release\")", "parse_local_checksum_manifest", "target/oliphaunt-wasix/release-assets", "validate_wasix_release_asset_contents", @@ -1489,7 +1488,7 @@ def check_ci_builder_planning() -> None: full_targets = extension_native_targets(extension_jobs, extension_tasks) expected_full_targets = { target.target - for target in artifact_targets.artifact_targets( + for target in product_metadata.artifact_targets( product="liboliphaunt-native", kind="native-runtime", published_only=True, diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index 702ba2f9..d62f6bae 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/policy/check-release-policy.py -tools/release/artifact_targets.py tools/release/check_artifact_targets.py tools/release/check_consumer_shape.py tools/release/check_release_metadata.py diff --git a/tools/release/artifact_targets.py b/tools/release/artifact_targets.py deleted file mode 100644 index d68f641b..00000000 --- a/tools/release/artifact_targets.py +++ /dev/null @@ -1,763 +0,0 @@ -#!/usr/bin/env python3 -"""Release artifact target metadata derived from Moon release metadata. - -Moon owns release-product identity and target membership. This module expands -compact product presets into concrete release asset rows so package managers, -CI matrices, and validators all read the same artifact graph. -""" - -from __future__ import annotations - -from dataclasses import dataclass -from pathlib import Path -from typing import Iterable - -import product_metadata - -ROOT = Path(__file__).resolve().parents[2] - -DESKTOP_TARGETS: dict[str, dict[str, str]] = { - "linux-arm64-gnu": { - "triple": "aarch64-unknown-linux-gnu", - "runner": "ubuntu-24.04-arm", - "archive": "tar.gz", - "npm_os": "linux", - "npm_cpu": "arm64", - "npm_libc": "glibc", - "liboliphaunt_npm_package": "@oliphaunt/liboliphaunt-linux-arm64-gnu", - "liboliphaunt_tools_npm_package": "@oliphaunt/tools-linux-arm64-gnu", - "broker_npm_package": "@oliphaunt/broker-linux-arm64-gnu", - "node_package": "@oliphaunt/node-direct-linux-arm64-gnu", - "wasix_llvm_url": "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", - "npm_os": "linux", - "npm_cpu": "x64", - "npm_libc": "glibc", - "liboliphaunt_npm_package": "@oliphaunt/liboliphaunt-linux-x64-gnu", - "liboliphaunt_tools_npm_package": "@oliphaunt/tools-linux-x64-gnu", - "broker_npm_package": "@oliphaunt/broker-linux-x64-gnu", - "node_package": "@oliphaunt/node-direct-linux-x64-gnu", - "wasix_llvm_url": "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", - "npm_os": "darwin", - "npm_cpu": "arm64", - "liboliphaunt_npm_package": "@oliphaunt/liboliphaunt-darwin-arm64", - "liboliphaunt_tools_npm_package": "@oliphaunt/tools-darwin-arm64", - "broker_npm_package": "@oliphaunt/broker-darwin-arm64", - "node_package": "@oliphaunt/node-direct-darwin-arm64", - "wasix_llvm_url": "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", - }, - "windows-x64-msvc": { - "triple": "x86_64-pc-windows-msvc", - "runner": "windows-latest", - "archive": "zip", - "npm_os": "win32", - "npm_cpu": "x64", - "liboliphaunt_npm_package": "@oliphaunt/liboliphaunt-win32-x64-msvc", - "liboliphaunt_tools_npm_package": "@oliphaunt/tools-win32-x64-msvc", - "broker_npm_package": "@oliphaunt/broker-win32-x64-msvc", - "node_package": "@oliphaunt/node-direct-win32-x64-msvc", - "wasix_llvm_url": "https://github.com/wasmerio/llvm-custom-builds/releases/download/22.x/llvm-windows-amd64.tar.xz", - }, -} - -MOBILE_TARGETS: dict[str, dict[str, str]] = { - "android-arm64-v8a": { - "triple": "aarch64-linux-android", - "runner": "ubuntu-latest", - "android_abi": "arm64-v8a", - }, - "android-x86_64": { - "triple": "x86_64-linux-android", - "runner": "ubuntu-latest", - "android_abi": "x86_64", - }, - "ios-xcframework": { - "triple": "ios-xcframework", - "runner": "macos-26", - }, -} - -NATIVE_RUNTIME_TARGETS = {**DESKTOP_TARGETS, **MOBILE_TARGETS} -WASIX_TARGETS = {"portable", "linux-arm64-gnu", "linux-x64-gnu", "macos-arm64", "windows-x64-msvc"} -BROKER_TARGETS = {"linux-arm64-gnu", "linux-x64-gnu", "macos-arm64", "windows-x64-msvc"} -NODE_DIRECT_TARGETS = BROKER_TARGETS - - -def liboliphaunt_native_build_root(target_id: str) -> str: - if target_id not in NATIVE_RUNTIME_TARGETS: - product_metadata.fail(f"unknown liboliphaunt-native target {target_id}") - build_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 build_roots.get(target_id, f"target/liboliphaunt-pg18-{target_id}") - - -def liboliphaunt_native_ci_artifact_root(target_id: str) -> str: - if target_id not in NATIVE_RUNTIME_TARGETS: - product_metadata.fail(f"unknown liboliphaunt-native target {target_id}") - return f"target/liboliphaunt-native-ci/{target_id}" - - -def liboliphaunt_android_abi(target_id: str) -> str: - metadata = MOBILE_TARGETS.get(target_id) - abi = metadata.get("android_abi") if metadata is not None else None - if not abi: - product_metadata.fail(f"unsupported React Native Android runtime target {target_id}") - return abi - - -@dataclass(frozen=True) -class ArtifactTarget: - id: str - product: str - kind: str - target: str - asset: str - published: bool - surfaces: tuple[str, ...] - triple: str | None = None - runner: str | None = None - library_relative_path: str | None = None - executable_relative_path: str | None = None - npm_package: str | None = None - npm_os: str | None = None - npm_cpu: str | None = None - npm_libc: str | None = None - llvm_url: str | None = None - extension_artifacts: bool = True - - def asset_name(self, version: str) -> str: - return self.asset.format(version=version) - - -def _string(value: object, key: str, target_id: str, required: bool = True) -> str | None: - if isinstance(value, str) and value: - return value - if required: - product_metadata.fail(f"artifact target {target_id}.{key} must be a non-empty string") - if value is not None: - product_metadata.fail(f"artifact target {target_id}.{key} must be a string") - return None - - -def _surfaces(value: object, target_id: str) -> tuple[str, ...]: - if not isinstance(value, list) or not value or not all(isinstance(item, str) and item for item in value): - product_metadata.fail(f"artifact target {target_id}.surfaces must be a non-empty string list") - return tuple(value) - - -def _published(value: object, target_id: str) -> bool: - if isinstance(value, bool): - return value - product_metadata.fail(f"artifact target {target_id}.published must be true or false") - - -def _optional_bool(value: object, key: str, target_id: str, default: bool) -> bool: - if value is None: - return default - if isinstance(value, bool): - return value - product_metadata.fail(f"artifact target {target_id}.{key} must be true or false") - - -def _release_target_config(product: str, expected_preset: str) -> dict: - release = product_metadata.moon_release_metadata(product) - config = release.get("artifactTargets") - if not isinstance(config, dict): - product_metadata.fail(f"Moon release metadata for {product} must declare artifactTargets") - preset = config.get("preset") - if preset != expected_preset: - product_metadata.fail( - f"Moon release metadata for {product} artifactTargets.preset must be " - f"{expected_preset!r}, got {preset!r}" - ) - return config - - -def _target_list(config: dict, product: str, key: str) -> tuple[str, ...]: - value = config.get(key, []) - if not isinstance(value, list) or not all(isinstance(item, str) and item for item in value): - product_metadata.fail(f"Moon release metadata for {product} artifactTargets.{key} must be a string list") - if len(set(value)) != len(value): - product_metadata.fail(f"Moon release metadata for {product} artifactTargets.{key} contains duplicate targets") - return tuple(value) - - -def _planned_targets(config: dict, product: str) -> dict[str, dict]: - value = config.get("plannedTargets", {}) - if not isinstance(value, dict): - product_metadata.fail(f"Moon release metadata for {product} artifactTargets.plannedTargets must be a table") - planned: dict[str, dict] = {} - for target, details in value.items(): - if not isinstance(target, str) or not target: - product_metadata.fail(f"Moon release metadata for {product} planned target keys must be non-empty strings") - if not isinstance(details, dict): - product_metadata.fail(f"Moon release metadata for {product} planned target {target} must be a table") - reason = details.get("unsupportedReason") - if not isinstance(reason, str) or len(reason.strip()) < 40: - product_metadata.fail( - f"Moon release metadata for {product} planned target {target} must declare a concrete unsupportedReason" - ) - planned[target] = details - return planned - - -def _check_known_targets(product: str, targets: Iterable[str], known: set[str]) -> None: - unknown = sorted(set(targets) - known) - if unknown: - product_metadata.fail(f"Moon release metadata for {product} declares unknown artifact target(s): {unknown}") - - -def _archive_asset(product_prefix: str, target: str, archive: str) -> str: - if archive == "zip": - return f"{product_prefix}-{{version}}-{target}.zip" - return f"{product_prefix}-{{version}}-{target}.tar.gz" - - -def _native_library_relative_path(target: str) -> str: - if target.startswith("android-"): - abi = MOBILE_TARGETS[target]["android_abi"] - return f"jni/{abi}/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" - product_metadata.fail(f"unsupported liboliphaunt native target {target}") - - -def _native_surfaces(target: str) -> list[str]: - 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"] - - -def _liboliphaunt_native_target_tables() -> list[dict]: - product = "liboliphaunt-native" - config = _release_target_config(product, "liboliphaunt-native") - published = set(_target_list(config, product, "publishedTargets")) - planned = _planned_targets(config, product) - _check_known_targets(product, [*published, *planned], set(NATIVE_RUNTIME_TARGETS)) - if published & set(planned): - product_metadata.fail(f"Moon release metadata for {product} declares targets as both published and planned") - - rows: list[dict] = [] - for target in sorted([*published, *planned]): - platform = NATIVE_RUNTIME_TARGETS[target] - published_target = target in published - row = { - "id": f"{product}.{target}", - "product": product, - "kind": "native-runtime", - "target": target, - "triple": platform["triple"], - "runner": platform["runner"], - "asset": _archive_asset("liboliphaunt", target, platform.get("archive", "tar.gz")), - "library_relative_path": _native_library_relative_path(target), - "npm_package": platform.get("liboliphaunt_npm_package"), - "npm_os": platform.get("npm_os"), - "npm_cpu": platform.get("npm_cpu"), - "npm_libc": platform.get("npm_libc"), - "surfaces": _native_surfaces(target), - "published": published_target, - "_source_file": "Moon release metadata", - } - if not published_target: - row["tier"] = "planned" - row["unsupported_reason"] = planned[target]["unsupportedReason"] - rows.append(row) - - rows.extend( - [ - { - "id": f"{product}.apple-spm-xcframework", - "product": 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": f"{product}.runtime-resources", - "product": 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": f"{product}.icu-data", - "product": 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": f"{product}.package-size", - "product": 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": f"{product}.checksums", - "product": product, - "kind": "checksums", - "target": "portable", - "asset": "liboliphaunt-{version}-release-assets.sha256", - "surfaces": ["github-release"], - "published": True, - "_source_file": "Moon release metadata", - }, - ] - ) - 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("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": ["github-release", "rust-native-direct", "typescript-native-direct"], - "published": True, - "_source_file": "Moon release metadata", - } - ) - return rows - - -def _liboliphaunt_wasix_target_tables() -> list[dict]: - product = "liboliphaunt-wasix" - config = _release_target_config(product, "liboliphaunt-wasix") - published = set(_target_list(config, product, "publishedTargets")) - _check_known_targets(product, published, WASIX_TARGETS) - if "portable" not in published: - product_metadata.fail(f"Moon release metadata for {product} must publish the portable runtime target") - - rows: list[dict] = [ - { - "id": f"{product}.runtime-portable", - "product": product, - "kind": "wasix-runtime", - "target": "portable", - "asset": "liboliphaunt-wasix-{version}-runtime-portable.tar.zst", - "surfaces": ["github-release"], - "published": True, - "_source_file": "Moon release metadata", - } - ] - rows.append( - { - "id": f"{product}.icu-data", - "product": 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 target in sorted(published - {"portable"}): - platform = DESKTOP_TARGETS[target] - rows.append( - { - "id": f"{product}.aot-{target}", - "product": product, - "kind": "wasix-aot-runtime", - "target": target, - "triple": platform["triple"], - "runner": platform["runner"], - "llvm_url": platform["wasix_llvm_url"], - "asset": f"liboliphaunt-wasix-{{version}}-runtime-aot-{target}.tar.zst", - "surfaces": ["github-release"], - "published": True, - "_source_file": "Moon release metadata", - } - ) - rows.append( - { - "id": f"{product}.checksums", - "product": product, - "kind": "checksums", - "target": "portable", - "asset": "liboliphaunt-wasix-{version}-release-assets.sha256", - "surfaces": ["github-release"], - "published": True, - "_source_file": "Moon release metadata", - } - ) - return rows - - -def _broker_target_tables() -> list[dict]: - product = "oliphaunt-broker" - config = _release_target_config(product, "broker-helper") - published = set(_target_list(config, product, "publishedTargets")) - _check_known_targets(product, published, BROKER_TARGETS) - rows: list[dict] = [] - for target in sorted(published): - platform = DESKTOP_TARGETS[target] - rows.append( - { - "id": f"{product}.{target}", - "product": product, - "kind": "broker-helper", - "target": target, - "triple": platform["triple"], - "runner": platform["runner"], - "asset": _archive_asset("oliphaunt-broker", target, platform["archive"]), - "executable_relative_path": "bin/oliphaunt-broker.exe" if target == "windows-x64-msvc" else "bin/oliphaunt-broker", - "npm_package": platform["broker_npm_package"], - "npm_os": platform.get("npm_os"), - "npm_cpu": platform.get("npm_cpu"), - "npm_libc": platform.get("npm_libc"), - "surfaces": ["github-release", "rust-broker", "typescript-broker"], - "published": True, - "_source_file": "Moon release metadata", - } - ) - rows.append( - { - "id": f"{product}.checksums", - "product": 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 - - -def _node_direct_target_tables() -> list[dict]: - product = "oliphaunt-node-direct" - config = _release_target_config(product, "node-direct-addon") - published = set(_target_list(config, product, "publishedTargets")) - _check_known_targets(product, published, NODE_DIRECT_TARGETS) - rows: list[dict] = [] - for target in sorted(published): - platform = DESKTOP_TARGETS[target] - rows.append( - { - "id": f"{product}.{target}", - "product": product, - "kind": "node-direct-addon", - "target": target, - "triple": platform["triple"], - "runner": platform["runner"], - "asset": _archive_asset("oliphaunt-node-direct", target, platform["archive"]), - "library_relative_path": "oliphaunt_node.node", - "npm_package": platform["node_package"], - "npm_os": platform.get("npm_os"), - "npm_cpu": platform.get("npm_cpu"), - "npm_libc": platform.get("npm_libc"), - "surfaces": ["github-release", "npm-optional"], - "published": True, - "_source_file": "Moon release metadata", - } - ) - rows.append( - { - "id": f"{product}.checksums", - "product": 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 - - -def _moon_target_tables() -> list[dict]: - return [ - *_liboliphaunt_native_target_tables(), - *_liboliphaunt_wasix_target_tables(), - *_broker_target_tables(), - *_node_direct_target_tables(), - ] - - -def raw_artifact_target_tables(graph: dict | None = None) -> list[dict]: - """Return artifact target tables from Moon release metadata.""" - - data = graph if graph is not None else product_metadata.load_graph() - graph_targets = data.get("artifact_targets", []) - if not isinstance(graph_targets, list): - product_metadata.fail("compatibility artifact_targets must be an array of tables") - tables: list[dict] = _moon_target_tables() - for raw in graph_targets: - if not isinstance(raw, dict): - product_metadata.fail("compatibility artifact_targets entries must be tables") - table = dict(raw) - table.setdefault("_source_file", "product metadata compatibility graph") - tables.append(table) - return tables - - -def artifact_targets( - graph: dict | None = None, - *, - product: str | None = None, - kind: str | None = None, - surface: str | None = None, - published_only: bool = False, -) -> list[ArtifactTarget]: - data = graph if graph is not None else product_metadata.load_graph() - raw_targets = raw_artifact_target_tables(data) - - products = product_metadata.graph_products(data) - parsed: list[ArtifactTarget] = [] - seen: set[str] = set() - for raw in raw_targets: - target_id = _string(raw.get("id"), "id", "") - assert target_id is not None - if target_id in seen: - source_file = raw.get("_source_file", "unknown source") - product_metadata.fail(f"duplicate artifact target id {target_id} in {source_file}") - seen.add(target_id) - - target_product = _string(raw.get("product"), "product", target_id) - assert target_product is not None - if target_product not in products: - product_metadata.fail(f"artifact target {target_id} references unknown product {target_product}") - - parsed_target = ArtifactTarget( - id=target_id, - product=target_product, - kind=_string(raw.get("kind"), "kind", target_id) or "", - target=_string(raw.get("target"), "target", target_id) or "", - asset=_string(raw.get("asset"), "asset", target_id) or "", - published=_published(raw.get("published"), target_id), - surfaces=_surfaces(raw.get("surfaces"), target_id), - triple=_string(raw.get("triple"), "triple", target_id, required=False), - runner=_string(raw.get("runner"), "runner", target_id, required=False), - library_relative_path=_string(raw.get("library_relative_path"), "library_relative_path", target_id, required=False), - executable_relative_path=_string(raw.get("executable_relative_path"), "executable_relative_path", target_id, required=False), - npm_package=_string(raw.get("npm_package"), "npm_package", target_id, required=False), - npm_os=_string(raw.get("npm_os"), "npm_os", target_id, required=False), - npm_cpu=_string(raw.get("npm_cpu"), "npm_cpu", target_id, required=False), - npm_libc=_string(raw.get("npm_libc"), "npm_libc", target_id, required=False), - llvm_url=_string(raw.get("llvm_url"), "llvm_url", target_id, required=False), - extension_artifacts=_optional_bool(raw.get("extension_artifacts"), "extension_artifacts", target_id, True), - ) - if product is not None and parsed_target.product != product: - continue - if kind is not None and parsed_target.kind != kind: - continue - if surface is not None and surface not in parsed_target.surfaces: - continue - if published_only and not parsed_target.published: - continue - parsed.append(parsed_target) - - return parsed - - -def expected_assets( - product: str, - version: str, - *, - surface: str = "github-release", - published_only: bool = True, - kinds: Iterable[str] | None = None, -) -> list[str]: - allowed_kinds = set(kinds) if kinds is not None else None - assets = [ - target.asset_name(version) - for target in artifact_targets( - product=product, - surface=surface, - published_only=published_only, - ) - if allowed_kinds is None or target.kind in allowed_kinds - ] - 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) - - -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_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": - 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 = [ - ("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_artifact_targets.py b/tools/release/check_artifact_targets.py index c05b80c2..f69c1596 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -10,7 +10,6 @@ from pathlib import Path from typing import NoReturn -import artifact_targets import product_metadata @@ -82,12 +81,12 @@ def reject_text(path: str, text: str, message: str) -> None: def validate_target_shape() -> None: - targets = artifact_targets.artifact_targets() + targets = product_metadata.artifact_targets() if not targets: fail("artifact target metadata must define targets") raw_targets = { raw.get("id"): raw - for raw in artifact_targets.raw_artifact_target_tables(product_metadata.load_graph()) + for raw in product_metadata.raw_artifact_target_tables(product_metadata.load_graph()) if isinstance(raw, dict) and isinstance(raw.get("id"), str) } @@ -205,7 +204,7 @@ def validate_extension_artifact_targets() -> None: expected_native_targets = { target.target - for target in artifact_targets.artifact_targets( + for target in product_metadata.artifact_targets( product="liboliphaunt-native", kind="native-runtime", published_only=True, @@ -214,7 +213,7 @@ def validate_extension_artifact_targets() -> None: } expected_wasix_targets = { wasm_extension_target_id(target.target) - for target in artifact_targets.artifact_targets( + for target in product_metadata.artifact_targets( product="liboliphaunt-wasix", published_only=True, ) @@ -428,10 +427,10 @@ 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(): + for artifact in product_metadata.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(): + for product in product_metadata.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( @@ -492,7 +491,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 artifact_targets.sdk_package_products(): + for project_id in product_metadata.sdk_package_products(): moon_file = ( "src/bindings/wasix-rust/moon.yml" if project_id == "oliphaunt-wasix-rust" @@ -676,7 +675,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 artifact_targets.sdk_package_products(): + for product_id in product_metadata.sdk_package_products(): require_text( "tools/release/release.py", f'validate_staged_sdk_package("{product_id}")', @@ -1180,7 +1179,7 @@ def validate_target_matrices() -> None: liboliphaunt_targets = {item["target"] for item in liboliphaunt_matrix["include"]} expected_liboliphaunt_targets = { target.target - for target in artifact_targets.artifact_targets( + for target in product_metadata.artifact_targets( product="liboliphaunt-native", kind="native-runtime", published_only=True, @@ -1213,7 +1212,7 @@ def validate_target_matrices() -> None: broker_targets = {item["target"] for item in broker_matrix["include"]} expected_broker_targets = { target.target - for target in artifact_targets.artifact_targets( + for target in product_metadata.artifact_targets( product="oliphaunt-broker", kind="broker-helper", published_only=True, @@ -1229,7 +1228,7 @@ def validate_target_matrices() -> None: node_direct_targets = {item["target"] for item in node_direct_matrix["include"]} expected_node_direct_targets = { target.target - for target in artifact_targets.artifact_targets( + for target in product_metadata.artifact_targets( product="oliphaunt-node-direct", kind="node-direct-addon", published_only=True, @@ -1260,7 +1259,7 @@ def validate_target_matrices() -> None: def validate_typescript_runtime_targets() -> None: - for target in artifact_targets.artifact_targets( + for target in product_metadata.artifact_targets( product="liboliphaunt-native", kind="native-runtime", surface="typescript-native-direct", @@ -1288,7 +1287,7 @@ def validate_typescript_runtime_targets() -> None: reject_text(path, target.npm_package, f"TypeScript native resolver must not advertise unpublished target {target.id}") reject_text(path, target.target, f"TypeScript native resolver must not expose unpublished target id {target.target}") - for target in artifact_targets.artifact_targets( + for target in product_metadata.artifact_targets( product="oliphaunt-broker", kind="broker-helper", surface="typescript-broker", @@ -1311,7 +1310,7 @@ def validate_typescript_runtime_targets() -> None: reject_text(path, target.npm_package, f"TypeScript broker resolver must not advertise unpublished target {target.id}") reject_text(path, target.target, f"TypeScript broker resolver must not expose unpublished target id {target.target}") - for target in artifact_targets.artifact_targets( + for target in product_metadata.artifact_targets( product="oliphaunt-node-direct", kind="node-direct-addon", surface="npm-optional", @@ -1351,7 +1350,7 @@ def validate_rust_broker_targets() -> None: "OLIPHAUNT_BROKER_ASSET_DIR", "Rust broker resolver must support package-shaped broker artifact fixtures", ) - for target in artifact_targets.artifact_targets( + for target in product_metadata.artifact_targets( product="oliphaunt-broker", kind="broker-helper", surface="rust-broker", @@ -1417,7 +1416,7 @@ def validate_expected_product_assets() -> None: for product, assets in expected.items(): actual = { target.asset - for target in artifact_targets.artifact_targets( + for target in product_metadata.artifact_targets( product=product, surface="github-release", published_only=True, diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 3270f837..bd28d4d9 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -17,7 +17,6 @@ from pathlib import Path from typing import NoReturn -import artifact_targets import product_metadata import package_liboliphaunt_wasix_cargo_artifacts @@ -273,7 +272,7 @@ def product_publish_targets(product: str) -> list[str]: def npm_registry_packages(product: str, kind: str, surface: str) -> set[str]: packages = set() - for target in artifact_targets.artifact_targets( + for target in product_metadata.artifact_targets( product=product, kind=kind, surface=surface, @@ -286,19 +285,19 @@ def npm_registry_packages(product: str, kind: str, surface: str) -> set[str]: def liboliphaunt_native_expected_registry_packages() -> set[str]: - runtime_targets = artifact_targets.artifact_targets( + runtime_targets = product_metadata.artifact_targets( product="liboliphaunt-native", kind="native-runtime", surface="rust-native-direct", published_only=True, ) - tools_targets = artifact_targets.artifact_targets( + tools_targets = product_metadata.artifact_targets( product="liboliphaunt-native", kind="native-tools", surface="typescript-native-direct", published_only=True, ) - android_targets = artifact_targets.artifact_targets( + android_targets = product_metadata.artifact_targets( product="liboliphaunt-native", kind="native-runtime", surface="maven", @@ -356,7 +355,7 @@ def native_npm_tool_split_failures( def broker_expected_registry_packages() -> set[str]: - targets = artifact_targets.artifact_targets( + targets = product_metadata.artifact_targets( product="oliphaunt-broker", kind="broker-helper", published_only=True, @@ -911,7 +910,7 @@ def check_broker(findings: list[Finding]) -> None: severity="P0", ) version = product_metadata.read_current_version(product) - for target in artifact_targets.artifact_targets( + for target in product_metadata.artifact_targets( product=product, kind="broker-helper", surface="rust-broker", @@ -1021,7 +1020,7 @@ def check_node_direct(findings: list[Finding]) -> None: severity="P0", ) - node_targets = artifact_targets.artifact_targets( + node_targets = product_metadata.artifact_targets( product=product, kind="node-direct-addon", surface="npm-optional", @@ -1458,7 +1457,7 @@ def check_typescript(findings: list[Finding]) -> None: f"src/sdks/js/package.json dependencies={package.get('dependencies')!r}", severity="P0", ) - expected_optional = artifact_targets.typescript_optional_runtime_package_versions() + expected_optional = product_metadata.typescript_optional_runtime_package_versions() optional_dependencies = package.get("optionalDependencies", {}) require( findings, @@ -1996,7 +1995,7 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: severity="P0", ) version = product_metadata.read_current_version(product) - expected_assets = set(artifact_targets.expected_assets(product, version, surface="github-release")) + expected_assets = set(product_metadata.expected_assets(product, version, surface="github-release")) require( findings, product, diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index f9d315b0..4a0240c8 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -10,7 +10,6 @@ from pathlib import Path from typing import NoReturn -import artifact_targets import package_liboliphaunt_wasix_cargo_artifacts import product_metadata import release @@ -116,7 +115,7 @@ def validate_platform_npm_packages( package_dirs = npm_package_dirs_under(package_root) targets = [ target - for target in artifact_targets.artifact_targets(product=product, kind=kind, surface=surface, published_only=True) + for target in product_metadata.artifact_targets(product=product, kind=kind, surface=surface, published_only=True) if target.npm_package is not None ] expected_packages = sorted(target.npm_package for target in targets if target.npm_package is not None) @@ -1082,7 +1081,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 = artifact_targets.typescript_optional_runtime_package_versions() + expected_optional = product_metadata.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/local_registry_publish.py b/tools/release/local_registry_publish.py index 5384befb..766d4f2b 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -34,7 +34,6 @@ from pathlib import Path from typing import Any, Iterable -import artifact_targets import product_metadata @@ -58,9 +57,9 @@ 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(), + product_metadata.ci_aggregate_release_asset_artifact_name("liboliphaunt-native"), + product_metadata.ci_aggregate_release_asset_artifact_name("liboliphaunt-wasix"), + *product_metadata.ci_wasix_runtime_artifact_names(), *product_metadata.ci_wasix_extension_artifact_names(), *product_metadata.ci_extension_package_artifact_names(), ] @@ -69,12 +68,12 @@ def local_publish_aggregate_artifacts() -> list[str]: def local_publish_artifacts() -> list[str]: 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"), - *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(), + *product_metadata.ci_release_asset_artifact_names("liboliphaunt-native", "native-runtime"), + *product_metadata.ci_wasix_aot_runtime_artifact_names(), + *product_metadata.ci_release_asset_artifact_names("oliphaunt-broker", "broker-helper"), + *product_metadata.ci_release_asset_artifact_names("oliphaunt-node-direct", "node-direct-addon"), + *product_metadata.ci_npm_package_artifact_names("oliphaunt-node-direct", "node-direct-addon"), + *product_metadata.ci_sdk_package_artifact_names(), ] duplicates = sorted({artifact for artifact in artifacts if artifacts.count(artifact) > 1}) if duplicates: diff --git a/tools/release/product_metadata.py b/tools/release/product_metadata.py index f835b579..47c91b54 100644 --- a/tools/release/product_metadata.py +++ b/tools/release/product_metadata.py @@ -14,10 +14,11 @@ import subprocess import sys import tomllib +from dataclasses import dataclass from functools import lru_cache from pathlib import Path from types import SimpleNamespace -from typing import Any, NoReturn +from typing import Any, Iterable, NoReturn ROOT = Path(__file__).resolve().parents[2] @@ -271,6 +272,294 @@ def load_graph() -> dict[str, Any]: } +@dataclass(frozen=True) +class ArtifactTarget: + id: str + product: str + kind: str + target: str + asset: str + published: bool + surfaces: tuple[str, ...] + triple: str | None = None + runner: str | None = None + library_relative_path: str | None = None + executable_relative_path: str | None = None + npm_package: str | None = None + npm_os: str | None = None + npm_cpu: str | None = None + npm_libc: str | None = None + llvm_url: str | None = None + extension_artifacts: bool = True + + def asset_name(self, version: str) -> str: + return self.asset.format(version=version) + + +@lru_cache(maxsize=None) +def _release_graph_query_rows(command: str, args: tuple[str, ...] = ()) -> tuple[dict[str, Any], ...]: + try: + output = subprocess.check_output( + ["tools/dev/bun.sh", "tools/release/release_graph_query.mjs", command, *args], + cwd=ROOT, + text=True, + stderr=subprocess.PIPE, + ) + except subprocess.CalledProcessError as error: + detail = (error.stderr or "").strip() + if detail: + fail(f"release graph {command} query failed: {detail}") + fail(f"release graph {command} query failed with exit code {error.returncode}") + rows = json.loads(output) + if not isinstance(rows, list) or not all(isinstance(row, dict) for row in rows): + fail(f"release graph {command} query must return a JSON object list") + return tuple(rows) + + +def _target_string(row: dict[str, Any], key: str, target_id: str, *, required: bool = True) -> str | None: + value = row.get(key) + if isinstance(value, str) and value: + return value + if required: + fail(f"artifact target {target_id}.{key} must be a non-empty string") + if value is not None: + fail(f"artifact target {target_id}.{key} must be a string") + return None + + +def _target_bool(row: dict[str, Any], key: str, target_id: str, *, default: bool | None = None) -> bool: + value = row.get(key) + if isinstance(value, bool): + return value + if value is None and default is not None: + return default + fail(f"artifact target {target_id}.{key} must be true or false") + + +def _target_surfaces(row: dict[str, Any], target_id: str) -> tuple[str, ...]: + value = row.get("surfaces") + if not isinstance(value, list) or not value or not all(isinstance(item, str) and item for item in value): + fail(f"artifact target {target_id}.surfaces must be a non-empty string list") + return tuple(value) + + +def _artifact_target_from_row(row: dict[str, Any]) -> ArtifactTarget: + target_id = _target_string(row, "id", "") + assert target_id is not None + return ArtifactTarget( + id=target_id, + product=_target_string(row, "product", target_id) or "", + kind=_target_string(row, "kind", target_id) or "", + target=_target_string(row, "target", target_id) or "", + asset=_target_string(row, "asset", target_id) or "", + published=_target_bool(row, "published", target_id), + surfaces=_target_surfaces(row, target_id), + triple=_target_string(row, "triple", target_id, required=False), + runner=_target_string(row, "runner", target_id, required=False), + library_relative_path=_target_string(row, "library_relative_path", target_id, required=False), + executable_relative_path=_target_string(row, "executable_relative_path", target_id, required=False), + npm_package=_target_string(row, "npm_package", target_id, required=False), + npm_os=_target_string(row, "npm_os", target_id, required=False), + npm_cpu=_target_string(row, "npm_cpu", target_id, required=False), + npm_libc=_target_string(row, "npm_libc", target_id, required=False), + llvm_url=_target_string(row, "llvm_url", target_id, required=False), + extension_artifacts=_target_bool(row, "extension_artifacts", target_id, default=True), + ) + + +def _artifact_target_args( + *, + product: str | None = None, + kind: str | None = None, + surface: str | None = None, + published_only: bool = False, +) -> tuple[str, ...]: + args: list[str] = [] + if product is not None: + args.extend(["--product", product]) + if kind is not None: + args.extend(["--kind", kind]) + if surface is not None: + args.extend(["--surface", surface]) + if published_only: + args.append("--published-only") + return tuple(args) + + +def raw_artifact_target_tables(graph: dict | None = None) -> list[dict[str, Any]]: + """Return raw artifact target rows from the canonical Bun release graph.""" + + return [ + dict(row) + for row in _release_graph_query_rows("raw-artifact-targets") + ] + + +def artifact_targets( + graph: dict | None = None, + *, + product: str | None = None, + kind: str | None = None, + surface: str | None = None, + published_only: bool = False, +) -> list[ArtifactTarget]: + rows = _release_graph_query_rows( + "artifact-targets", + _artifact_target_args( + product=product, + kind=kind, + surface=surface, + published_only=published_only, + ), + ) + return [_artifact_target_from_row(row) for row in rows] + + +def expected_assets( + product: str, + version: str, + *, + surface: str = "github-release", + published_only: bool = True, + kinds: Iterable[str] | None = None, +) -> list[str]: + allowed_kinds = set(kinds) if kinds is not None else None + assets = [ + target.asset_name(version) + for target in artifact_targets( + product=product, + surface=surface, + published_only=published_only, + ) + if allowed_kinds is None or target.kind in allowed_kinds + ] + if not assets: + 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: + 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: + fail(f"{product} has no published {kind} CI npm package targets") + 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: + fail("liboliphaunt-wasix has no published WASIX AOT runtime targets") + return sorted(names) + + +def ci_aggregate_release_asset_artifact_name(product: str) -> str: + config = product_config(product) + release_artifacts = config.get("release_artifacts") + if not isinstance(release_artifacts, list) or not release_artifacts: + 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: + fail("liboliphaunt-wasix has no published WASIX runtime targets") + return sorted(names) + + +def ci_sdk_package_artifact_name(product: str) -> str: + config = product_config(product) + if config.get("kind") != "sdk": + 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 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 = [ + ("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: + fail(f"{product} has no published {kind} TypeScript optional package targets") + for target in targets: + if target.npm_package is None: + fail(f"{target.id} must declare npm_package for TypeScript optional dependencies") + if target.npm_package in package_products: + 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: read_current_version(product) + for package_name, product in typescript_optional_runtime_package_products().items() + } + + def graph_products(graph: dict | None = None) -> dict[str, dict[str, Any]]: products: dict[str, dict[str, Any]] = {} manifest = _release_please_manifest() diff --git a/tools/release/release-artifact-targets.mjs b/tools/release/release-artifact-targets.mjs index 09ec15e0..4504aeda 100644 --- a/tools/release/release-artifact-targets.mjs +++ b/tools/release/release-artifact-targets.mjs @@ -486,7 +486,7 @@ function nodeDirectRows(prefix) { return rows; } -function rawArtifactTargetRows(prefix) { +export function rawArtifactTargetRows(prefix = "release-artifact-targets.mjs") { return [ ...liboliphauntNativeRows(prefix), ...liboliphauntWasixRows(prefix), @@ -511,6 +511,17 @@ function stringField(row, key, id, required, prefix) { function normalizeArtifactTarget(row, prefix) { const id = stringField(row, "id", "", true, prefix); + const libraryRelativePath = stringField(row, "library_relative_path", id, false, prefix); + const executableRelativePath = stringField(row, "executable_relative_path", id, false, prefix); + const npmPackage = stringField(row, "npm_package", id, false, prefix); + const npmOs = stringField(row, "npm_os", id, false, prefix); + const npmCpu = stringField(row, "npm_cpu", id, false, prefix); + const npmLibc = stringField(row, "npm_libc", id, false, prefix); + const llvmUrl = stringField(row, "llvm_url", id, false, prefix); + const sourceFile = + stringField(row, "_source_file", id, false, prefix) ?? + stringField(row, "source_file", id, false, prefix); + const unsupportedReason = stringField(row, "unsupported_reason", id, false, prefix); const target = { id, product: stringField(row, "product", id, true, prefix), @@ -521,14 +532,27 @@ function normalizeArtifactTarget(row, prefix) { 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), + libraryRelativePath, + executableRelativePath, + npmPackage, + npmOs, + npmCpu, + npmLibc, + llvmUrl, extensionArtifacts: row.extension_artifacts ?? true, + sourceFile, + tier: stringField(row, "tier", id, false, prefix), + unsupportedReason, + library_relative_path: libraryRelativePath, + executable_relative_path: executableRelativePath, + npm_package: npmPackage, + npm_os: npmOs, + npm_cpu: npmCpu, + npm_libc: npmLibc, + llvm_url: llvmUrl, + extension_artifacts: row.extension_artifacts ?? true, + source_file: sourceFile, + unsupported_reason: unsupportedReason, }; if (typeof target.published !== "boolean") { fail(prefix, `artifact target ${id}.published must be true or false`); diff --git a/tools/release/release.py b/tools/release/release.py index 785641d1..aff56fcf 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -17,7 +17,6 @@ from pathlib import Path, PurePosixPath from typing import NoReturn -import artifact_targets import package_liboliphaunt_wasix_cargo_artifacts import product_metadata @@ -712,7 +711,7 @@ def maven_pom_url(coordinate: str, version: str) -> str: ) -def rust_artifact_cargo_target_cfg(target: artifact_targets.ArtifactTarget) -> str: +def rust_artifact_cargo_target_cfg(target: product_metadata.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": @@ -741,7 +740,7 @@ def render_oliphaunt_release_cargo_toml(source: str, native_version: str, broker "# artifacts are published and indexed.", ] target_dependencies: dict[str, list[str]] = {} - for target in artifact_targets.artifact_targets( + for target in product_metadata.artifact_targets( product="liboliphaunt-native", kind="native-runtime", surface="rust-native-direct", @@ -752,7 +751,7 @@ def render_oliphaunt_release_cargo_toml(source: str, native_version: str, broker cfg = rust_artifact_cargo_target_cfg(target) target_dependencies.setdefault(cfg, []).append(f'{crate} = {{ version = "={native_version}" }}') target_dependencies.setdefault(cfg, []).append(f'{tools_facade} = {{ version = "={native_version}" }}') - for target in artifact_targets.artifact_targets( + for target in product_metadata.artifact_targets( product="oliphaunt-broker", kind="broker-helper", surface="rust-broker", @@ -783,7 +782,7 @@ def validate_generated_oliphaunt_release_artifact_coverage(manifest_path: Path) ) native_version = current_product_version("liboliphaunt-native") - native_targets = artifact_targets.artifact_targets( + native_targets = product_metadata.artifact_targets( product="liboliphaunt-native", kind="native-runtime", surface="rust-native-direct", @@ -909,7 +908,7 @@ def prepare_oliphaunt_release_source(version: str) -> Path: package = rendered.split("[package]", 1)[1].split("[", 1)[0] if f'version = "{version}"' not in package: fail(f"generated oliphaunt release source must keep SDK version {version}") - for target in artifact_targets.artifact_targets( + for target in product_metadata.artifact_targets( product="liboliphaunt-native", kind="native-runtime", surface="rust-native-direct", @@ -921,7 +920,7 @@ def prepare_oliphaunt_release_source(version: str) -> Path: tools_facade = LIBOLIPHAUNT_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( + for target in product_metadata.artifact_targets( product="oliphaunt-broker", kind="broker-helper", surface="rust-broker", @@ -968,7 +967,7 @@ def validate_wasix_release_assets() -> None: "target/oliphaunt-wasix/release-assets; download the CI workflow " "liboliphaunt-wasix-release-assets artifact before release validation or publishing" ) - expected = set(artifact_targets.expected_assets(product, version, surface="github-release")) + expected = set(product_metadata.expected_assets(product, version, surface="github-release")) actual = {path.name for path in asset_dir.iterdir() if path.is_file()} missing = sorted(expected - actual) if missing: @@ -1850,15 +1849,15 @@ def command_ci_artifacts(args: list[str]) -> None: 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) + names = product_metadata.ci_release_asset_artifact_names(parsed.product, parsed.kind) 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) + names = product_metadata.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) + names = product_metadata.ci_sdk_package_artifact_names(parsed.product) for name in names: print(name) @@ -1868,9 +1867,9 @@ def command_ci_products(args: list[str]) -> None: 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()) + sdk_products = set(product_metadata.sdk_package_products()) if parsed.products_json is None: - products = list(artifact_targets.sdk_package_products()) + products = list(product_metadata.sdk_package_products()) else: products = selected_products_from_passthrough(["--products-json", parsed.products_json]) for product in products: @@ -2105,10 +2104,10 @@ def publish_node_direct_release_assets(head_ref: str) -> None: upload_github_release_assets("oliphaunt-node-direct", assets=assets) -def node_direct_optional_package_targets(version: str) -> list[tuple[str, Path, artifact_targets.ArtifactTarget]]: +def node_direct_optional_package_targets(version: str) -> list[tuple[str, Path, product_metadata.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( + packages: list[tuple[str, Path, product_metadata.ArtifactTarget]] = [] + for target in product_metadata.artifact_targets( product="oliphaunt-node-direct", kind="node-direct-addon", surface="npm-optional", @@ -2155,10 +2154,10 @@ def artifact_npm_package_targets( kind: str, surface: str, package_root: Path, -) -> list[tuple[str, Path, artifact_targets.ArtifactTarget]]: +) -> list[tuple[str, Path, product_metadata.ArtifactTarget]]: package_dirs = npm_package_dirs_under(package_root) - packages: list[tuple[str, Path, artifact_targets.ArtifactTarget]] = [] - for target in artifact_targets.artifact_targets(product=product, kind=kind, surface=surface, published_only=True): + packages: list[tuple[str, Path, product_metadata.ArtifactTarget]] = [] + for target in product_metadata.artifact_targets(product=product, kind=kind, surface=surface, published_only=True): package_name = target.npm_package if package_name is None: fail(f"{target.id} must declare npm_package for npm artifact package publication") @@ -2783,7 +2782,7 @@ def broker_cargo_artifact_crates(version: str) -> list[tuple[str, Path, Path]]: source_root = ROOT / "target" / "oliphaunt-broker" / "cargo-package-sources" expected_crates = { broker_cargo_package_name(target.target) - for target in artifact_targets.artifact_targets( + for target in product_metadata.artifact_targets( product="oliphaunt-broker", kind="broker-helper", surface="rust-broker", @@ -2838,7 +2837,7 @@ 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( + native_targets = product_metadata.artifact_targets( product="liboliphaunt-native", kind="native-runtime", surface="rust-native-direct", diff --git a/tools/release/release_graph_query.mjs b/tools/release/release_graph_query.mjs index 719774ff..e3543875 100644 --- a/tools/release/release_graph_query.mjs +++ b/tools/release/release_graph_query.mjs @@ -1,6 +1,8 @@ #!/usr/bin/env bun import { + allArtifactTargets, extensionArtifactTargets, + rawArtifactTargetRows, } from "./release-artifact-targets.mjs"; import { buildPlan, @@ -149,6 +151,68 @@ function runPlansForPaths(argv) { ); } +function parseArtifactTargetOptions(argv) { + let product; + let kind; + let surface; + let publishedOnly = false; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === "--product") { + product = argv[++index]; + if (!product) { + fail("--product requires a value"); + } + } else if (value.startsWith("--product=")) { + product = value.slice("--product=".length); + } else if (value === "--kind") { + kind = argv[++index]; + if (!kind) { + fail("--kind requires a value"); + } + } else if (value.startsWith("--kind=")) { + kind = value.slice("--kind=".length); + } else if (value === "--surface") { + surface = argv[++index]; + if (!surface) { + fail("--surface requires a value"); + } + } else if (value.startsWith("--surface=")) { + surface = value.slice("--surface=".length); + } else if (value === "--published-only") { + publishedOnly = true; + } else { + fail(`unknown argument ${value}`); + } + } + return { product, kind, surface, publishedOnly }; +} + +function runArtifactTargets(argv) { + printJson(allArtifactTargets(parseArtifactTargetOptions(argv), TOOL)); +} + +function runRawArtifactTargets(argv) { + const { product, kind, surface, publishedOnly } = parseArtifactTargetOptions(argv); + printJson( + rawArtifactTargetRows(TOOL).filter((target) => { + 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 !== true) { + return false; + } + return true; + }), + ); +} + function runExtensionTargets(argv) { let product; let family; @@ -192,6 +256,8 @@ Commands: release-order --products-json JSON plan [--changed-file PATH...] plans-for-paths --paths-json JSON + artifact-targets [--product PRODUCT] [--kind KIND] [--surface SURFACE] [--published-only] + raw-artifact-targets [--product PRODUCT] [--kind KIND] [--surface SURFACE] [--published-only] extension-targets [--product PRODUCT] [--family native|wasix] [--published-only] `; } @@ -208,6 +274,10 @@ function main(argv) { runPlan(rest); } else if (command === "plans-for-paths") { runPlansForPaths(rest); + } else if (command === "artifact-targets") { + runArtifactTargets(rest); + } else if (command === "raw-artifact-targets") { + runRawArtifactTargets(rest); } else if (command === "extension-targets") { runExtensionTargets(rest); } else if (command === "--help" || command === "-h") { From 8a801c3172cca029dc3061400c4131b43247d911 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 04:15:12 +0000 Subject: [PATCH 147/308] chore: codify split wasix tools contract --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 17 ++- tools/release/check-staged-artifacts.mjs | 37 ++--- tools/release/check_consumer_shape.py | 25 ++-- tools/release/check_release_metadata.py | 23 ++-- tools/release/product_metadata.py | 93 ++++++++++++- tools/release/release.py | 9 +- tools/release/release_graph_query.mjs | 8 ++ .../release/wasix-cargo-artifact-contract.mjs | 130 ++++++++++++++++++ 8 files changed, 277 insertions(+), 65 deletions(-) create mode 100644 tools/release/wasix-cargo-artifact-contract.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 5fd45597..ef13bd72 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -807,11 +807,18 @@ until the current-state gates here are checked with fresh local evidence. names from `artifact_targets`, with only registry naming conventions kept in the checker. - 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. + tools, ICU, root AOT, tools-AOT crate names, AOT target-cfg dependency maps, + and `tools` feature dependency expectations from + `tools/release/wasix-cargo-artifact-contract.mjs` via + `release_graph_query.mjs wasix-cargo-artifact-contract`. Release metadata, + consumer-shape, release publication, and staged artifact checks consume that + shared contract instead of importing the WASIX cargo artifact packager for + read-only metadata. Focused validation passed with + `tools/dev/bun.sh tools/release/check-staged-artifacts.mjs --help`, + `tools/dev/bun.sh tools/release/release_graph_query.mjs wasix-cargo-artifact-contract`, + `python3 tools/release/check_release_metadata.py`, and + `python3 tools/release/check_consumer_shape.py --products-json + '["liboliphaunt-native","liboliphaunt-wasix","oliphaunt-wasix-rust","oliphaunt-rust"]'`. - 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 diff --git a/tools/release/check-staged-artifacts.mjs b/tools/release/check-staged-artifacts.mjs index 4bb48e30..0e382d37 100644 --- a/tools/release/check-staged-artifacts.mjs +++ b/tools/release/check-staged-artifacts.mjs @@ -18,40 +18,21 @@ import { extensionArtifactTargets, } from "./release-artifact-targets.mjs"; import { loadGraph } from "./release-graph.mjs"; +import { + AOT_PACKAGES as WASIX_AOT_PACKAGES, + AOT_TARGET_CFGS as WASIX_AOT_TARGET_CFGS, + AOT_TARGET_TRIPLES as WASIX_AOT_TARGET_TRIPLES, + ICU_PACKAGE, + RUNTIME_PACKAGE as WASIX_RUNTIME_PACKAGE, + TOOLS_AOT_PACKAGES as WASIX_TOOLS_AOT_PACKAGES, + TOOLS_PACKAGE as WASIX_TOOLS_PACKAGE, +} from "./wasix-cargo-artifact-contract.mjs"; const PREFIX = "check-staged-artifacts.mjs"; const SDK_ROOT = path.join(ROOT, "target/sdk-artifacts"); const EXTENSION_ROOT = path.join(ROOT, "target/extension-artifacts"); const MOBILE_ROOT = path.join(ROOT, "target/mobile-build/react-native"); -const WASIX_RUNTIME_PACKAGE = "liboliphaunt-wasix-portable"; -const WASIX_TOOLS_PACKAGE = "oliphaunt-wasix-tools"; -const ICU_PACKAGE = "oliphaunt-icu"; -const WASIX_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", -}; -const WASIX_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", -}; -const WASIX_AOT_TARGET_TRIPLES = { - "macos-arm64": "aarch64-apple-darwin", - "linux-arm64-gnu": "aarch64-unknown-linux-gnu", - "linux-x64-gnu": "x86_64-unknown-linux-gnu", - "windows-x64-msvc": "x86_64-pc-windows-msvc", -}; -const WASIX_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"))', -}; - const PUBLIC_EXTENSION_RELEASE_MANIFEST_KEYS = new Set([ "schema", "product", diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index bd28d4d9..11cb4d5e 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -18,7 +18,6 @@ from typing import NoReturn import product_metadata -import package_liboliphaunt_wasix_cargo_artifacts ROOT = Path(__file__).resolve().parents[2] @@ -1582,7 +1581,7 @@ def check_wasm(findings: list[Finding]) -> None: severity="P0", ) expected_tools_feature = ( - package_liboliphaunt_wasix_cargo_artifacts.public_tools_feature_dependencies() + product_metadata.wasix_public_tools_feature_dependencies() ) require( findings, @@ -1677,10 +1676,10 @@ def check_wasm(findings: list[Finding]) -> None: severity="P0", ) expected_aot_dependencies = ( - package_liboliphaunt_wasix_cargo_artifacts.public_aot_cargo_dependencies() + product_metadata.wasix_public_aot_cargo_dependencies() ) expected_tools_aot_dependencies = ( - package_liboliphaunt_wasix_cargo_artifacts.public_tools_aot_cargo_dependencies() + product_metadata.wasix_public_tools_aot_cargo_dependencies() ) missing_aot_dependencies = [] for cfg, crate in expected_aot_dependencies.items(): @@ -1888,7 +1887,7 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: registry_packages = set(product_registry_packages(product)) expected_registry_packages = { f"crates:{name}" - for name in package_liboliphaunt_wasix_cargo_artifacts.public_cargo_package_names() + for name in product_metadata.wasix_public_cargo_package_names() } require( findings, @@ -1918,13 +1917,13 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: findings, product, "wasix-portable-runtime-tool-contract", - package_liboliphaunt_wasix_cargo_artifacts.CORE_RUNTIME_ARCHIVE_FILES + product_metadata.wasix_core_runtime_archive_files() == ("oliphaunt/bin/initdb", "oliphaunt/bin/postgres") - and package_liboliphaunt_wasix_cargo_artifacts.TOOLS_PAYLOAD_FILES + and product_metadata.wasix_tools_payload_files() == ("bin/pg_dump.wasix.wasm", "bin/psql.wasix.wasm") - and package_liboliphaunt_wasix_cargo_artifacts.FORBIDDEN_RUNTIME_ARCHIVE_TOOL_FILES + and product_metadata.wasix_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 + and product_metadata.wasix_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 @@ -2068,10 +2067,10 @@ 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_package = product_metadata.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 + product_metadata.wasix_extension_aot_package_name(product, target) + for target in product_metadata.wasix_expected_extension_aot_targets() } native_qualified_registry_packages = [ package for package in product_registry_packages(product) if "-native-" in package @@ -2090,7 +2089,7 @@ def check_exact_extension(findings: list[Finding], product: str) -> None: and wasix_aot_packages == { f"{product}-wasix-aot-{target}" - for target in package_liboliphaunt_wasix_cargo_artifacts.EXPECTED_EXTENSION_AOT_TARGETS + for target in product_metadata.wasix_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.", diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 4a0240c8..5d8ffcbb 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -10,7 +10,6 @@ from pathlib import Path from typing import NoReturn -import package_liboliphaunt_wasix_cargo_artifacts import product_metadata import release @@ -281,11 +280,11 @@ def validate_exact_extension_registry_shape(graph: dict) -> None: 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) + wasix_package = product_metadata.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) + for target in product_metadata.wasix_expected_extension_aot_targets(): + package = product_metadata.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}") @@ -1413,10 +1412,10 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None ): fail("oliphaunt-wasix source must optionally depend on the local oliphaunt-icu path crate version") expected_aot_dependencies = ( - package_liboliphaunt_wasix_cargo_artifacts.public_aot_cargo_dependencies() + product_metadata.wasix_public_aot_cargo_dependencies() ) expected_tools_aot_dependencies = ( - package_liboliphaunt_wasix_cargo_artifacts.public_tools_aot_cargo_dependencies() + product_metadata.wasix_public_tools_aot_cargo_dependencies() ) target_tables = manifest.get("target", {}) for cfg, crate in expected_aot_dependencies.items(): @@ -1436,7 +1435,7 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None ): fail(f"oliphaunt-wasix must optionally depend on {crate} at the exact liboliphaunt-wasix runtime version behind {cfg}") expected_tools_feature = ( - package_liboliphaunt_wasix_cargo_artifacts.public_tools_feature_dependencies() + product_metadata.wasix_public_tools_feature_dependencies() ) tools_feature = set(manifest.get("features", {}).get("tools", [])) if tools_feature != expected_tools_feature: @@ -1471,13 +1470,13 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None 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 + product_metadata.wasix_core_runtime_archive_files() != ("oliphaunt/bin/initdb", "oliphaunt/bin/postgres") - or package_liboliphaunt_wasix_cargo_artifacts.TOOLS_PAYLOAD_FILES + or product_metadata.wasix_tools_payload_files() != ("bin/pg_dump.wasix.wasm", "bin/psql.wasix.wasm") - or package_liboliphaunt_wasix_cargo_artifacts.FORBIDDEN_RUNTIME_ARCHIVE_TOOL_FILES + or product_metadata.wasix_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 + or product_metadata.wasix_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 @@ -1562,7 +1561,7 @@ 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 = { f"crates:{name}" - for name in package_liboliphaunt_wasix_cargo_artifacts.public_cargo_package_names() + for name in product_metadata.wasix_public_cargo_package_names() } if registry_packages != expected_registry_packages: fail( diff --git a/tools/release/product_metadata.py b/tools/release/product_metadata.py index 47c91b54..d9b3c436 100644 --- a/tools/release/product_metadata.py +++ b/tools/release/product_metadata.py @@ -297,7 +297,7 @@ def asset_name(self, version: str) -> str: @lru_cache(maxsize=None) -def _release_graph_query_rows(command: str, args: tuple[str, ...] = ()) -> tuple[dict[str, Any], ...]: +def _release_graph_query_json(command: str, args: tuple[str, ...] = ()) -> Any: try: output = subprocess.check_output( ["tools/dev/bun.sh", "tools/release/release_graph_query.mjs", command, *args], @@ -310,7 +310,12 @@ def _release_graph_query_rows(command: str, args: tuple[str, ...] = ()) -> tuple if detail: fail(f"release graph {command} query failed: {detail}") fail(f"release graph {command} query failed with exit code {error.returncode}") - rows = json.loads(output) + return json.loads(output) + + +@lru_cache(maxsize=None) +def _release_graph_query_rows(command: str, args: tuple[str, ...] = ()) -> tuple[dict[str, Any], ...]: + rows = _release_graph_query_json(command, args) if not isinstance(rows, list) or not all(isinstance(row, dict) for row in rows): fail(f"release graph {command} query must return a JSON object list") return tuple(rows) @@ -415,6 +420,90 @@ def artifact_targets( return [_artifact_target_from_row(row) for row in rows] +@lru_cache(maxsize=1) +def _wasix_cargo_artifact_contract() -> dict[str, Any]: + value = _release_graph_query_json("wasix-cargo-artifact-contract") + if not isinstance(value, dict): + fail("release graph wasix-cargo-artifact-contract query must return a JSON object") + return value + + +def _wasix_contract_string(key: str) -> str: + value = _wasix_cargo_artifact_contract().get(key) + if not isinstance(value, str) or not value: + fail(f"WASIX Cargo artifact contract {key} must be a non-empty string") + return value + + +def _wasix_contract_string_list(key: str) -> tuple[str, ...]: + value = _wasix_cargo_artifact_contract().get(key) + if not isinstance(value, list) or not all(isinstance(item, str) and item for item in value): + fail(f"WASIX Cargo artifact contract {key} must be a string list") + return tuple(value) + + +def _wasix_contract_string_map(key: str) -> dict[str, str]: + value = _wasix_cargo_artifact_contract().get(key) + if not isinstance(value, dict) or not all( + isinstance(item_key, str) and item_key and isinstance(item_value, str) and item_value + for item_key, item_value in value.items() + ): + fail(f"WASIX Cargo artifact contract {key} must be a string map") + return dict(value) + + +def wasix_cargo_artifact_schema() -> str: + return _wasix_contract_string("schema") + + +def wasix_public_cargo_package_names() -> tuple[str, ...]: + return _wasix_contract_string_list("publicCargoPackageNames") + + +def wasix_public_aot_cargo_dependencies() -> dict[str, str]: + return _wasix_contract_string_map("publicAotCargoDependencies") + + +def wasix_public_tools_aot_cargo_dependencies() -> dict[str, str]: + return _wasix_contract_string_map("publicToolsAotCargoDependencies") + + +def wasix_public_tools_feature_dependencies() -> set[str]: + return set(_wasix_contract_string_list("publicToolsFeatureDependencies")) + + +def wasix_core_runtime_archive_files() -> tuple[str, ...]: + return _wasix_contract_string_list("coreRuntimeArchiveFiles") + + +def wasix_tools_payload_files() -> tuple[str, ...]: + return _wasix_contract_string_list("toolsPayloadFiles") + + +def wasix_forbidden_runtime_archive_tool_files() -> tuple[str, ...]: + return _wasix_contract_string_list("forbiddenRuntimeArchiveToolFiles") + + +def wasix_tools_aot_artifacts() -> set[str]: + return set(_wasix_contract_string_list("toolsAotArtifacts")) + + +def wasix_expected_extension_aot_targets() -> tuple[str, ...]: + return _wasix_contract_string_list("expectedExtensionAotTargets") + + +def wasix_extension_package_name(product: str) -> str: + if not product: + fail("WASIX extension package product must be non-empty") + return f"{product}-wasix" + + +def wasix_extension_aot_package_name(product: str, target: str) -> str: + if not product or not target: + fail("WASIX extension AOT package product and target must be non-empty") + return f"{product}-wasix-aot-{target}" + + def expected_assets( product: str, version: str, diff --git a/tools/release/release.py b/tools/release/release.py index aff56fcf..f5ba1676 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -17,7 +17,6 @@ from pathlib import Path, PurePosixPath from typing import NoReturn -import package_liboliphaunt_wasix_cargo_artifacts import product_metadata @@ -834,7 +833,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 = set(package_liboliphaunt_wasix_cargo_artifacts.public_cargo_package_names()) + artifact_crates = set(product_metadata.wasix_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) @@ -850,7 +849,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 = set(package_liboliphaunt_wasix_cargo_artifacts.public_cargo_package_names()) + required_crates = set(product_metadata.wasix_public_cargo_package_names()) missing = [ crate for crate in sorted(required_crates) @@ -2935,11 +2934,11 @@ def liboliphaunt_wasix_cargo_artifact_crates(version: str) -> list[tuple[str, Pa fail(f"missing generated liboliphaunt-wasix Cargo artifact manifest: {manifest_path.relative_to(ROOT)}") data = json.loads(manifest_path.read_text(encoding="utf-8")) packages_data = data.get("packages") - if data.get("schema") != package_liboliphaunt_wasix_cargo_artifacts.SCHEMA or not isinstance(packages_data, list): + if data.get("schema") != product_metadata.wasix_cargo_artifact_schema() or not isinstance(packages_data, list): fail(f"{manifest_path.relative_to(ROOT)} has an invalid schema") expected_base_crates = set( - package_liboliphaunt_wasix_cargo_artifacts.public_cargo_package_names() + product_metadata.wasix_public_cargo_package_names() ) configured_crates = set(cratesio_product_crates("liboliphaunt-wasix")) if configured_crates != expected_base_crates: diff --git a/tools/release/release_graph_query.mjs b/tools/release/release_graph_query.mjs index e3543875..85a15f27 100644 --- a/tools/release/release_graph_query.mjs +++ b/tools/release/release_graph_query.mjs @@ -12,6 +12,7 @@ import { releaseOrder, releaseProductProjectId, } from "./release-graph.mjs"; +import { wasixCargoArtifactContract } from "./wasix-cargo-artifact-contract.mjs"; const TOOL = "release_graph_query.mjs"; @@ -247,6 +248,10 @@ function runExtensionTargets(argv) { printJson(extensionArtifactTargets({ product, family, publishedOnly }, TOOL)); } +function runWasixCargoArtifactContract() { + printJson(wasixCargoArtifactContract()); +} + function usage() { return `usage: tools/release/release_graph_query.mjs [options] @@ -259,6 +264,7 @@ Commands: artifact-targets [--product PRODUCT] [--kind KIND] [--surface SURFACE] [--published-only] raw-artifact-targets [--product PRODUCT] [--kind KIND] [--surface SURFACE] [--published-only] extension-targets [--product PRODUCT] [--family native|wasix] [--published-only] + wasix-cargo-artifact-contract `; } @@ -280,6 +286,8 @@ function main(argv) { runRawArtifactTargets(rest); } else if (command === "extension-targets") { runExtensionTargets(rest); + } else if (command === "wasix-cargo-artifact-contract") { + runWasixCargoArtifactContract(); } else if (command === "--help" || command === "-h") { console.log(usage()); } else { diff --git a/tools/release/wasix-cargo-artifact-contract.mjs b/tools/release/wasix-cargo-artifact-contract.mjs new file mode 100644 index 00000000..c33a32c1 --- /dev/null +++ b/tools/release/wasix-cargo-artifact-contract.mjs @@ -0,0 +1,130 @@ +import { compareText } from "./release-graph.mjs"; + +export const WASIX_CARGO_ARTIFACT_SCHEMA = "oliphaunt-liboliphaunt-wasix-cargo-artifacts-v2"; +export const RUNTIME_PACKAGE = "liboliphaunt-wasix-portable"; +export const TOOLS_PACKAGE = "oliphaunt-wasix-tools"; +export const ICU_PACKAGE = "oliphaunt-icu"; +export const ICU_PAYLOAD_ARCHIVE = "icu-data.tar.zst"; + +export const TOOLS_PAYLOAD_FILES = [ + "bin/pg_dump.wasix.wasm", + "bin/psql.wasix.wasm", +]; + +export const CORE_RUNTIME_ARCHIVE_FILES = [ + "oliphaunt/bin/initdb", + "oliphaunt/bin/postgres", +]; + +export const FORBIDDEN_RUNTIME_ARCHIVE_TOOL_FILES = [ + "oliphaunt/bin/pg_ctl", + "oliphaunt/bin/pg_dump", + "oliphaunt/bin/psql", +]; + +export const TOOLS_AOT_ARTIFACTS = [ + "tool:pg_dump", + "tool:psql", +]; + +export const 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", +}; + +export const 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", +}; + +export const AOT_TARGET_TRIPLES = { + "macos-arm64": "aarch64-apple-darwin", + "linux-arm64-gnu": "aarch64-unknown-linux-gnu", + "linux-x64-gnu": "x86_64-unknown-linux-gnu", + "windows-x64-msvc": "x86_64-pc-windows-msvc", +}; + +export const 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"))', +}; + +export function publicCargoPackageNames() { + return [ + ICU_PACKAGE, + RUNTIME_PACKAGE, + TOOLS_PACKAGE, + ...Object.values(AOT_PACKAGES), + ...Object.values(TOOLS_AOT_PACKAGES), + ].sort(compareText); +} + +export function publicAotCargoDependencies() { + return Object.fromEntries( + Object.keys(AOT_PACKAGES) + .sort(compareText) + .map((target) => [ + AOT_TARGET_CFGS[AOT_TARGET_TRIPLES[target]], + AOT_PACKAGES[target], + ]), + ); +} + +export function publicToolsAotCargoDependencies() { + return Object.fromEntries( + Object.keys(TOOLS_AOT_PACKAGES) + .sort(compareText) + .map((target) => [ + AOT_TARGET_CFGS[AOT_TARGET_TRIPLES[target]], + TOOLS_AOT_PACKAGES[target], + ]), + ); +} + +export function publicToolsFeatureDependencies() { + return [ + `dep:${TOOLS_PACKAGE}`, + ...Object.values(TOOLS_AOT_PACKAGES).map((name) => `dep:${name}`), + ].sort(compareText); +} + +export function wasixExtensionPackageName(product) { + return `${product}-wasix`; +} + +export function wasixExtensionAotPackageName(product, target) { + return `${product}-wasix-aot-${target}`; +} + +export function expectedExtensionAotTargets() { + return [...new Set(Object.values(AOT_TARGET_TRIPLES))].sort(compareText); +} + +export function wasixCargoArtifactContract() { + return { + schema: WASIX_CARGO_ARTIFACT_SCHEMA, + runtimePackage: RUNTIME_PACKAGE, + toolsPackage: TOOLS_PACKAGE, + icuPackage: ICU_PACKAGE, + icuPayloadArchive: ICU_PAYLOAD_ARCHIVE, + coreRuntimeArchiveFiles: [...CORE_RUNTIME_ARCHIVE_FILES], + toolsPayloadFiles: [...TOOLS_PAYLOAD_FILES], + forbiddenRuntimeArchiveToolFiles: [...FORBIDDEN_RUNTIME_ARCHIVE_TOOL_FILES], + toolsAotArtifacts: [...TOOLS_AOT_ARTIFACTS], + aotPackages: { ...AOT_PACKAGES }, + toolsAotPackages: { ...TOOLS_AOT_PACKAGES }, + aotTargetTriples: { ...AOT_TARGET_TRIPLES }, + aotTargetCfgs: { ...AOT_TARGET_CFGS }, + expectedExtensionAotTargets: expectedExtensionAotTargets(), + publicCargoPackageNames: publicCargoPackageNames(), + publicAotCargoDependencies: publicAotCargoDependencies(), + publicToolsAotCargoDependencies: publicToolsAotCargoDependencies(), + publicToolsFeatureDependencies: publicToolsFeatureDependencies(), + }; +} From fce6b4fdf5f8329a9d28d57bbcf41547ebb951d2 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 04:24:53 +0000 Subject: [PATCH 148/308] chore: route product metadata through bun graph --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 15 ++ tools/release/product_metadata.py | 163 ++++-------------- 2 files changed, 51 insertions(+), 127 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index ef13bd72..479355ca 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,21 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Reduced duplicate Python release graph modeling in + `tools/release/product_metadata.py`. `load_graph()`, `graph_products()`, + `product_config()`, product ids, extension product ids, `package_path()`, and + Moon release metadata lookups now consume the canonical Bun + `release_graph_query.mjs graph` output instead of rebuilding the product path + map from Python release-please and Moon parsing. The remaining Python helpers + still read release-please config only where they validate release-please + version-file and changelog semantics directly. Fresh checks passed: + graph-backed helper parity against `tools/dev/bun.sh + tools/release/release_graph_query.mjs graph`, `python3 -m py_compile` for all + remaining Python release/policy helpers, `python3 + tools/release/check_artifact_targets.py`, `python3 + tools/release/check_release_metadata.py`, and focused `python3 + tools/release/check_consumer_shape.py --products-json + '["liboliphaunt-native","liboliphaunt-wasix","oliphaunt-rust","oliphaunt-wasix-rust","oliphaunt-js"]'`. - 2026-06-27: Removed the duplicate Python runtime/helper artifact target model in `tools/release/artifact_targets.py`. Python release callers now use `product_metadata.artifact_targets()` compatibility wrappers backed by the diff --git a/tools/release/product_metadata.py b/tools/release/product_metadata.py index d9b3c436..4aaa6ed0 100644 --- a/tools/release/product_metadata.py +++ b/tools/release/product_metadata.py @@ -9,7 +9,6 @@ from __future__ import annotations import json -import os import re import subprocess import sys @@ -23,7 +22,6 @@ ROOT = Path(__file__).resolve().parents[2] RELEASE_PLEASE_CONFIG_PATH = ROOT / "release-please-config.json" -RELEASE_PLEASE_MANIFEST_PATH = ROOT / ".release-please-manifest.json" EXTENSION_CLASSES = {"contrib", "external", "first-party"} EXTENSION_VERSIONING_BY_CLASS = { "contrib": "postgres-bound", @@ -86,18 +84,6 @@ def _release_please_config() -> dict[str, Any]: return _read_json(RELEASE_PLEASE_CONFIG_PATH) -@lru_cache(maxsize=1) -def _release_please_manifest() -> dict[str, Any]: - return _read_json(RELEASE_PLEASE_MANIFEST_PATH) - - -def _moon_bin() -> str: - if moon_bin := os.environ.get("MOON_BIN"): - return moon_bin - proto_moon = Path.home() / ".proto" / "bin" / "moon" - return str(proto_moon) if proto_moon.exists() else "moon" - - @lru_cache(maxsize=1) def _packages() -> dict[str, dict[str, Any]]: packages = _release_please_config().get("packages") @@ -126,99 +112,21 @@ def _release_please_packages_by_component() -> dict[str, tuple[str, dict[str, An return packages -@lru_cache(maxsize=1) -def _moon_query_projects() -> list[dict[str, Any]]: - output = subprocess.check_output([_moon_bin(), "query", "projects"], cwd=ROOT, text=True) - value = json.loads(output) - projects = value.get("projects") - if not isinstance(projects, list): - fail("moon query projects did not return a projects array") - return projects - - -def _moon_project_release_metadata(project: dict[str, Any]) -> dict[str, Any] | None: - config = project.get("config") if isinstance(project.get("config"), dict) else {} - project_config = config.get("project") if isinstance(config.get("project"), dict) else {} - metadata = project_config.get("metadata") if isinstance(project_config.get("metadata"), dict) else {} - release = metadata.get("release") - return release if isinstance(release, dict) else None - - -@lru_cache(maxsize=1) -def _moon_release_projects_by_component() -> dict[str, dict[str, Any]]: - projects: dict[str, dict[str, Any]] = {} - for project in _moon_query_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 {} - tags = config.get("tags") if isinstance(config.get("tags"), list) else [] - release = _moon_project_release_metadata(project) - if "release-product" not in tags: - if release is not None: - fail(f"Moon project {project['id']} declares release metadata but is not tagged release-product") - continue - if release is None: - fail(f"Moon release product {project['id']} must declare project.metadata.release") - component = release.get("component") - package_path = release.get("packagePath") - if not isinstance(component, str) or not component: - fail(f"Moon release product {project['id']} must declare release.component") - if component != project["id"]: - fail(f"Moon release product {project['id']} release.component must match the project id") - if not isinstance(package_path, str) or not package_path: - fail(f"Moon release product {project['id']} must declare release.packagePath") - if component in projects: - fail(f"duplicate Moon release component {component}") - projects[component] = { - "project_id": project["id"], - "project_source": project.get("source") or "", - "path": package_path, - "release": release, - } - if not projects: - fail("Moon project graph does not contain any release-product projects") - return dict(sorted(projects.items())) - - -@lru_cache(maxsize=1) -def _product_paths_by_id() -> dict[str, str]: - moon_products = _moon_release_projects_by_component() - release_please_products = _release_please_packages_by_component() - moon_components = set(moon_products) - release_please_components = set(release_please_products) - if moon_components != release_please_components: - fail( - "Moon release-product components must match release-please components: " - f"moon={sorted(moon_components)}, release-please={sorted(release_please_components)}" - ) - paths: dict[str, str] = {} - for component, metadata in moon_products.items(): - package_path = metadata["path"] - release_please_path, package_config = release_please_products[component] - if release_please_path != package_path: - fail( - f"{component} Moon release.packagePath {package_path!r} must match " - f"release-please package path {release_please_path!r}" - ) - if package_config.get("component") != component: - fail(f"{package_path}.component must be {component!r}") - paths[component] = package_path - return paths - - def package_path(product: str) -> str: - paths = _product_paths_by_id() - value = paths.get(product) - if value is None: - fail(f"unknown release product {product!r}") + value = product_config(product).get("path") + if not isinstance(value, str) or not value: + fail(f"release graph product {product!r} must declare a package path") return value def moon_release_metadata(product: str) -> dict[str, Any]: - metadata = _moon_release_projects_by_component().get(product) - if metadata is None: + projects = load_graph().get("moon_projects") + project = projects.get(product) if isinstance(projects, dict) else None + if not isinstance(project, dict): fail(f"unknown Moon release component {product!r}") - release = metadata.get("release") + project_config = project.get("project") + metadata = project_config.get("metadata") if isinstance(project_config, dict) else None + release = metadata.get("release") if isinstance(metadata, dict) else None if not isinstance(release, dict): fail(f"Moon release component {product!r} has no release metadata") return release @@ -261,15 +169,7 @@ def _effective_release_metadata(product: str) -> dict[str, Any]: def load_graph() -> dict[str, Any]: """Compatibility return value for callers that still accept a graph arg.""" - return { - "policy": { - "repository": "f0rr0/oliphaunt", - "default_branch": "main", - "versioning": "independent", - }, - "products": graph_products(), - "artifact_targets": [], - } + return _release_graph() @dataclass(frozen=True) @@ -321,6 +221,17 @@ def _release_graph_query_rows(command: str, args: tuple[str, ...] = ()) -> tuple return tuple(rows) +@lru_cache(maxsize=1) +def _release_graph() -> dict[str, Any]: + value = _release_graph_query_json("graph") + if not isinstance(value, dict): + fail("release graph query must return a JSON object") + products = value.get("products") + if not isinstance(products, dict) or not products: + fail("release graph query must return a non-empty products object") + return value + + def _target_string(row: dict[str, Any], key: str, target_id: str, *, required: bool = True) -> str | None: value = row.get(key) if isinstance(value, str) and value: @@ -650,37 +561,35 @@ def typescript_optional_runtime_package_versions() -> dict[str, str]: def graph_products(graph: dict | None = None) -> dict[str, dict[str, Any]]: - products: dict[str, dict[str, Any]] = {} - manifest = _release_please_manifest() - for product, path in _product_paths_by_id().items(): - config = _effective_release_metadata(product) - package_config = _package_config(product) - config["path"] = path - config["tag_prefix"] = tag_prefix(product) - config["changelog_path"] = changelog_path(product) - config["version_files"] = version_files(product) - config.setdefault("derived_version_files", []) - if path not in manifest: - fail(f".release-please-manifest.json is missing {path}") - products[product] = config - return products + source = load_graph() if graph is None else graph + products = source.get("products") if isinstance(source, dict) else None + if not isinstance(products, dict) or not products: + fail("release graph must contain a non-empty products object") + parsed: dict[str, dict[str, Any]] = {} + for product, config in products.items(): + if not isinstance(product, str) or not product: + fail("release graph product ids must be non-empty strings") + if not isinstance(config, dict): + fail(f"release graph product {product} config must be an object") + parsed[product] = dict(config) + return parsed def product_config(product: str, graph: dict | None = None) -> dict[str, Any]: - config = graph_products().get(product) + config = graph_products(graph).get(product) if config is None: fail(f"unknown release product {product!r}") return config def product_ids(graph: dict | None = None) -> list[str]: - return list(graph_products()) + return list(graph_products(graph)) def extension_product_ids(graph: dict | None = None) -> list[str]: return sorted( product - for product, config in graph_products().items() + for product, config in graph_products(graph).items() if config.get("kind") == "exact-extension-artifact" ) From ea5d451e1acd2e10871321b381738c304378cf5b Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 04:29:41 +0000 Subject: [PATCH 149/308] test: guard react-native runtime feature rejection --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 10 ++++++++- .../react-native/src/__tests__/client.test.ts | 22 ++++++++++++++++++- tools/policy/check-sdk-parity.sh | 2 ++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 479355ca..90e644a7 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -52,7 +52,7 @@ until the current-state gates here are checked with fresh local evidence. 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 +- [x] Add or adjust machine checks for any invariant currently enforced only by convention or docs. - [x] Harden TypeScript Node/Bun/Deno runtime cache publication so package-managed runtime/tool/extension materialization publishes through a @@ -78,6 +78,14 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Added a React Native parity guard for unsupported shared + runtime-resource `runtimeFeatures`: `client.packageSizeReport()` now has a + unit test proving the platform SDK rejection is propagated after resource + config normalization, and `tools/policy/check-sdk-parity.sh` requires that + regression test alongside the existing Swift and Kotlin negative tests. Fresh + checks passed: `pnpm --dir src/sdks/react-native test` and + `pnpm --dir src/sdks/react-native typecheck`, and + `tools/policy/check-sdk-parity.sh`. - 2026-06-27: Reduced duplicate Python release graph modeling in `tools/release/product_metadata.py`. `load_graph()`, `graph_products()`, `product_config()`, product ids, extension product ids, `package_path()`, and diff --git a/src/sdks/react-native/src/__tests__/client.test.ts b/src/sdks/react-native/src/__tests__/client.test.ts index 034ae044..3dadf4a4 100644 --- a/src/sdks/react-native/src/__tests__/client.test.ts +++ b/src/sdks/react-native/src/__tests__/client.test.ts @@ -21,6 +21,7 @@ async function main(): Promise { await testSupportedModesExposePlatformRuntimeContract(); testOpenConfigTypeSurface(); await testPackageSizeReportDelegatesToNativeSdk(); + await testPackageSizeReportRejectsUnsupportedRuntimeFeaturesFromNativeSdk(); await testPackageSizeReportRejectsBlankResourceRootBeforeNativeCall(); await testProcessMemoryReportDelegatesToNativeSdk(); testJsiBinaryTransportFixturesAreModeled(); @@ -182,6 +183,20 @@ async function testPackageSizeReportDelegatesToNativeSdk(): Promise { }); } +async function testPackageSizeReportRejectsUnsupportedRuntimeFeaturesFromNativeSdk(): Promise { + const native = new MockNative({ + packageSizeReportError: new Error('unsupported runtime resource runtimeFeatures: jit'), + }); + const client = createOliphauntClient(native); + + await assert.rejects(async () => { + await client.packageSizeReport({ resourceRoot: '/tmp/oliphaunt-rn-resources' }); + }, /unsupported runtime resource runtimeFeatures: jit/); + assert.deepEqual(native.packageSizeReportCalls, [ + { resourceRoot: '/tmp/oliphaunt-rn-resources' }, + ]); +} + async function testPackageSizeReportRejectsBlankResourceRootBeforeNativeCall(): Promise { const native = new MockNative(); const client = createOliphauntClient(native); @@ -1340,8 +1355,10 @@ class MockNative implements Spec { }> = []; execCalls = 0; private nextHandle = 1; + private readonly packageSizeReportError: Error | null; - constructor(options: { installJsi?: boolean } = {}) { + constructor(options: { installJsi?: boolean; packageSizeReportError?: Error } = {}) { + this.packageSizeReportError = options.packageSizeReportError ?? null; if (options.installJsi !== false) { installMockJsiTransport(this); } @@ -1438,6 +1455,9 @@ class MockNative implements Spec { async packageSizeReport(config: unknown) { this.packageSizeReportCalls.push(config); + if (this.packageSizeReportError != null) { + throw this.packageSizeReportError; + } return { packageBytes: 185, runtimeBytes: 100, diff --git a/tools/policy/check-sdk-parity.sh b/tools/policy/check-sdk-parity.sh index 2a633d2b..2647e8f7 100755 --- a/tools/policy/check-sdk-parity.sh +++ b/tools/policy/check-sdk-parity.sh @@ -1415,6 +1415,8 @@ require_text src/sdks/react-native/src/client.ts "packageSizeReport" \ "React Native SDK must expose package-size report parsing" require_text src/sdks/react-native/src/__tests__/client.test.ts "testPackageSizeReportDelegatesToNativeSdk" \ "React Native SDK tests must prove package-size report delegation" +require_text src/sdks/react-native/src/__tests__/client.test.ts "testPackageSizeReportRejectsUnsupportedRuntimeFeaturesFromNativeSdk" \ + "React Native SDK tests must prove native runtimeFeatures rejection propagates" require_text src/sdks/react-native/android/src/main/java/dev/oliphaunt/reactnative/OliphauntModule.kt "OliphauntAndroid.packageSizeReport" \ "React Native Android must delegate package-size reports to the Kotlin SDK" require_text src/sdks/react-native/ios/OliphauntAdapter.swift "packageSizeReportWithConfig" \ From 17d523d03bfbdeaf73a3a3a826b3990b4f41ec1b Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 04:33:22 +0000 Subject: [PATCH 150/308] chore: expose python tooling inventory --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 17 ++++++++++-- tools/policy/check-python-entrypoints.mjs | 26 ++++++++++++++++++- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 90e644a7..fd5ae8bf 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,18 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Tightened the Python tooling inventory audit. + `tools/policy/check-python-entrypoints.mjs` now rejects unknown flags and + makes `--list` print the validated tracked Python entrypoints instead of only + a count, giving the remaining migration pass concrete file-level evidence for + the current 9 intentional Python scripts. Fresh checks passed: + `tools/dev/bun.sh + tools/policy/check-python-entrypoints.mjs`, `tools/dev/bun.sh + tools/policy/check-python-entrypoints.mjs --list`, `tools/dev/bun.sh + tools/policy/check-python-entrypoints.mjs --help`, and an unknown-flag + negative smoke. Follow-up policy checks passed: `bash + tools/policy/check-tooling-stack.sh` and `bash + tools/policy/check-policy-tools.sh`. - 2026-06-27: Added a React Native parity guard for unsupported shared runtime-resource `runtimeFeatures`: `client.packageSizeReport()` now has a unit test proving the platform SDK rejection is propagated after resource @@ -941,8 +953,9 @@ until the current-state gates here are checked with fresh local evidence. - 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, and runtime lock helpers. New + The current inventory contains release orchestration/package validators, + product metadata adapters, the WASIX Cargo artifact packager, local registry + publishing, release policy checks, and the extension model generator. 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. diff --git a/tools/policy/check-python-entrypoints.mjs b/tools/policy/check-python-entrypoints.mjs index 5bd6488e..8c670a66 100644 --- a/tools/policy/check-python-entrypoints.mjs +++ b/tools/policy/check-python-entrypoints.mjs @@ -4,12 +4,29 @@ import { readFileSync } from "node:fs"; const ALLOWLIST = "tools/policy/python-entrypoints.allowlist"; const PYTHON_PATHSPEC = ":(glob)**/*.py"; +const args = process.argv.slice(2); function fail(message) { console.error(`check-python-entrypoints.mjs: ${message}`); process.exit(1); } +function usage() { + console.log("usage: tools/policy/check-python-entrypoints.mjs [--list]"); +} + +let list = false; +for (const arg of args) { + if (arg === "--list") { + list = true; + } else if (arg === "--help" || arg === "-h") { + usage(); + process.exit(0); + } else { + fail(`unknown argument: ${arg}`); + } +} + function gitLsFiles(pathspec) { const result = spawnSync("git", ["ls-files", "-z", "--", pathspec], { encoding: "buffer", @@ -78,4 +95,11 @@ if (missing.length > 0 || stale.length > 0) { fail("update the inventory or port the Python file to Bun"); } -console.log(`Python entrypoint inventory verified (${trackedPython.length} tracked files).`); +if (list) { + console.log(`Python entrypoint inventory verified (${trackedPython.length} tracked files):`); + for (const path of trackedPython) { + console.log(` ${path}`); + } +} else { + console.log(`Python entrypoint inventory verified (${trackedPython.length} tracked files).`); +} From 409ae4dc1e2ae7375aaa255065e15f0b849cc1d4 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 04:37:47 +0000 Subject: [PATCH 151/308] test: guard wasix split tools feature surface --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 14 ++++++ .../wasix-rust/tools/check-package.sh | 47 +++++++++++++++++++ tools/policy/check-sdk-parity.sh | 4 ++ 3 files changed, 65 insertions(+) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index fd5ae8bf..b203d620 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,20 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Tightened WASIX Rust split-tools SDK parity. The WASIX package + check now requires the `tools` feature to select the split + `oliphaunt-wasix-tools` crate plus all tools-AOT target crates, and requires + the public `pg_dump`/`psql` module and crate-root exports to stay behind + `#[cfg(feature = "tools")]`. `tools/policy/check-sdk-parity.sh` now requires + those package-shape assertions, matching the documented rule that WASIX + `pg_dump` and `psql` exist only when the split tools feature is selected. + Fresh checks passed: `bash src/bindings/wasix-rust/tools/check-package.sh` + and `tools/policy/check-sdk-parity.sh`. Follow-up checks passed: + `python3 tools/release/check_release_metadata.py`, focused `python3 + tools/release/check_consumer_shape.py --products-json + '["oliphaunt-wasix-rust"]'`, `cargo check -p oliphaunt-wasix --locked + --no-default-features --lib`, `bash tools/policy/check-policy-tools.sh`, and + `bash tools/policy/check-docs.sh`. - 2026-06-27: Tightened the Python tooling inventory audit. `tools/policy/check-python-entrypoints.mjs` now rejects unknown flags and makes `--list` print the validated tracked Python entrypoints instead of only diff --git a/src/bindings/wasix-rust/tools/check-package.sh b/src/bindings/wasix-rust/tools/check-package.sh index adc2d27f..f7f86f27 100755 --- a/src/bindings/wasix-rust/tools/check-package.sh +++ b/src/bindings/wasix-rust/tools/check-package.sh @@ -30,6 +30,36 @@ reject_pattern() { fi } +require_source_text() { + local file="$1" + local text="$2" + local message="$3" + if ! grep -Fq "$text" "$file"; then + echo "$message" >&2 + exit 1 + fi +} + +require_cfg_tools_line() { + local file="$1" + local line="$2" + local message="$3" + if ! awk -v expected="$line" ' + previous == "#[cfg(feature = \"tools\")]" && $0 == expected { + found = 1 + } + { + previous = $0 + } + END { + exit found ? 0 : 1 + } + ' "$file"; then + echo "$message" >&2 + exit 1 + fi +} + require_entry "Cargo.toml" require_entry "README.md" require_entry "src/lib.rs" @@ -76,4 +106,21 @@ if ! awk ' exit 1 fi +require_source_text src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml '"dep:oliphaunt-wasix-tools",' \ + "oliphaunt-wasix tools feature must select the split oliphaunt-wasix-tools crate" +require_source_text src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml '"dep:oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu",' \ + "oliphaunt-wasix tools feature must select the Linux x64 tools-AOT crate" +require_source_text src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml '"dep:oliphaunt-wasix-tools-aot-aarch64-unknown-linux-gnu",' \ + "oliphaunt-wasix tools feature must select the Linux arm64 tools-AOT crate" +require_source_text src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml '"dep:oliphaunt-wasix-tools-aot-aarch64-apple-darwin",' \ + "oliphaunt-wasix tools feature must select the macOS arm64 tools-AOT crate" +require_source_text src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml '"dep:oliphaunt-wasix-tools-aot-x86_64-pc-windows-msvc",' \ + "oliphaunt-wasix tools feature must select the Windows x64 tools-AOT crate" +require_cfg_tools_line src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/mod.rs "pub mod pg_dump;" \ + "WASIX split-tools public module must stay behind cfg(feature = \"tools\")" +require_cfg_tools_line src/bindings/wasix-rust/crates/oliphaunt-wasix/src/oliphaunt/mod.rs "pub use pg_dump::{PgDumpOptions, PsqlOptions, preflight_wasix_tools};" \ + "WASIX split-tools internal exports must stay behind cfg(feature = \"tools\")" +require_cfg_tools_line src/bindings/wasix-rust/crates/oliphaunt-wasix/src/lib.rs "pub use oliphaunt::{PgDumpOptions, PsqlOptions, preflight_wasix_tools};" \ + "WASIX split-tools crate-root exports must stay behind cfg(feature = \"tools\")" + echo "oliphaunt-wasix package shape verified: $listing" diff --git a/tools/policy/check-sdk-parity.sh b/tools/policy/check-sdk-parity.sh index 2647e8f7..cf3a874b 100755 --- a/tools/policy/check-sdk-parity.sh +++ b/tools/policy/check-sdk-parity.sh @@ -128,6 +128,10 @@ 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_text src/bindings/wasix-rust/tools/check-package.sh "WASIX split-tools public module must stay behind cfg" \ + "WASIX package check must keep public pg_dump/psql APIs behind the tools feature" +require_text src/bindings/wasix-rust/tools/check-package.sh "oliphaunt-wasix tools feature must select the split oliphaunt-wasix-tools crate" \ + "WASIX package check must require the tools feature to select split tools payload crates" 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"' \ From 80f2ab7512f8b4ca1e3d898c1041e1666249b7e9 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 04:40:23 +0000 Subject: [PATCH 152/308] chore: remove retired perf matrix wrapper --- docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 11 +++++++++++ tools/perf/matrix/run_bench_matrix.sh | 12 ------------ tools/policy/check-repo-structure.sh | 4 +--- tools/policy/check-test-strategy.mjs | 5 +---- 4 files changed, 13 insertions(+), 19 deletions(-) delete mode 100755 tools/perf/matrix/run_bench_matrix.sh diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index b203d620..2018ff54 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,17 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Removed confirmed dead perf tooling entrypoint + `tools/perf/matrix/run_bench_matrix.sh`. Repository grep showed no active + docs, CI, Moon, source, or example caller outside policy checks, and the file + itself only printed a retired-compatibility warning before delegating to + `tools/perf/matrix/run_native_oliphaunt_matrix.sh`. Repo-structure policy now + rejects tracking that retired wrapper again, while the peer SDK test-strategy + check keeps guarding the current performance docs against old benchmark + labels. Fresh checks passed: `bash tools/policy/check-repo-structure.sh`, + `tools/policy/check-test-strategy.mjs`, `bash + tools/policy/check-policy-tools.sh`, `bash tools/policy/check-docs.sh`, a + stale-reference `git grep`, and `git diff --check`. - 2026-06-27: Tightened WASIX Rust split-tools SDK parity. The WASIX package check now requires the `tools` feature to select the split `oliphaunt-wasix-tools` crate plus all tools-AOT target crates, and requires diff --git a/tools/perf/matrix/run_bench_matrix.sh b/tools/perf/matrix/run_bench_matrix.sh deleted file mode 100755 index 4e991638..00000000 --- a/tools/perf/matrix/run_bench_matrix.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -script_dir="$(cd "$(dirname "$0")" && pwd)" - -cat >&2 <<'MSG' -tools/perf/matrix/run_bench_matrix.sh is a retired compatibility entrypoint. -Use tools/perf/matrix/run_native_oliphaunt_matrix.sh for native direct, -broker, server, PostgreSQL, SQLite, and WASIX comparison plans. -MSG - -exec "$script_dir/run_native_oliphaunt_matrix.sh" "$@" diff --git a/tools/policy/check-repo-structure.sh b/tools/policy/check-repo-structure.sh index 9482895b..9c6b1d6a 100755 --- a/tools/policy/check-repo-structure.sh +++ b/tools/policy/check-repo-structure.sh @@ -590,9 +590,7 @@ require_file benchmarks/wasix/README.md require_file benchmarks/mobile/README.md require_file benchmarks/reports/README.md reject_tracked_under tools/perf/fixtures -reject_text tools/perf/matrix/run_bench_matrix.sh 'node-bench' -reject_text tools/perf/matrix/run_bench_matrix.sh 'bench-oxide' -reject_text tools/perf/matrix/run_bench_matrix.sh 'nodefs' +reject_tracked_under tools/perf/matrix/run_bench_matrix.sh require_text docs/maintainers/tooling.md 'tools/xtask/src/template_runner.rs' require_text docs/maintainers/tooling.md 'tools/xtask/src/asset_checks.rs' require_text docs/maintainers/tooling.md 'tools/xtask/src/asset_manifest.rs' diff --git a/tools/policy/check-test-strategy.mjs b/tools/policy/check-test-strategy.mjs index fc1c1c6c..7f9f1a05 100755 --- a/tools/policy/check-test-strategy.mjs +++ b/tools/policy/check-test-strategy.mjs @@ -615,10 +615,7 @@ for (const file of [ requireText(file, 'supportedModes'); } -for (const file of [ - 'tools/perf/matrix/run_bench_matrix.sh', - 'src/docs/content/reference/performance.mdx', -]) { +for (const file of ['src/docs/content/reference/performance.mdx']) { rejectText(file, 'node-bench'); rejectText(file, 'bench-oxide'); rejectText(file, 'nodefs'); From a32d0a71584838e2e8a11d350d051c811c799217 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 04:44:16 +0000 Subject: [PATCH 153/308] chore: inventory rust helper crates --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 28 +++- tools/policy/check-rust-helper-crates.mjs | 121 ++++++++++++++++++ tools/policy/check-tooling-stack.sh | 3 + tools/policy/rust-helper-crates.allowlist | 4 + 4 files changed, 149 insertions(+), 7 deletions(-) create mode 100644 tools/policy/check-rust-helper-crates.mjs create mode 100644 tools/policy/rust-helper-crates.allowlist diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 2018ff54..75d9d3d8 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: Added an explicit Rust helper crate inventory. The new + `tools/policy/check-rust-helper-crates.mjs` policy check verifies that the + only tracked Rust helper crates under `tools/` are `tools/perf/runner` and + `tools/xtask`, rejects stale or unlisted helper crates, and requires each to + remain unpublished with empty default features so routine policy checks do not + compile optional runtime-heavy paths. `check-tooling-stack.sh` now runs the + inventory beside the Python entrypoint inventory. Fresh checks passed: + `tools/dev/bun.sh tools/policy/check-rust-helper-crates.mjs`, + `tools/dev/bun.sh tools/policy/check-rust-helper-crates.mjs --list`, + `tools/dev/bun.sh tools/policy/check-rust-helper-crates.mjs --help`, an + unknown-flag negative smoke, `bash tools/policy/check-tooling-stack.sh`, + `bash tools/policy/check-policy-tools.sh`, `bash + tools/policy/check-repo-structure.sh`, and `bash tools/policy/check-docs.sh`. - 2026-06-27: Removed confirmed dead perf tooling entrypoint `tools/perf/matrix/run_bench_matrix.sh`. Repository grep showed no active docs, CI, Moon, source, or example caller outside policy checks, and the file @@ -1049,13 +1062,14 @@ until the current-state gates here are checked with fresh local evidence. `check-tooling-stack.sh`, `check-repo-structure.sh`, `check_artifact_targets.py`, and `check-release-policy.py`; the intentional Python inventory contained 32 tracked files at that point. -- 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. +- Rust helper inventory is machine-checked by + `tools/policy/check-rust-helper-crates.mjs` and 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 diff --git a/tools/policy/check-rust-helper-crates.mjs b/tools/policy/check-rust-helper-crates.mjs new file mode 100644 index 00000000..34a7074e --- /dev/null +++ b/tools/policy/check-rust-helper-crates.mjs @@ -0,0 +1,121 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; +import { readFileSync } from "node:fs"; + +const ALLOWLIST = "tools/policy/rust-helper-crates.allowlist"; +const RUST_HELPER_PATHSPEC = ":(glob)tools/**/Cargo.toml"; +const args = process.argv.slice(2); + +function fail(message) { + console.error(`check-rust-helper-crates.mjs: ${message}`); + process.exit(1); +} + +function usage() { + console.log("usage: tools/policy/check-rust-helper-crates.mjs [--list]"); +} + +let list = false; +for (const arg of args) { + if (arg === "--list") { + list = true; + } else if (arg === "--help" || arg === "-h") { + usage(); + process.exit(0); + } else { + fail(`unknown argument: ${arg}`); + } +} + +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("/Cargo.toml")) { + fail(`${ALLOWLIST}:${index + 1} is not a repo-relative Cargo.toml path: ${line}`); + } + if (!line.startsWith("tools/")) { + fail(`${ALLOWLIST}:${index + 1} must stay under tools/: ${line}`); + } + entries.push(line); + } + return entries; +} + +function assertSortedUnique(entries) { + const sorted = [...entries].sort(); + if (entries.join("\n") !== sorted.join("\n")) { + 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]}`); + } + } +} + +function assertHelperCratePolicy(path) { + const text = readFileSync(path, "utf8"); + if (!text.includes("publish = false")) { + fail(`${path} must be unpublished internal tooling`); + } + if (!text.includes("default = []")) { + fail(`${path} must keep default features empty so policy checks do not compile optional runtime-heavy paths`); + } +} + +const trackedRustHelpers = gitLsFiles(RUST_HELPER_PATHSPEC); +const allowlistedRustHelpers = parseAllowlist(); +assertSortedUnique(allowlistedRustHelpers); + +const tracked = new Set(trackedRustHelpers); +const allowed = new Set(allowlistedRustHelpers); +const missing = trackedRustHelpers.filter((path) => !allowed.has(path)); +const stale = allowlistedRustHelpers.filter((path) => !tracked.has(path)); + +if (missing.length > 0 || stale.length > 0) { + if (missing.length > 0) { + console.error("tracked Rust helper crates missing from the intentional inventory:"); + for (const path of missing) { + console.error(` ${path}`); + } + } + if (stale.length > 0) { + console.error("stale Rust helper inventory entries:"); + for (const path of stale) { + console.error(` ${path}`); + } + } + fail("update the inventory or move the helper to Bun"); +} + +for (const path of trackedRustHelpers) { + assertHelperCratePolicy(path); +} + +if (list) { + console.log(`Rust helper crate inventory verified (${trackedRustHelpers.length} tracked crates):`); + for (const path of trackedRustHelpers) { + console.log(` ${path}`); + } +} else { + console.log(`Rust helper crate inventory verified (${trackedRustHelpers.length} tracked crates).`); +} diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index bd0fe927..4c867572 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -40,8 +40,10 @@ 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-rust-helper-crates.mjs require_file tools/policy/check-native-boundaries.mjs require_file tools/policy/python-entrypoints.allowlist +require_file tools/policy/rust-helper-crates.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 @@ -255,6 +257,7 @@ 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 +bun tools/policy/check-rust-helper-crates.mjs 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 diff --git a/tools/policy/rust-helper-crates.allowlist b/tools/policy/rust-helper-crates.allowlist new file mode 100644 index 00000000..af4ae21c --- /dev/null +++ b/tools/policy/rust-helper-crates.allowlist @@ -0,0 +1,4 @@ +# Intentional Rust helper crate inventory. +# New Rust helper crates under tools/ should stay product/runtime-critical or move to Bun. +tools/perf/runner/Cargo.toml +tools/xtask/Cargo.toml From b1f4127d5a9d16b4848a57689777f8087a9d7da9 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 04:55:49 +0000 Subject: [PATCH 154/308] test: refresh wasix example registry locks --- examples/electron-wasix/src-wasix/Cargo.lock | 22 +++++++++---------- examples/tauri-wasix/src-tauri/Cargo.lock | 22 +++++++++---------- .../tauri-sqlx-vanilla/src-tauri/Cargo.lock | 22 +++++++++---------- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/examples/electron-wasix/src-wasix/Cargo.lock b/examples/electron-wasix/src-wasix/Cargo.lock index f0f6f5f7..b2440035 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 = "19b4cb312b8aad0c3632a151c41c5a7efc482a2d022a772bb06607306aa49e5c" +checksum = "f7c773796df578853baca2f0dcfb610dc78c103f17fbd260f053c5945a5d0ba1" 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 = "603fd79b3d921540314b0a2ff2c99b3f7cea3ad00c51835b1b4c8e5a649e6256" +checksum = "9611d8528c54f4a6981217d6acaddaba0b26cbc20841b8698cb14332fd1b8a64" 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 = "025c4b99a90255fe6ab91bbcd52f7f88178c98ef2fc13ddbeb69a9963f997a25" +checksum = "43067bd9d8aa2499d867443a39dcba33195f83c525193a730b6e9b7d66570f88" 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 = "21d775229d615bbc33473d2db9a99d4507801295632c245f369e4b8228c8db10" +checksum = "8856bae97b2d60f323f5847db4223fe768a0ee34ebb785b795b11482bd1a9b86" 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 = "02daa854eeb9f42d4a153a0915ff20f02972f13e9e9677ee4ec9ab2d82f35207" +checksum = "a813fb560bf766f17233f41ae60abd7463dd6a13b019792b614550c64be77e29" 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 = "64d462e41e6db08ef2ac2ff1d12af03be8f129316446131a2436aedf72aa1452" +checksum = "36fd320f5f132639038848bf307d10dbdbf4b6b47ecd794d0d3ff7674e2ae3d6" 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 = "8d650462930a132844428188fa1d12526dd2484e30ce1656b9723d5cc7d771b8" +checksum = "3a767b3afef41b9d6692c74870df7739aeb208bf3078a92a116afb4558872b4d" 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 = "f8a06f357b991187874a05817226c8179fab48f6e2c26ff5d0d2f6f7f5eef3a1" +checksum = "5129bc72a7419128b828189dc54a3a5a82eafc1754b08e8b0316528fcdbfea3b" 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 = "c94c962fff8482b62033972d0226f999d18bfab1a951dfe3b3e9845665fbe232" +checksum = "00ababb85de5d0fde8235e1f833726944cb4b1ff948de487166759e9d9784390" 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 = "8efd73a996aabcef6fe30cd22df3148cffc6da6b5a5d74c7ffff0c0c09519e75" +checksum = "f0efc748599c21e28a1900dc055847dbdb65f79948159fb1333229713a4b1bf5" 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 = "161a20f9ab843569e3bd9c963a7f8d6f9f8283d70cc4f65ddf7fc516c8e04a31" +checksum = "608a00fadaa05b4e1d714024d1ef77d6ce536f1f547cc1dc37ed686bdf1f2340" 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 f6425ecc..739ffce6 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 = "19b4cb312b8aad0c3632a151c41c5a7efc482a2d022a772bb06607306aa49e5c" +checksum = "f7c773796df578853baca2f0dcfb610dc78c103f17fbd260f053c5945a5d0ba1" 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 = "603fd79b3d921540314b0a2ff2c99b3f7cea3ad00c51835b1b4c8e5a649e6256" +checksum = "9611d8528c54f4a6981217d6acaddaba0b26cbc20841b8698cb14332fd1b8a64" 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 = "025c4b99a90255fe6ab91bbcd52f7f88178c98ef2fc13ddbeb69a9963f997a25" +checksum = "43067bd9d8aa2499d867443a39dcba33195f83c525193a730b6e9b7d66570f88" 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 = "21d775229d615bbc33473d2db9a99d4507801295632c245f369e4b8228c8db10" +checksum = "8856bae97b2d60f323f5847db4223fe768a0ee34ebb785b795b11482bd1a9b86" 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 = "02daa854eeb9f42d4a153a0915ff20f02972f13e9e9677ee4ec9ab2d82f35207" +checksum = "a813fb560bf766f17233f41ae60abd7463dd6a13b019792b614550c64be77e29" 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 = "64d462e41e6db08ef2ac2ff1d12af03be8f129316446131a2436aedf72aa1452" +checksum = "36fd320f5f132639038848bf307d10dbdbf4b6b47ecd794d0d3ff7674e2ae3d6" 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 = "8d650462930a132844428188fa1d12526dd2484e30ce1656b9723d5cc7d771b8" +checksum = "3a767b3afef41b9d6692c74870df7739aeb208bf3078a92a116afb4558872b4d" 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 = "f8a06f357b991187874a05817226c8179fab48f6e2c26ff5d0d2f6f7f5eef3a1" +checksum = "5129bc72a7419128b828189dc54a3a5a82eafc1754b08e8b0316528fcdbfea3b" 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 = "c94c962fff8482b62033972d0226f999d18bfab1a951dfe3b3e9845665fbe232" +checksum = "00ababb85de5d0fde8235e1f833726944cb4b1ff948de487166759e9d9784390" 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 = "8efd73a996aabcef6fe30cd22df3148cffc6da6b5a5d74c7ffff0c0c09519e75" +checksum = "f0efc748599c21e28a1900dc055847dbdb65f79948159fb1333229713a4b1bf5" 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 = "161a20f9ab843569e3bd9c963a7f8d6f9f8283d70cc4f65ddf7fc516c8e04a31" +checksum = "608a00fadaa05b4e1d714024d1ef77d6ce536f1f547cc1dc37ed686bdf1f2340" dependencies = [ "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 a724a595..fca68186 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 = "19b4cb312b8aad0c3632a151c41c5a7efc482a2d022a772bb06607306aa49e5c" +checksum = "f7c773796df578853baca2f0dcfb610dc78c103f17fbd260f053c5945a5d0ba1" 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 = "603fd79b3d921540314b0a2ff2c99b3f7cea3ad00c51835b1b4c8e5a649e6256" +checksum = "9611d8528c54f4a6981217d6acaddaba0b26cbc20841b8698cb14332fd1b8a64" 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 = "025c4b99a90255fe6ab91bbcd52f7f88178c98ef2fc13ddbeb69a9963f997a25" +checksum = "43067bd9d8aa2499d867443a39dcba33195f83c525193a730b6e9b7d66570f88" 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 = "21d775229d615bbc33473d2db9a99d4507801295632c245f369e4b8228c8db10" +checksum = "8856bae97b2d60f323f5847db4223fe768a0ee34ebb785b795b11482bd1a9b86" 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 = "02daa854eeb9f42d4a153a0915ff20f02972f13e9e9677ee4ec9ab2d82f35207" +checksum = "a813fb560bf766f17233f41ae60abd7463dd6a13b019792b614550c64be77e29" 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 = "64d462e41e6db08ef2ac2ff1d12af03be8f129316446131a2436aedf72aa1452" +checksum = "36fd320f5f132639038848bf307d10dbdbf4b6b47ecd794d0d3ff7674e2ae3d6" 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 = "8d650462930a132844428188fa1d12526dd2484e30ce1656b9723d5cc7d771b8" +checksum = "3a767b3afef41b9d6692c74870df7739aeb208bf3078a92a116afb4558872b4d" 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 = "f8a06f357b991187874a05817226c8179fab48f6e2c26ff5d0d2f6f7f5eef3a1" +checksum = "5129bc72a7419128b828189dc54a3a5a82eafc1754b08e8b0316528fcdbfea3b" 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 = "c94c962fff8482b62033972d0226f999d18bfab1a951dfe3b3e9845665fbe232" +checksum = "00ababb85de5d0fde8235e1f833726944cb4b1ff948de487166759e9d9784390" 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 = "8efd73a996aabcef6fe30cd22df3148cffc6da6b5a5d74c7ffff0c0c09519e75" +checksum = "f0efc748599c21e28a1900dc055847dbdb65f79948159fb1333229713a4b1bf5" 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 = "161a20f9ab843569e3bd9c963a7f8d6f9f8283d70cc4f65ddf7fc516c8e04a31" +checksum = "608a00fadaa05b4e1d714024d1ef77d6ce536f1f547cc1dc37ed686bdf1f2340" dependencies = [ "serde_json", "sha2 0.10.9", From 35c1dea4ed1ca9888a645f9ac6b26d2018ab1a9a Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 05:07:38 +0000 Subject: [PATCH 155/308] chore: port example policy check to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 11 + examples/moon.yml | 3 +- examples/tools/check-examples.mjs | 220 ++++++++++++++++++ examples/tools/check-examples.sh | 151 +----------- moon.yml | 2 +- 5 files changed, 235 insertions(+), 152 deletions(-) create mode 100644 examples/tools/check-examples.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 75d9d3d8..b01a1c57 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,17 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Moved the cross-product example ownership/local-registry policy + checker from shell logic into `examples/tools/check-examples.mjs` so the + canonical Moon tasks run through the pinned Bun launcher. The old + `examples/tools/check-examples.sh` path remains a thin compatibility + launcher. Fresh checks passed: `tools/dev/bun.sh + examples/tools/check-examples.mjs`, `bash examples/tools/check-examples.sh`, + `$HOME/.proto/shims/moon run integration-examples:check`, + `tools/policy/check-moon-product-graph.mjs`, `bash + tools/policy/check-tooling-stack.sh`, `bash + tools/policy/check-policy-tools.sh`, `bash + tools/policy/check-repo-structure.sh`, and `git diff --check`. - 2026-06-27: Added an explicit Rust helper crate inventory. The new `tools/policy/check-rust-helper-crates.mjs` policy check verifies that the only tracked Rust helper crates under `tools/` are `tools/perf/runner` and diff --git a/examples/moon.yml b/examples/moon.yml index 042c181e..5a6d3ba5 100644 --- a/examples/moon.yml +++ b/examples/moon.yml @@ -19,7 +19,7 @@ owners: tasks: check: tags: ["quality", "static"] - command: "bash examples/tools/check-examples.sh" + command: "bash tools/dev/bun.sh examples/tools/check-examples.mjs" inputs: - "/examples/**/*" - "/src/sdks/react-native/examples/**/*" @@ -31,6 +31,7 @@ tasks: - "/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" + - "/examples/tools/check-examples.mjs" - "/examples/tools/check-examples.sh" options: cache: true diff --git a/examples/tools/check-examples.mjs b/examples/tools/check-examples.mjs new file mode 100644 index 00000000..ca2920b0 --- /dev/null +++ b/examples/tools/check-examples.mjs @@ -0,0 +1,220 @@ +#!/usr/bin/env bun +import { existsSync, readFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; + +let ROOT = process.cwd(); + +function fail(message) { + console.error(message); + process.exit(1); +} + +function run(command, args) { + console.log(`\n==> ${[command, ...args].join(" ")}`); + const result = spawnSync(command, args, { + cwd: ROOT, + stdio: "inherit", + }); + if (result.error) { + fail(result.error.message); + } + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + +function output(command, args) { + const result = spawnSync(command, args, { + cwd: ROOT, + encoding: "utf8", + }); + if (result.error) { + fail(result.error.message); + } + if (result.status !== 0) { + fail(result.stderr.trim() || `${command} ${args.join(" ")} failed`); + } + return result.stdout; +} + +function gitLsFiles(...pathspecs) { + const args = ["ls-files", "-z"]; + if (pathspecs.length > 0) { + args.push("--", ...pathspecs); + } + return output("git", args) + .split("\0") + .filter(Boolean); +} + +function requireFile(path) { + if (!existsSync(path)) { + fail(`missing required product-local example file: ${path}`); + } +} + +function requireText(path, pattern) { + const text = readFileSync(path, "utf8"); + if (!new RegExp(pattern, "m").test(text)) { + fail(`missing required example scheduling pattern in ${path}: ${pattern}`); + } +} + +function requireWasixToolsSmoke(path) { + requireText(path, String.raw`preflight_tools\(\)`); + requireText(path, "dump_sql"); + requireText(path, String.raw`psql\(|PsqlOptions::new\(\)`); +} + +function rejectText(path, pattern) { + const text = readFileSync(path, "utf8"); + if (new RegExp(pattern, "m").test(text)) { + fail(`forbidden example local dependency pattern in ${path}: ${pattern}`); + } +} + +function rejectFile(path) { + if (existsSync(path)) { + fail(`forbidden stale example file: ${path}`); + } +} + +ROOT = output("git", ["rev-parse", "--show-toplevel"]).trim(); +if (ROOT.length === 0) { + fail("must run inside the Oliphaunt git checkout"); +} +process.chdir(ROOT); + +run("bash", ["examples/tools/check-lockfiles.sh", "--check"]); + +const allowedRootExamples = + /^(examples\/moon\.yml|examples\/README\.md|examples\/tools\/[^/]+|examples\/(tauri|tauri-wasix|electron|electron-wasix)(\/.*)?)$/; +const violations = gitLsFiles("examples").filter((path) => !allowedRootExamples.test(path)); +if (violations.length > 0) { + console.error("root examples/ may contain only cross-product example policy/tooling"); + console.error(violations.join("\n")); + process.exit(1); +} + +const trackedNodeModules = gitLsFiles( + "examples/**/node_modules/**", + "src/**/examples/**/node_modules/**", +); +if (trackedNodeModules.length > 0) { + console.error("example dependencies must not be tracked"); + console.error(trackedNodeModules.join("\n")); + process.exit(1); +} + +requireFile("src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/package.json"); +requireFile("src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml"); +requireText("src/bindings/wasix-rust/moon.yml", String.raw`^ example-check:$`); +requireText("src/bindings/wasix-rust/moon.yml", String.raw`tags: \["examples", "quality", "ci-wasm-regression"\]`); +requireText( + "src/bindings/wasix-rust/tools/check-examples.sh", + String.raw`examples/tools/with-local-registries\.sh bash "\$0"`, +); +requireText("src/bindings/wasix-rust/tools/check-examples.sh", "PNPM_CONFIG_LOCKFILE"); + +requireFile("examples/tools/with-local-registries.sh"); +requireText("examples/tools/with-local-registries.sh", String.raw`export CARGO_HOME="\$cargo_home"`); +requireFile("examples/tools/run-tauri-webdriver-smoke.sh"); +requireFile("examples/tools/tauri-webdriver-smoke.mjs"); +requireFile("examples/tools/run-electron-driver-smoke.sh"); +requireFile("examples/tools/electron-driver-smoke.mjs"); +requireFile("examples/tools/electron-test-driver.mjs"); +requireText("examples/tools/run-tauri-webdriver-smoke.sh", String.raw`cargo install tauri-driver --locked --version 2\.0\.6`); +requireText( + "examples/tools/run-tauri-webdriver-smoke.sh", + String.raw`pnpm --filter "\./\$app_dir" install --no-frozen-lockfile`, +); +requireText( + "examples/tools/run-electron-driver-smoke.sh", + String.raw`pnpm --filter "\./\$app_dir" install --no-frozen-lockfile`, +); +requireText( + "examples/tools/run-electron-driver-smoke.sh", + String.raw`assert_npm_package "@oliphaunt/tools-linux-x64-gnu" "0\.1\.0"`, +); +requireText("examples/tools/tauri-webdriver-smoke.mjs", "tauri webdriver todo smoke passed"); +requireText("examples/tools/electron-driver-smoke.mjs", "electron driver todo smoke passed"); +requireText("examples/tools/electron-test-driver.mjs", "installElectronTodoTestDriver"); +for (const example of ["tauri", "tauri-wasix", "electron", "electron-wasix"]) { + requireFile(`examples/${example}/package.json`); + requireFile(`examples/${example}/README.md`); + requireFile(`examples/${example}/.npmrc`); + requireText(`examples/${example}/.npmrc`, String.raw`^registry=http://127\.0\.0\.1:4873/$`); + requireText(`examples/${example}/.npmrc`, String.raw`^link-workspace-packages=false$`); + requireText(`examples/${example}/.npmrc`, String.raw`^prefer-workspace-packages=false$`); +} +requireFile("examples/tauri/src-tauri/Cargo.toml"); +requireFile("examples/tauri-wasix/src-tauri/Cargo.toml"); +requireFile("examples/electron-wasix/src-wasix/Cargo.toml"); +requireText("examples/electron/package.json", String.raw`"@oliphaunt/ts": "0\.1\.0"`); +requireText("examples/electron/package.json", String.raw`"@oliphaunt/extension-hstore": "0\.1\.0"`); +requireText("examples/electron/package.json", String.raw`"@oliphaunt/extension-pg-trgm": "0\.1\.0"`); +requireText("examples/electron/package.json", String.raw`"@oliphaunt/extension-unaccent": "0\.1\.0"`); +requireText("examples/electron/package.json", String.raw`"pg": "\^8\.16\.3"`); +rejectFile("examples/electron/src/oliphaunt-kysely.ts"); +requireText("examples/tauri/src-tauri/Cargo.toml", 'registry = "oliphaunt-local"'); +requireText("examples/tauri/src-tauri/Cargo.toml", "oliphaunt-tools ="); +requireText("examples/tauri/src-tauri/Cargo.toml", "oliphaunt-extension-hstore-linux-x64-gnu"); +requireText("examples/tauri/src-tauri/Cargo.toml", "oliphaunt-extension-pg-trgm-linux-x64-gnu"); +requireText("examples/tauri/src-tauri/Cargo.toml", "oliphaunt-extension-unaccent-linux-x64-gnu"); +requireText("examples/tauri-wasix/src-tauri/Cargo.toml", 'registry = "oliphaunt-local"'); +requireText("examples/tauri-wasix/src-tauri/Cargo.toml", '"tools"'); +requireText("examples/tauri-wasix/src-tauri/Cargo.toml", "oliphaunt-wasix-tools"); +requireText("examples/tauri-wasix/src-tauri/Cargo.toml", "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu"); +requireText("examples/tauri-wasix/src-tauri/Cargo.toml", "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu"); +requireText("examples/tauri-wasix/src-tauri/Cargo.lock", "oliphaunt-extension-hstore-wasix-aot-x86_64-unknown-linux-gnu"); +requireText("examples/tauri-wasix/src-tauri/Cargo.lock", "oliphaunt-extension-pg-trgm-wasix-aot-x86_64-unknown-linux-gnu"); +requireText("examples/tauri-wasix/src-tauri/Cargo.lock", "oliphaunt-extension-unaccent-wasix-aot-x86_64-unknown-linux-gnu"); +requireWasixToolsSmoke("examples/tauri-wasix/src-tauri/src/lib.rs"); +requireText("examples/electron-wasix/src-wasix/Cargo.toml", 'registry = "oliphaunt-local"'); +requireText("examples/electron-wasix/src-wasix/Cargo.toml", '"tools"'); +requireText("examples/electron-wasix/src-wasix/Cargo.toml", "oliphaunt-wasix-tools"); +requireText("examples/electron-wasix/src-wasix/Cargo.toml", "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu"); +requireText("examples/electron-wasix/src-wasix/Cargo.toml", "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu"); +requireText("examples/electron-wasix/src-wasix/Cargo.lock", "oliphaunt-extension-hstore-wasix-aot-x86_64-unknown-linux-gnu"); +requireText("examples/electron-wasix/src-wasix/Cargo.lock", "oliphaunt-extension-pg-trgm-wasix-aot-x86_64-unknown-linux-gnu"); +requireText("examples/electron-wasix/src-wasix/Cargo.lock", "oliphaunt-extension-unaccent-wasix-aot-x86_64-unknown-linux-gnu"); +requireWasixToolsSmoke("examples/electron-wasix/src-wasix/src/main.rs"); +requireText( + "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml", + 'registry = "oliphaunt-local"', +); +requireText("src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml", '"tools"'); +requireText( + "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml", + "oliphaunt-wasix-tools", +); +requireText( + "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml", + "liboliphaunt-wasix-aot-x86_64-unknown-linux-gnu", +); +requireText( + "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml", + "oliphaunt-wasix-tools-aot-x86_64-unknown-linux-gnu", +); +requireWasixToolsSmoke("src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs"); +rejectText( + "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/src/bench.rs", + String.raw`tcp_addr\(\)\.is_none\(\)`, +); +rejectText("examples/electron/package.json", '"@oliphaunt/ts": "workspace:\\*"'); +rejectText("examples/tauri/src-tauri/Cargo.toml", 'path = "../../../src/sdks/rust'); +rejectText("examples/tauri-wasix/src-tauri/Cargo.toml", 'path = "../../../src/bindings/wasix-rust'); +rejectText("examples/electron-wasix/src-wasix/Cargo.toml", 'path = "../../../src/bindings/wasix-rust'); +rejectText( + "src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml", + 'path = "../../../crates/oliphaunt-wasix"', +); + +requireFile("src/sdks/react-native/examples/expo/package.json"); +requireFile("src/sdks/react-native/examples/expo/maestro/installed-smoke.yaml"); +requireText("src/sdks/react-native/moon.yml", String.raw`^ mobile-build-android:$`); +requireText("src/sdks/react-native/moon.yml", String.raw`^ mobile-e2e-android:$`); +requireText("src/sdks/react-native/moon.yml", String.raw`^ mobile-build-ios:$`); +requireText("src/sdks/react-native/moon.yml", String.raw`^ mobile-e2e-ios:$`); + +console.log("example ownership and scheduling policy verified"); diff --git a/examples/tools/check-examples.sh b/examples/tools/check-examples.sh index 8e8727f6..bd58f036 100755 --- a/examples/tools/check-examples.sh +++ b/examples/tools/check-examples.sh @@ -6,153 +6,4 @@ root="$(git rev-parse --show-toplevel 2>/dev/null)" || { exit 1 } cd "$root" - -run() { - printf '\n==> %s\n' "$*" - "$@" -} - -run examples/tools/check-lockfiles.sh --check - -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 -)" -if [[ -n "$violations" ]]; then - echo "root examples/ may contain only cross-product example policy/tooling" >&2 - echo "$violations" >&2 - exit 1 -fi - -tracked_node_modules="$( - git ls-files 'examples/**/node_modules/**' 'src/**/examples/**/node_modules/**' || true -)" -if [[ -n "$tracked_node_modules" ]]; then - echo "example dependencies must not be tracked" >&2 - echo "$tracked_node_modules" >&2 - exit 1 -fi - -require_file() { - local path="$1" - if [[ ! -f "$path" ]]; then - echo "missing required product-local example file: $path" >&2 - exit 1 - fi -} - -require_text() { - local path="$1" - local pattern="$2" - if ! grep -Eq "$pattern" "$path"; then - echo "missing required example scheduling pattern in $path: $pattern" >&2 - exit 1 - 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" - if grep -Eq "$pattern" "$path"; then - echo "forbidden example local dependency pattern in $path: $pattern" >&2 - exit 1 - 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:$' -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_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" -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/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' -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/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 =' -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" '"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_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' -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_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' -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" -require_text "src/sdks/react-native/moon.yml" '^ mobile-build-android:$' -require_text "src/sdks/react-native/moon.yml" '^ mobile-e2e-android:$' -require_text "src/sdks/react-native/moon.yml" '^ mobile-build-ios:$' -require_text "src/sdks/react-native/moon.yml" '^ mobile-e2e-ios:$' - -echo "example ownership and scheduling policy verified" +exec tools/dev/bun.sh examples/tools/check-examples.mjs "$@" diff --git a/moon.yml b/moon.yml index 31dd3d0d..ec34c3d3 100644 --- a/moon.yml +++ b/moon.yml @@ -239,7 +239,7 @@ tasks: runFromWorkspaceRoot: true smoke: tags: ["runtime", "smoke"] - command: "bash examples/tools/check-examples.sh" + command: "bash tools/dev/bun.sh examples/tools/check-examples.mjs" inputs: - "/examples/**/*" - "/src/**/*" From b3e72d99f55d54c8bd51b604932cfaa2334f2556 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 05:13:49 +0000 Subject: [PATCH 156/308] test: cover example bun tooling in policy gate --- docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 10 ++++++++++ tools/policy/check-policy-tools.sh | 4 ++-- tools/policy/check-repo-structure.sh | 1 + tools/policy/check-tooling-stack.sh | 3 +++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index b01a1c57..77ef8e65 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -89,6 +89,16 @@ until the current-state gates here are checked with fresh local evidence. tools/policy/check-tooling-stack.sh`, `bash tools/policy/check-policy-tools.sh`, `bash tools/policy/check-repo-structure.sh`, and `git diff --check`. +- 2026-06-27: Extended the central policy-tool syntax gate to bundle + `examples/tools/*.mjs` alongside `.github/scripts`, `tools/policy`, and + `tools/graph`, so Bun-backed example tooling migrations are checked by the + same policy lane. Fresh checks passed: `bash + tools/policy/check-policy-tools.sh`, `bash tools/policy/check-tooling-stack.sh`, + `bash tools/policy/check-repo-structure.sh`, + `tools/policy/check-sdk-parity.sh`, `tools/dev/bun.sh + examples/tools/check-examples.mjs`, `tools/policy/check-moon-product-graph.mjs`, + `bash tools/policy/check-docs.sh`, `tools/release/release.py check`, and + `git diff --check`. - 2026-06-27: Added an explicit Rust helper crate inventory. The new `tools/policy/check-rust-helper-crates.mjs` policy check verifies that the only tracked Rust helper crates under `tools/` are `tools/perf/runner` and diff --git a/tools/policy/check-policy-tools.sh b/tools/policy/check-policy-tools.sh index f522319b..455af426 100755 --- a/tools/policy/check-policy-tools.sh +++ b/tools/policy/check-policy-tools.sh @@ -30,11 +30,11 @@ cleanup() { trap cleanup EXIT HUP INT TERM while IFS= read -r script; do - output_name="${script#tools/policy/}" + output_name="${script#./}" output_name="${output_name//\//__}" output_name="${output_name%.mjs}.js" run bun build "$script" --target=bun --outfile="$js_check_root/$output_name" -done < <(find .github/scripts tools/policy tools/graph -type f -name '*.mjs' | LC_ALL=C sort) +done < <(find .github/scripts examples/tools 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 9c6b1d6a..2f6ed0c3 100755 --- a/tools/policy/check-repo-structure.sh +++ b/tools/policy/check-repo-structure.sh @@ -414,6 +414,7 @@ require_text src/shared/fixtures/moon.yml 'id: "shared-fixtures"' require_text src/shared/fixtures/moon.yml 'target/shared-fixtures/manifest.generated.json' require_text tools/policy/moon.yml 'tools/policy/check-policy-tools.sh' require_text tools/policy/check-policy-tools.sh 'bun build "$script" --target=bun' +require_text tools/policy/check-policy-tools.sh 'examples/tools' require_text tools/policy/check-tooling-stack.sh 'tools/policy/assertions/assert-moon-task-policy.mjs' require_text tools/policy/moon.yml '/tools/graph/**/*' require_text tools/graph/moon.yml 'id: "graph-tools"' diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 4c867572..74fee926 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -37,6 +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 examples/tools/check-examples.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 @@ -240,6 +241,8 @@ 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 'examples/tools' tools/policy/check-policy-tools.sh || + fail "policy tooling syntax gate must include Bun-backed example tooling" 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 || From 99f789c2e5b163449cace9b5d6bceec10f69fdb7 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 05:20:44 +0000 Subject: [PATCH 157/308] chore: remove retired helper wrappers --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 19 ++ .../native/bin/build-macos-happy-path.sh | 5 - .../bin/run-native-postgres-regression-sql.sh | 74 ------ .../tools/check-asset-input-fingerprint.sh | 44 ---- tools/perf/bench-react-native-expo-android.sh | 7 - tools/perf/bench-react-native-expo-ios.sh | 7 - tools/perf/matrix/build_bench_matrix.mjs | 239 ------------------ tools/policy/check-repo-structure.sh | 6 + 8 files changed, 25 insertions(+), 376 deletions(-) delete mode 100755 src/runtimes/liboliphaunt/native/bin/build-macos-happy-path.sh delete mode 100755 src/runtimes/liboliphaunt/native/bin/run-native-postgres-regression-sql.sh delete mode 100755 src/runtimes/liboliphaunt/wasix/tools/check-asset-input-fingerprint.sh delete mode 100755 tools/perf/bench-react-native-expo-android.sh delete mode 100755 tools/perf/bench-react-native-expo-ios.sh delete mode 100644 tools/perf/matrix/build_bench_matrix.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 77ef8e65..9a39fd16 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -123,6 +123,25 @@ until the current-state gates here are checked with fresh local evidence. `tools/policy/check-test-strategy.mjs`, `bash tools/policy/check-policy-tools.sh`, `bash tools/policy/check-docs.sh`, a stale-reference `git grep`, and `git diff --check`. +- 2026-06-27: Removed six more confirmed dead helper wrappers after a targeted + shell/JavaScript helper reference sweep and full-path `git grep` found no + docs, CI, Moon, release, policy, or example callers: + `src/runtimes/liboliphaunt/native/bin/build-macos-happy-path.sh`, + `src/runtimes/liboliphaunt/native/bin/run-native-postgres-regression-sql.sh`, + `src/runtimes/liboliphaunt/wasix/tools/check-asset-input-fingerprint.sh`, + `tools/perf/bench-react-native-expo-android.sh`, + `tools/perf/bench-react-native-expo-ios.sh`, and + `tools/perf/matrix/build_bench_matrix.mjs`. The canonical replacements are + `build-postgres18-macos.sh`, `cargo run -p xtask -- assets verify-committed`, + React Native `mobile-drill`, and `run_mobile_footprint_matrix.sh` / + `summarize_native_oliphaunt_matrix.mjs`. Repo-structure policy now rejects + tracking those retired helper paths again. Fresh checks passed: stale-reference + `git grep`, `bash tools/policy/check-repo-structure.sh`, `bash + tools/policy/check-policy-tools.sh`, `bash tools/policy/check-docs.sh`, + `bash tools/policy/check-tooling-stack.sh`, `bash + tools/perf/check-native-perf-harness.sh`, + `tools/policy/check-moon-product-graph.mjs`, `tools/release/release.py + check`, and `git diff --check`. - 2026-06-27: Tightened WASIX Rust split-tools SDK parity. The WASIX package check now requires the `tools` feature to select the split `oliphaunt-wasix-tools` crate plus all tools-AOT target crates, and requires diff --git a/src/runtimes/liboliphaunt/native/bin/build-macos-happy-path.sh b/src/runtimes/liboliphaunt/native/bin/build-macos-happy-path.sh deleted file mode 100755 index 46a3b419..00000000 --- a/src/runtimes/liboliphaunt/native/bin/build-macos-happy-path.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -exec "$script_dir/build-postgres18-macos.sh" "$@" diff --git a/src/runtimes/liboliphaunt/native/bin/run-native-postgres-regression-sql.sh b/src/runtimes/liboliphaunt/native/bin/run-native-postgres-regression-sql.sh deleted file mode 100755 index ec862928..00000000 --- a/src/runtimes/liboliphaunt/native/bin/run-native-postgres-regression-sql.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env bash -set -uo pipefail - -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -. "$script_dir/common.sh" -repo_root="$(oliphaunt_resolve_repo_root "$script_dir")" -work_root="${OLIPHAUNT_WORK_ROOT:-$repo_root/target/liboliphaunt-pg18}" -liboliphaunt="${LIBOLIPHAUNT_PATH:-$work_root/out/liboliphaunt.dylib}" -initdb="${OLIPHAUNT_INITDB:-$work_root/install/bin/initdb}" -postgres="${OLIPHAUNT_POSTGRES:-$work_root/install/bin/postgres}" -test_bin="${OLIPHAUNT_POSTGRES_REGRESSION_BIN:-}" - -cases=( - datatypes_cover_oliphaunt_basic_surface - ddl_schema_view_trigger_and_rollback_behave_like_postgres - transactions_savepoints_and_error_recovery_match_postgres - expected_sql_error_recovery_stays_inside_protocol_loop - pg17_uuidv4_alias_error_is_recoverable - planner_uses_indexes_for_selective_queries_and_updates - direct_blob_copy_round_trips_csv_with_oliphaunt_dev_blob_surface -) - -if [ ! -f "$liboliphaunt" ] || [ ! -x "$initdb" ] || [ ! -x "$postgres" ]; then - echo "native liboliphaunt artifacts are missing; run src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh first" >&2 - exit 1 -fi - -if [ -z "$test_bin" ] || [ ! -x "$test_bin" ]; then - ( - cd "$repo_root" - cargo test --test postgres_regression --no-run - ) || exit $? - test_bin="$( - find "$repo_root/target/debug/deps" \ - -maxdepth 1 \ - -type f \ - -name 'postgres_regression-*' \ - -perm -111 \ - -print | - sort | - tail -n 1 - )" -fi - -if [ -z "$test_bin" ] || [ ! -x "$test_bin" ]; then - echo "could not locate compiled postgres_regression test binary" >&2 - exit 1 -fi - -export OLIPHAUNT_INITDB="$initdb" -export OLIPHAUNT_POSTGRES="$postgres" -export LIBOLIPHAUNT_PATH="$liboliphaunt" -export OLIPHAUNT_INITDB="$initdb" -export OLIPHAUNT_POSTGRES="$postgres" -export LIBOLIPHAUNT_PATH="$liboliphaunt" -export OLIPHAUNT_INITDB="$initdb" -export OLIPHAUNT_POSTGRES="$postgres" -export OLIPHAUNT_WASM_POSTGRES_REGRESSION_NATIVE=1 - -failed=() -for case in "${cases[@]}"; do - printf '\n===== native SQL regression: %s =====\n' "$case" - if ! "$test_bin" "$case" --exact --nocapture; then - failed+=("$case") - fi -done - -if [ "${#failed[@]}" -ne 0 ]; then - printf '\nFAILED native SQL regression cases:\n' >&2 - printf ' %s\n' "${failed[@]}" >&2 - exit 1 -fi - -printf '\nAll native SQL regression cases passed.\n' diff --git a/src/runtimes/liboliphaunt/wasix/tools/check-asset-input-fingerprint.sh b/src/runtimes/liboliphaunt/wasix/tools/check-asset-input-fingerprint.sh deleted file mode 100755 index e7005abb..00000000 --- a/src/runtimes/liboliphaunt/wasix/tools/check-asset-input-fingerprint.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/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" - -base_ref="${ASSET_INPUT_BASE_REF:-}" -if [[ -z "$base_ref" ]]; then - if git rev-parse --verify -q '@{upstream}' >/dev/null; then - base_ref='@{upstream}' - else - base_ref='origin/main' - fi -fi - -if ! git rev-parse --verify -q "${base_ref}^{commit}" >/dev/null; then - echo "asset input fingerprint check skipped: ${base_ref} is not available" >&2 - exit 0 -fi - -changed="$( - git diff --name-only "${base_ref}...HEAD" -- \ - src/sources/third-party \ - src/sources/toolchains \ - src/extensions/catalog/extensions.promoted.toml \ - src/extensions/catalog/extensions.smoke.toml \ - src/runtimes/liboliphaunt/wasix/assets/build \ - src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml \ - src/runtimes/liboliphaunt/wasix/crates/assets/build.rs \ - src/runtimes/liboliphaunt/wasix/crates/assets/src \ - src/runtimes/liboliphaunt/wasix/crates/aot \ - tools/xtask/src \ - src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 -)" - -if [[ -z "$changed" ]]; then - echo "asset input fingerprint check skipped: no asset input changes" - exit 0 -fi - -cargo run -p xtask -- assets verify-committed diff --git a/tools/perf/bench-react-native-expo-android.sh b/tools/perf/bench-react-native-expo-android.sh deleted file mode 100755 index 39436ad3..00000000 --- a/tools/perf/bench-react-native-expo-android.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -export OLIPHAUNT_EXPO_ANDROID_RUNNER="${OLIPHAUNT_EXPO_ANDROID_RUNNER:-benchmark}" -export OLIPHAUNT_EXPO_ANDROID_TIMEOUT_SECONDS="${OLIPHAUNT_EXPO_ANDROID_TIMEOUT_SECONDS:-360}" -exec "$script_dir/../../src/sdks/react-native/tools/mobile-drill.sh" android benchmark "$@" diff --git a/tools/perf/bench-react-native-expo-ios.sh b/tools/perf/bench-react-native-expo-ios.sh deleted file mode 100755 index 11f88e46..00000000 --- a/tools/perf/bench-react-native-expo-ios.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -export OLIPHAUNT_EXPO_IOS_RUNNER="${OLIPHAUNT_EXPO_IOS_RUNNER:-benchmark}" -export OLIPHAUNT_EXPO_IOS_TIMEOUT_SECONDS="${OLIPHAUNT_EXPO_IOS_TIMEOUT_SECONDS:-360}" -exec "$script_dir/../../src/sdks/react-native/tools/mobile-drill.sh" ios benchmark "$@" diff --git a/tools/perf/matrix/build_bench_matrix.mjs b/tools/perf/matrix/build_bench_matrix.mjs deleted file mode 100644 index 92003fa3..00000000 --- a/tools/perf/matrix/build_bench_matrix.mjs +++ /dev/null @@ -1,239 +0,0 @@ -import fs from 'node:fs/promises' -import process from 'node:process' - -function parseArgs(argv) { - const args = {} - for (let index = 0; index < argv.length; index += 1) { - const key = argv[index] - if (!key.startsWith('--')) { - continue - } - const value = argv[index + 1] - if (value && !value.startsWith('--')) { - args[key] = value - index += 1 - } else { - args[key] = 'true' - } - } - return args -} - -function requireArg(args, key) { - const value = args[key] - if (!value) { - throw new Error(`${key} is required`) - } - return value -} - -function sum(values) { - return values.reduce((total, value) => total + value, 0) -} - -function mean(values) { - return sum(values) / values.length -} - -function round(value, decimals = 2) { - return Number(value.toFixed(decimals)) -} - -function formatMicros(value) { - return `${round(value)} us` -} - -function formatMillis(value) { - return `${round(value)} ms` -} - -function formatMillisFromMicros(value) { - if (value === null || value === undefined) { - return '-' - } - return formatMillis(value / 1000) -} - -function formatSecondsFromMicros(value) { - return `${round(value / 1_000_000, 3)} s` -} - -function formatRatio(numerator, denominator) { - if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator === 0) { - return '-' - } - return `${round(numerator / denominator, 2)}x` -} - -function readJson(jsonPath) { - return fs.readFile(jsonPath, 'utf8').then((text) => JSON.parse(text)) -} - -function collectRun(report, suite, mode) { - const run = report.runs.find((entry) => entry.suite === suite && entry.mode === mode) - if (!run) { - throw new Error(`missing ${suite}/${mode} run`) - } - return run -} - -function rttAverageMicros(run) { - return mean(run.tests.map((test) => test.averageMicros ?? test.trimmedAverageMicros)) -} - -function speedTotalMicros(run) { - return sum(run.tests.map((test) => test.elapsedMicros)) -} - -function indexTestsById(run) { - return new Map(run.tests.map((test) => [test.id, test])) -} - -async function main() { - const args = parseArgs(process.argv.slice(2)) - const output = requireArg(args, '--output') - const oxidePath = requireArg(args, '--oxide') - const nativePath = requireArg(args, '--native') - const nodePath = requireArg(args, '--node') - const nodeServerPath = requireArg(args, '--node-server') - const runId = requireArg(args, '--run-id') - const nativeVersion = requireArg(args, '--native-version') - const machineOs = requireArg(args, '--machine-os') - const machineCpu = requireArg(args, '--machine-cpu') - const machineRam = requireArg(args, '--machine-ram') - const machineCores = requireArg(args, '--machine-cores') - - const [oxide, native, node, nodeServer] = await Promise.all([ - readJson(oxidePath), - readJson(nativePath), - readJson(nodePath), - readJson(nodeServerPath), - ]) - - const oxideRttSqlx = collectRun(oxide, 'rtt', 'server_sqlx') - const oxideSpeedSqlx = collectRun(oxide, 'speed', 'server_sqlx') - const nativeRttSqlx = collectRun(native, 'rtt', 'native_postgres_sqlx') - const nativeSpeedSqlx = collectRun(native, 'speed', 'native_postgres_sqlx') - const legacyRttSqlx = collectRun(node, 'rtt', 'legacy_wasix_sqlx') - const legacySpeedSqlx = collectRun(node, 'speed', 'legacy_wasix_sqlx') - - const headlineModes = [ - { - label: 'native pg + SQLx', - rttRun: nativeRttSqlx, - speedRun: nativeSpeedSqlx, - openMicros: nativeRttSqlx.openMicros, - connectMicros: nativeRttSqlx.connectMicros, - setupMicros: nativeRttSqlx.setupMicros, - }, - { - label: 'oliphaunt-wasix + SQLx', - rttRun: oxideRttSqlx, - speedRun: oxideSpeedSqlx, - openMicros: oxideRttSqlx.openMicros, - connectMicros: oxideRttSqlx.connectMicros, - setupMicros: oxideRttSqlx.setupMicros, - }, - { - label: 'legacy WASIX SQLx', - rttRun: legacyRttSqlx, - speedRun: legacySpeedSqlx, - openMicros: legacyRttSqlx.openMicros, - connectMicros: legacyRttSqlx.connectMicros, - setupMicros: legacyRttSqlx.setupMicros, - }, - ] - - const speedMaps = { - oxideSqlx: indexTestsById(oxideSpeedSqlx), - nativeSqlx: indexTestsById(nativeSpeedSqlx), - legacySqlx: indexTestsById(legacySpeedSqlx), - } - - const lines = [] - lines.push(`# Benchmark Matrix ${runId}`) - lines.push('') - lines.push('Machine-local comparison for the current checkout. Each mode runs serially, never in parallel, so no benchmark shares CPU, disk, or memory pressure with another run.') - lines.push('') - lines.push('## Environment') - lines.push('') - lines.push(`- OS: \`${machineOs}\``) - lines.push(`- CPU: \`${machineCpu}\``) - lines.push(`- RAM: \`${machineRam}\``) - lines.push(`- Logical cores: \`${machineCores}\``) - lines.push(`- Node: \`${nodeServer.node}\``) - lines.push( - `- legacy control packages: \`${nodeServer.package}@${nodeServer.version}\`, \`${nodeServer.socketPackage}@${nodeServer.socketVersion}\``, - ) - lines.push(`- Native Postgres: \`${nativeVersion}\``) - lines.push(`- Oxide Wasmer: \`${oxide.wasmerVersion}\``) - lines.push(`- Oxide Wasmer WASIX: \`${oxide.wasmerWasixVersion}\``) - lines.push(`- RTT iterations: \`${oxide.rttIterations}\``) - lines.push(`- Speed source: exact upstream SQL from \`benchmarks/native/sql\``) - lines.push('') - lines.push('## Headline') - lines.push('') - lines.push('| Metric | native pg + SQLx | oliphaunt-wasix + SQLx | legacy WASIX SQLx |') - lines.push('|---|---:|---:|---:|') - - lines.push( - `| Open | ${formatMillisFromMicros(headlineModes[0].openMicros)} | ${formatMillisFromMicros(headlineModes[1].openMicros)} | ${formatMillisFromMicros(headlineModes[2].openMicros)} |`, - ) - - lines.push( - `| Connect | ${formatMillisFromMicros(headlineModes[0].connectMicros)} | ${formatMillisFromMicros(headlineModes[1].connectMicros)} | ${formatMillisFromMicros(headlineModes[2].connectMicros)} |`, - ) - - const rttMetrics = headlineModes.map((mode) => ({ - label: mode.label, - value: rttAverageMicros(mode.rttRun), - })) - lines.push( - `| RTT mean | ${formatMicros(rttMetrics[0].value)} | ${formatMicros(rttMetrics[1].value)} | ${formatMicros(rttMetrics[2].value)} |`, - ) - - const speedMetrics = headlineModes.map((mode) => ({ - label: mode.label, - value: speedTotalMicros(mode.speedRun), - })) - lines.push( - `| Speed total | ${formatSecondsFromMicros(speedMetrics[0].value)} | ${formatSecondsFromMicros(speedMetrics[1].value)} | ${formatSecondsFromMicros(speedMetrics[2].value)} |`, - ) - - lines.push('') - lines.push('## Relative view') - lines.push('') - lines.push(`- oliphaunt-wasix + SQLx RTT vs legacy WASIX SQLx: ${formatRatio(rttAverageMicros(oxideRttSqlx), rttAverageMicros(legacyRttSqlx))}`) - lines.push(`- oliphaunt-wasix + SQLx RTT vs native pg + SQLx: ${formatRatio(rttAverageMicros(oxideRttSqlx), rttAverageMicros(nativeRttSqlx))}`) - lines.push(`- oliphaunt-wasix + SQLx speed total vs legacy WASIX SQLx: ${formatRatio(speedTotalMicros(oxideSpeedSqlx), speedTotalMicros(legacySpeedSqlx))}`) - lines.push(`- oliphaunt-wasix + SQLx speed total vs native pg + SQLx: ${formatRatio(speedTotalMicros(oxideSpeedSqlx), speedTotalMicros(nativeSpeedSqlx))}`) - lines.push('') - lines.push('## Speed Suite') - lines.push('') - lines.push('| ID | Test | native pg + SQLx | oliphaunt-wasix + SQLx | legacy WASIX SQLx |') - lines.push('|---|---|---:|---:|---:|') - - for (const test of oxideSpeedSqlx.tests) { - const oxideSqlx = speedMaps.oxideSqlx.get(test.id).elapsedMicros - const nativeSqlx = speedMaps.nativeSqlx.get(test.id).elapsedMicros - const legacySqlx = speedMaps.legacySqlx.get(test.id).elapsedMicros - lines.push( - `| ${test.id} | ${test.label} | ${formatMillis(nativeSqlx / 1000)} | ${formatMillis(oxideSqlx / 1000)} | ${formatMillis(legacySqlx / 1000)} |`, - ) - } - - lines.push('') - lines.push('## Notes') - lines.push('') - lines.push('- This matrix is meant for local reproducibility, not universal absolute claims. Different CPUs, filesystems, runtime versions, and native Postgres builds will move the numbers.') - lines.push('- The serial runner intentionally avoids parallel execution so disk caches, CPU scheduling, and memory pressure stay isolated by mode.') - lines.push('- The SQLx-to-SQLx comparison to focus on in product docs is `native pg + SQLx` vs `oliphaunt-wasix + SQLx` vs `legacy WASIX SQLx`.') - lines.push('') - - await fs.writeFile(output, `${lines.join('\n')}\n`) -} - -main().catch((error) => { - console.error(error) - process.exitCode = 1 -}) diff --git a/tools/policy/check-repo-structure.sh b/tools/policy/check-repo-structure.sh index 2f6ed0c3..14b20be5 100755 --- a/tools/policy/check-repo-structure.sh +++ b/tools/policy/check-repo-structure.sh @@ -591,7 +591,13 @@ require_file benchmarks/wasix/README.md require_file benchmarks/mobile/README.md require_file benchmarks/reports/README.md reject_tracked_under tools/perf/fixtures +reject_tracked_under tools/perf/bench-react-native-expo-android.sh +reject_tracked_under tools/perf/bench-react-native-expo-ios.sh +reject_tracked_under tools/perf/matrix/build_bench_matrix.mjs reject_tracked_under tools/perf/matrix/run_bench_matrix.sh +reject_tracked_under src/runtimes/liboliphaunt/native/bin/build-macos-happy-path.sh +reject_tracked_under src/runtimes/liboliphaunt/native/bin/run-native-postgres-regression-sql.sh +reject_tracked_under src/runtimes/liboliphaunt/wasix/tools/check-asset-input-fingerprint.sh require_text docs/maintainers/tooling.md 'tools/xtask/src/template_runner.rs' require_text docs/maintainers/tooling.md 'tools/xtask/src/asset_checks.rs' require_text docs/maintainers/tooling.md 'tools/xtask/src/asset_manifest.rs' From d6cc3f65af6efc620a4f006c05183c50aa9fa023 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 05:32:17 +0000 Subject: [PATCH 158/308] chore: add helper reference sweep tooling --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 16 ++ tools/policy/check-repo-structure.sh | 2 + tools/policy/check-repo.sh | 31 ---- tools/policy/check-tooling-stack.sh | 1 + .../list-helper-reference-candidates.mjs | 149 ++++++++++++++++++ 5 files changed, 168 insertions(+), 31 deletions(-) delete mode 100755 tools/policy/check-repo.sh create mode 100644 tools/policy/list-helper-reference-candidates.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 9a39fd16..2b953596 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,22 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Added repeatable Bun dead-code candidate tooling and removed the + stale `tools/policy/check-repo.sh` umbrella wrapper. The new + `tools/policy/list-helper-reference-candidates.mjs` scans live tracked shell, + Python, and JavaScript helper entrypoints and reports low-reference + candidates with both full-path and basename reference counts. The report is + advisory so legitimate human-facing entrypoints do not block CI, while + `check-repo-structure.sh` rejects the retired wrapper path. Fresh checks + passed: `tools/dev/bun.sh tools/policy/list-helper-reference-candidates.mjs + --help`, `tools/dev/bun.sh tools/policy/list-helper-reference-candidates.mjs + --max-refs 0`, `tools/dev/bun.sh + tools/policy/list-helper-reference-candidates.mjs --max-refs 1 --json`, the + unknown-argument failure path, `bash tools/policy/check-policy-tools.sh`, + `bash tools/policy/check-tooling-stack.sh`, `bash + tools/policy/check-repo-structure.sh`, `bash tools/policy/check-docs.sh`, + `tools/policy/check-moon-product-graph.mjs`, and + `tools/release/release.py check`. - 2026-06-27: Moved the cross-product example ownership/local-registry policy checker from shell logic into `examples/tools/check-examples.mjs` so the canonical Moon tasks run through the pinned Bun launcher. The old diff --git a/tools/policy/check-repo-structure.sh b/tools/policy/check-repo-structure.sh index 14b20be5..a6fd489f 100755 --- a/tools/policy/check-repo-structure.sh +++ b/tools/policy/check-repo-structure.sh @@ -209,6 +209,7 @@ 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.mjs +require_file tools/policy/list-helper-reference-candidates.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 @@ -595,6 +596,7 @@ reject_tracked_under tools/perf/bench-react-native-expo-android.sh reject_tracked_under tools/perf/bench-react-native-expo-ios.sh reject_tracked_under tools/perf/matrix/build_bench_matrix.mjs reject_tracked_under tools/perf/matrix/run_bench_matrix.sh +reject_tracked_under tools/policy/check-repo.sh reject_tracked_under src/runtimes/liboliphaunt/native/bin/build-macos-happy-path.sh reject_tracked_under src/runtimes/liboliphaunt/native/bin/run-native-postgres-regression-sql.sh reject_tracked_under src/runtimes/liboliphaunt/wasix/tools/check-asset-input-fingerprint.sh diff --git a/tools/policy/check-repo.sh b/tools/policy/check-repo.sh deleted file mode 100755 index 5e7c71f0..00000000 --- a/tools/policy/check-repo.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/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" -PATH="${CARGO_HOME:-$HOME/.cargo}/bin:$PATH" -export PATH - -run() { - printf '\n==> %s\n' "$*" - "$@" -} - -require() { - if ! command -v "$1" >/dev/null 2>&1; then - echo "missing required command: $1" >&2 - echo "run tools/dev/bootstrap-tools.sh to install pinned maintainer tools" >&2 - exit 1 - fi -} - -run tools/policy/check-repo-structure.sh -run tools/policy/check-tooling-stack.sh -run tools/policy/check-docs.sh -run tools/policy/check-release-policy.py -run tools/release/check_release_metadata.py -run tools/policy/check-moon-product-graph.mjs -run tools/policy/check-prek.sh diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 74fee926..e45dc8a6 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -40,6 +40,7 @@ require_file tools/test/run-js-tests.mjs require_file examples/tools/check-examples.mjs require_file tools/graph/cache-witness.mjs require_file tools/policy/check-final-source-architecture.mjs +require_file tools/policy/list-helper-reference-candidates.mjs require_file tools/policy/check-python-entrypoints.mjs require_file tools/policy/check-rust-helper-crates.mjs require_file tools/policy/check-native-boundaries.mjs diff --git a/tools/policy/list-helper-reference-candidates.mjs b/tools/policy/list-helper-reference-candidates.mjs new file mode 100644 index 00000000..a4844bae --- /dev/null +++ b/tools/policy/list-helper-reference-candidates.mjs @@ -0,0 +1,149 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; +import { statSync } from "node:fs"; +import { basename } from "node:path"; + +const args = process.argv.slice(2); + +function fail(message) { + console.error(`list-helper-reference-candidates.mjs: ${message}`); + process.exit(1); +} + +function usage() { + console.log(`usage: tools/policy/list-helper-reference-candidates.mjs [--max-refs N] [--json] + +Lists tracked shell, Python, and JavaScript helper entrypoints with few textual +references. The output is advisory: each candidate still needs manual review +before removal because some entrypoints are intentionally invoked by humans or +external tools.`); +} + +let maxRefs = 1; +let json = false; +for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (arg === "--max-refs") { + const raw = args[index + 1]; + if (!raw || raw.startsWith("--")) { + fail("--max-refs requires a numeric value"); + } + maxRefs = Number(raw); + if (!Number.isInteger(maxRefs) || maxRefs < 0) { + fail("--max-refs must be a non-negative integer"); + } + index += 1; + } else if (arg === "--json") { + json = true; + } else if (arg === "--help" || arg === "-h") { + usage(); + process.exit(0); + } else { + fail(`unknown argument: ${arg}`); + } +} + +function run(command, commandArgs, options = {}) { + const result = spawnSync(command, commandArgs, { + encoding: "utf8", + ...options, + }); + if (result.error) { + fail(result.error.message); + } + return result; +} + +function gitOutput(gitArgs) { + const result = run("git", gitArgs); + if (result.status !== 0) { + fail(result.stderr.trim() || `git ${gitArgs.join(" ")} failed`); + } + return result.stdout; +} + +const root = gitOutput(["rev-parse", "--show-toplevel"]).trim(); +if (!root) { + fail("must run inside the Oliphaunt git checkout"); +} +process.chdir(root); + +function trackedHelpers() { + return gitOutput([ + "ls-files", + "-z", + "--", + "*.sh", + "*.mjs", + "*.py", + ]) + .split("\0") + .filter(Boolean) + .filter((path) => isFile(path)) + .filter((path) => !path.includes("/node_modules/")) + .filter((path) => !path.startsWith("target/")) + .sort(); +} + +function isFile(path) { + try { + return statSync(path).isFile(); + } catch { + return false; + } +} + +function grepFixed(pattern) { + const result = run("git", ["grep", "-n", "-F", "--", pattern, "--", "."], { + cwd: root, + }); + if (result.status === 1) { + return []; + } + if (result.status !== 0) { + fail(result.stderr.trim() || `git grep failed for ${pattern}`); + } + return result.stdout.split(/\r?\n/u).filter(Boolean); +} + +function externalReferenceCount(path, pattern) { + return grepFixed(pattern).filter((line) => !line.startsWith(`${path}:`)).length; +} + +const candidates = trackedHelpers() + .map((path) => { + const pathReferences = externalReferenceCount(path, path); + const basenameReferences = externalReferenceCount(path, basename(path)); + return { + path, + basename: basename(path), + pathReferences, + basenameReferences, + }; + }) + .filter((candidate) => candidate.pathReferences <= maxRefs && candidate.basenameReferences <= maxRefs) + .sort((left, right) => { + const byPathReferences = left.pathReferences - right.pathReferences; + if (byPathReferences !== 0) { + return byPathReferences; + } + const byBasenameReferences = left.basenameReferences - right.basenameReferences; + if (byBasenameReferences !== 0) { + return byBasenameReferences; + } + return left.path.localeCompare(right.path); + }); + +if (json) { + console.log(JSON.stringify({ maxRefs, candidates }, null, 2)); +} else { + console.log(`Low-reference helper candidates (maxRefs=${maxRefs}):`); + if (candidates.length === 0) { + console.log(" none"); + } + for (const candidate of candidates) { + console.log( + ` ${candidate.path} pathRefs=${candidate.pathReferences} basenameRefs=${candidate.basenameReferences}`, + ); + } +} From 92d1020d73b837b828659d6e01cb1f77fbc69f5b Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 05:38:33 +0000 Subject: [PATCH 159/308] chore: expose python helper inventory sizes --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 13 +++++++++ tools/policy/check-python-entrypoints.mjs | 27 +++++++++++++++---- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 2b953596..c3753887 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: Made the remaining Python helper inventory machine-readable for + the Bun migration pass. `tools/policy/check-python-entrypoints.mjs --list` + now prints line and byte counts per tracked Python entrypoint, and `--json` + emits the same nine-file inventory for future prioritization. The current + remaining Python surface is all release or extension-modeling code, ranging + from `tools/release/product_metadata.py` at 1,101 lines to + `tools/release/release.py` at 3,411 lines; none are low-risk wrapper scripts. + Fresh checks passed: `tools/dev/bun.sh + tools/policy/check-python-entrypoints.mjs`, `tools/dev/bun.sh + tools/policy/check-python-entrypoints.mjs --list`, `tools/dev/bun.sh + tools/policy/check-python-entrypoints.mjs --json`, + `tools/dev/bun.sh tools/policy/check-python-entrypoints.mjs --help`, and the + unknown-argument failure path. - 2026-06-27: Added repeatable Bun dead-code candidate tooling and removed the stale `tools/policy/check-repo.sh` umbrella wrapper. The new `tools/policy/list-helper-reference-candidates.mjs` scans live tracked shell, diff --git a/tools/policy/check-python-entrypoints.mjs b/tools/policy/check-python-entrypoints.mjs index 8c670a66..99df0f6e 100644 --- a/tools/policy/check-python-entrypoints.mjs +++ b/tools/policy/check-python-entrypoints.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env bun import { spawnSync } from "node:child_process"; -import { readFileSync } from "node:fs"; +import { readFileSync, statSync } from "node:fs"; const ALLOWLIST = "tools/policy/python-entrypoints.allowlist"; const PYTHON_PATHSPEC = ":(glob)**/*.py"; @@ -12,13 +12,16 @@ function fail(message) { } function usage() { - console.log("usage: tools/policy/check-python-entrypoints.mjs [--list]"); + console.log("usage: tools/policy/check-python-entrypoints.mjs [--list] [--json]"); } let list = false; +let json = false; for (const arg of args) { if (arg === "--list") { list = true; + } else if (arg === "--json") { + json = true; } else if (arg === "--help" || arg === "-h") { usage(); process.exit(0); @@ -95,10 +98,24 @@ if (missing.length > 0 || stale.length > 0) { fail("update the inventory or port the Python file to Bun"); } -if (list) { +function inventoryEntry(path) { + const text = readFileSync(path, "utf8"); + const lineCount = text.length === 0 ? 0 : text.split(/\r?\n/u).length - (text.endsWith("\n") ? 1 : 0); + return { + path, + lineCount, + byteSize: statSync(path).size, + }; +} + +const inventory = trackedPython.map(inventoryEntry); + +if (json) { + console.log(JSON.stringify({ count: inventory.length, entries: inventory }, null, 2)); +} else if (list) { console.log(`Python entrypoint inventory verified (${trackedPython.length} tracked files):`); - for (const path of trackedPython) { - console.log(` ${path}`); + for (const entry of inventory) { + console.log(` ${entry.path} lines=${entry.lineCount} bytes=${entry.byteSize}`); } } else { console.log(`Python entrypoint inventory verified (${trackedPython.length} tracked files).`); From 63877e7a04d751b9f83f431c53ee445b0eb8e3cb Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 05:49:33 +0000 Subject: [PATCH 160/308] test: parse sdk manifest contract --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 16 +- tools/policy/check-repo-structure.sh | 1 + tools/policy/check-sdk-manifest.mjs | 322 ++++++++++++++++++ tools/policy/check-sdk-parity.sh | 116 +------ tools/policy/check-tooling-stack.sh | 2 + tools/policy/sdk-check-lib.sh | 29 -- 6 files changed, 342 insertions(+), 144 deletions(-) create mode 100644 tools/policy/check-sdk-manifest.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index c3753887..22194446 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -42,7 +42,7 @@ until the current-state gates here are checked with fresh local evidence. - [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 +- [x] 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 @@ -78,6 +78,20 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Replaced brittle raw-string SDK manifest assertions in + `tools/policy/check-sdk-parity.sh` with a parsed Bun contract checker. The new + `tools/policy/check-sdk-manifest.mjs` verifies the exact Rust, WASIX Rust, + Swift, Kotlin, React Native, and TypeScript SDK registry shape, path + existence, unique implementation ownership, delegated runtime references, + unsupported-mode reasons, and TypeScript broker-helper ownership. It is now + required by `check-sdk-parity.sh`, `check-tooling-stack.sh`, and + `check-repo-structure.sh`, and the old shell `require_manifest_text` helper + was removed. Fresh checks passed: `tools/dev/bun.sh + tools/policy/check-sdk-manifest.mjs`, `tools/dev/bun.sh + tools/policy/check-sdk-manifest.mjs --list`, `tools/dev/bun.sh + tools/policy/check-sdk-manifest.mjs --json`, `tools/dev/bun.sh + tools/policy/check-sdk-manifest.mjs --help`, and the unknown-argument failure + path. - 2026-06-27: Made the remaining Python helper inventory machine-readable for the Bun migration pass. `tools/policy/check-python-entrypoints.mjs --list` now prints line and byte counts per tracked Python entrypoint, and `--json` diff --git a/tools/policy/check-repo-structure.sh b/tools/policy/check-repo-structure.sh index a6fd489f..b53064b7 100755 --- a/tools/policy/check-repo-structure.sh +++ b/tools/policy/check-repo-structure.sh @@ -242,6 +242,7 @@ 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/policy/check-sdk-manifest.mjs require_file tools/test/moon.yml require_file tools/test/run-js-tests.mjs require_file src/docs/package.json diff --git a/tools/policy/check-sdk-manifest.mjs b/tools/policy/check-sdk-manifest.mjs new file mode 100644 index 00000000..27cb8362 --- /dev/null +++ b/tools/policy/check-sdk-manifest.mjs @@ -0,0 +1,322 @@ +#!/usr/bin/env bun + +import { existsSync, readFileSync, statSync } from 'node:fs'; + +const manifestPath = 'tools/policy/sdk-manifest.toml'; + +const expected = { + rust: { + classification: 'sdk', + package_name: 'oliphaunt', + implementation_path: 'src/sdks/rust', + documentation_path: 'src/docs/content/sdk/rust', + primary_targets: ['tauri', 'rust-desktop'], + runtime_owner: true, + 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', + }, + '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', + }, + swift: { + classification: 'sdk', + package_name: 'Oliphaunt', + implementation_path: 'src/sdks/swift', + documentation_path: 'src/docs/content/sdk/swift', + primary_targets: ['ios', 'macos'], + runtime_owner: true, + runtime_boundary: 'Oliphaunt', + 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', + }, + kotlin: { + classification: 'sdk', + package_name: 'oliphaunt', + implementation_path: 'src/sdks/kotlin', + documentation_path: 'src/docs/content/sdk/kotlin', + primary_targets: ['android'], + runtime_owner: true, + runtime_boundary: 'OliphauntAndroid', + 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', + }, + 'react-native': { + classification: 'sdk', + package_name: '@oliphaunt/react-native', + implementation_path: 'src/sdks/react-native', + documentation_path: 'src/docs/content/sdk/react-native', + primary_targets: ['react-native-ios', 'react-native-android', 'future-react-native-macos'], + runtime_owner: false, + runtime_boundary: 'TurboModule adapter', + delegates_apple_to: 'swift', + delegates_android_to: 'kotlin', + 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', + }, + typescript: { + classification: 'sdk', + package_name: '@oliphaunt/ts', + implementation_path: 'src/sdks/js', + documentation_path: 'src/docs/content/sdk/typescript', + primary_targets: ['node', 'bun', 'deno', 'tauri-javascript'], + runtime_owner: true, + runtime_boundary: '@oliphaunt/ts', + parity_role: 'desktop-javascript-peer', + 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-prepared-runtimeDirectory-validation', + resource_override: 'libraryPath-runtimeDirectory', + }, +}; + +const expectedSdkIds = Object.keys(expected); +const errors = []; + +function fail(message) { + console.error(`check-sdk-manifest.mjs: ${message}`); + process.exit(1); +} + +function usage() { + console.log('usage: tools/policy/check-sdk-manifest.mjs [--list] [--json]'); +} + +function isPlainObject(value) { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function sameValue(left, right) { + if (Array.isArray(left) || Array.isArray(right)) { + return ( + Array.isArray(left) && + Array.isArray(right) && + left.length === right.length && + left.every((value, index) => sameValue(value, right[index])) + ); + } + if (isPlainObject(left) || isPlainObject(right)) { + if (!isPlainObject(left) || !isPlainObject(right)) { + return false; + } + const leftKeys = Object.keys(left).sort(); + const rightKeys = Object.keys(right).sort(); + return ( + sameValue(leftKeys, rightKeys) && + leftKeys.every((key) => sameValue(left[key], right[key])) + ); + } + return Object.is(left, right); +} + +function formatValue(value) { + return JSON.stringify(value); +} + +function requireDirectory(path, sdkId, field) { + if (!existsSync(path)) { + errors.push(`[sdks.${sdkId}].${field} points at missing path ${formatValue(path)}`); + return; + } + if (!statSync(path).isDirectory()) { + errors.push(`[sdks.${sdkId}].${field} must point at a directory: ${formatValue(path)}`); + } +} + +function sorted(value) { + return [...value].sort((left, right) => left.localeCompare(right)); +} + +const args = process.argv.slice(2); +if (args.includes('--help')) { + usage(); + process.exit(0); +} +if (args.length > 1) { + fail(`expected at most one option, got ${args.join(' ')}`); +} +const mode = args[0] ?? 'check'; +if (!['check', '--list', '--json'].includes(mode)) { + fail(`unknown option: ${mode}`); +} + +const manifest = Bun.TOML.parse(readFileSync(manifestPath, 'utf8')); +if (manifest.schema_version !== 1) { + errors.push(`schema_version is ${formatValue(manifest.schema_version)}; expected 1`); +} +if (!isPlainObject(manifest.sdks)) { + errors.push('manifest must contain an [sdks] table'); +} + +const sdks = isPlainObject(manifest.sdks) ? manifest.sdks : {}; +const actualSdkIds = Object.keys(sdks); +if (!sameValue(sorted(actualSdkIds), sorted(expectedSdkIds))) { + errors.push( + `SDK ids are ${formatValue(sorted(actualSdkIds))}; expected ${formatValue(sorted(expectedSdkIds))}`, + ); +} + +const seenImplementationPaths = new Map(); +for (const sdkId of expectedSdkIds) { + const actual = sdks[sdkId]; + const contract = expected[sdkId]; + if (!isPlainObject(actual)) { + errors.push(`missing [sdks.${sdkId}]`); + continue; + } + + const actualFields = Object.keys(actual).sort(); + const expectedFields = Object.keys(contract).sort(); + if (!sameValue(actualFields, expectedFields)) { + errors.push( + `[sdks.${sdkId}] fields are ${formatValue(actualFields)}; expected ${formatValue(expectedFields)}`, + ); + } + + for (const [field, expectedValue] of Object.entries(contract)) { + if (!sameValue(actual[field], expectedValue)) { + errors.push( + `[sdks.${sdkId}].${field} is ${formatValue(actual[field])}; expected ${formatValue( + expectedValue, + )}`, + ); + } + } + + if (typeof actual.implementation_path === 'string') { + if (seenImplementationPaths.has(actual.implementation_path)) { + errors.push( + `[sdks.${sdkId}].implementation_path duplicates [sdks.${seenImplementationPaths.get( + actual.implementation_path, + )}] path ${formatValue(actual.implementation_path)}`, + ); + } + seenImplementationPaths.set(actual.implementation_path, sdkId); + requireDirectory(actual.implementation_path, sdkId, 'implementation_path'); + } + if (typeof actual.documentation_path === 'string') { + requireDirectory(actual.documentation_path, sdkId, 'documentation_path'); + } + + if (Array.isArray(actual.unsupported_modes) && actual.unsupported_modes.length > 0) { + if ( + typeof actual.unsupported_mode_reason !== 'string' || + actual.unsupported_mode_reason.length === 0 + ) { + errors.push(`[sdks.${sdkId}] must explain unsupported modes`); + } + } +} + +for (const sdkId of expectedSdkIds) { + const actual = sdks[sdkId]; + if (!isPlainObject(actual)) { + continue; + } + for (const delegateField of ['delegates_apple_to', 'delegates_android_to']) { + const delegate = actual[delegateField]; + if (delegate === undefined) { + continue; + } + if (!expectedSdkIds.includes(delegate)) { + errors.push(`[sdks.${sdkId}].${delegateField} points at unknown SDK ${formatValue(delegate)}`); + continue; + } + if (sdks[delegate]?.runtime_owner !== true) { + errors.push(`[sdks.${sdkId}].${delegateField} must point at a runtime-owning SDK`); + } + } +} + +if (sdks.typescript?.depends_on_rust_broker_helper === true) { + if (sdks.typescript.broker_helper_product !== 'oliphaunt-rust') { + errors.push('[sdks.typescript].broker_helper_product must remain oliphaunt-rust'); + } +} + +if (errors.length > 0) { + for (const error of errors) { + console.error(`check-sdk-manifest.mjs: ${error}`); + } + process.exit(1); +} + +if (mode === '--json') { + const summary = { + schemaVersion: manifest.schema_version, + sdkCount: expectedSdkIds.length, + sdks: Object.fromEntries( + expectedSdkIds.map((sdkId) => [ + sdkId, + { + packageName: sdks[sdkId].package_name, + runtimeOwner: sdks[sdkId].runtime_owner, + availableModes: sdks[sdkId].available_modes, + unsupportedModes: sdks[sdkId].unsupported_modes, + artifactResolution: sdks[sdkId].artifact_resolution, + toolResolution: sdks[sdkId].tool_resolution, + extensionResolution: sdks[sdkId].extension_resolution, + }, + ]), + ), + }; + console.log(JSON.stringify(summary, null, 2)); +} else if (mode === '--list') { + for (const sdkId of expectedSdkIds) { + const sdk = sdks[sdkId]; + console.log( + `${sdkId}: modes=${sdk.available_modes.join(',')} unsupported=${ + sdk.unsupported_modes.length > 0 ? sdk.unsupported_modes.join(',') : 'none' + } artifact=${sdk.artifact_resolution} tools=${sdk.tool_resolution} extensions=${ + sdk.extension_resolution + }`, + ); + } +} else { + console.log(`SDK manifest contract verified (${expectedSdkIds.length} SDKs).`); +} diff --git a/tools/policy/check-sdk-parity.sh b/tools/policy/check-sdk-parity.sh index cf3a874b..0b681732 100755 --- a/tools/policy/check-sdk-parity.sh +++ b/tools/policy/check-sdk-parity.sh @@ -11,6 +11,7 @@ require_file docs/internal/OLIPHAUNT_README.md require_file src/docs/content/reference/sdk-products.mdx require_file docs/maintainers/sdk-products-policy.md require_file tools/policy/sdk-manifest.toml +require_file tools/policy/check-sdk-manifest.mjs require_file docs/maintainers/rust-sdk-policy.md require_file src/sdks/swift/README.md require_file src/sdks/kotlin/README.md @@ -84,6 +85,7 @@ require_text src/sdks/swift/tools/check-sdk.sh 'ProtocolFixtureTests.swift' \ node tools/policy/generate-sdk-api-surface.mjs --check node tools/policy/check-sdk-doc-examples.mjs tools/policy/check-native-boundaries.sh +tools/dev/bun.sh tools/policy/check-sdk-manifest.mjs if ! cmp -s src/runtimes/liboliphaunt/native/include/oliphaunt.h src/sdks/swift/Sources/COliphaunt/include/oliphaunt.h; then echo "Swift COliphaunt packaged C ABI header must match src/runtimes/liboliphaunt/native/include/oliphaunt.h" >&2 @@ -100,22 +102,6 @@ require_text docs/internal/OLIPHAUNT_README.md '- `src/runtimes/liboliphaunt/nat "internal Oliphaunt README must use the canonical liboliphaunt directory name" require_text docs/internal/OLIPHAUNT_README.md '- `tools/policy/sdk-manifest.toml`: SDK ownership registry used by parity checks.' \ "internal Oliphaunt README must mention the SDK ownership registry" -require_manifest_text rust 'classification = "sdk"' \ - "SDK manifest must classify Rust as a product SDK" -require_manifest_text rust 'implementation_path = "src/sdks/rust"' \ - "SDK manifest must point Rust SDK ownership at the Rust crate" -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 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" \ @@ -132,86 +118,6 @@ require_text src/bindings/wasix-rust/tools/check-package.sh "WASIX split-tools p "WASIX package check must keep public pg_dump/psql APIs behind the tools feature" require_text src/bindings/wasix-rust/tools/check-package.sh "oliphaunt-wasix tools feature must select the split oliphaunt-wasix-tools crate" \ "WASIX package check must require the tools feature to select split tools payload crates" -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"]' \ - "SDK manifest must classify Swift as the iOS/macOS SDK" -require_manifest_text swift 'runtime_boundary = "Oliphaunt"' \ - "SDK manifest must classify Swift as the iOS/macOS runtime boundary" -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 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"]' \ - "SDK manifest must classify Kotlin as the Android SDK" -require_manifest_text kotlin 'runtime_boundary = "OliphauntAndroid"' \ - "SDK manifest must classify the Kotlin Android facade as the runtime boundary" -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 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' \ - "SDK manifest must prevent React Native from owning a separate database runtime" -require_manifest_text react-native 'delegates_apple_to = "swift"' \ - "SDK manifest must route React Native Apple runtime behavior through Swift" -require_manifest_text react-native 'delegates_android_to = "kotlin"' \ - "SDK manifest must route React Native Android runtime behavior through Kotlin" -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 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" @@ -226,24 +132,6 @@ for mobile_tool in pg_dump psql; do 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"' \ - "SDK manifest must name the TypeScript registry package" -require_manifest_text typescript 'primary_targets = ["node", "bun", "deno", "tauri-javascript"]' \ - "SDK manifest must classify TypeScript as the desktop JavaScript SDK" -require_manifest_text typescript 'available_modes = ["native-direct", "native-broker", "native-server"]' \ - "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-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" \ "TypeScript Deno native resolver must consume the split oliphaunt-tools package" require_text src/sdks/js/src/native/assets-deno.ts "materializeDenoToolsRuntime" \ diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index e45dc8a6..8d8b815d 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -43,6 +43,7 @@ require_file tools/policy/check-final-source-architecture.mjs require_file tools/policy/list-helper-reference-candidates.mjs require_file tools/policy/check-python-entrypoints.mjs require_file tools/policy/check-rust-helper-crates.mjs +require_file tools/policy/check-sdk-manifest.mjs require_file tools/policy/check-native-boundaries.mjs require_file tools/policy/python-entrypoints.allowlist require_file tools/policy/rust-helper-crates.allowlist @@ -262,6 +263,7 @@ grep -Fq 'install_cargo_tool ripgrep rg "$RIPGREP_VERSION"' tools/dev/bootstrap- bun tools/policy/check-python-entrypoints.mjs bun tools/policy/check-rust-helper-crates.mjs +bun tools/policy/check-sdk-manifest.mjs 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 diff --git a/tools/policy/sdk-check-lib.sh b/tools/policy/sdk-check-lib.sh index 534b15c0..e0ffa10c 100755 --- a/tools/policy/sdk-check-lib.sh +++ b/tools/policy/sdk-check-lib.sh @@ -45,35 +45,6 @@ require_text() { fi } -require_manifest_text() { - sdk="$1" - text="$2" - message="$3" - if ! awk -v section="[sdks.$sdk]" -v expected="$text" ' - $0 == section { - in_section = 1 - next - } - /^\[sdks\./ && in_section { - exit - } - in_section && index($0, expected) > 0 { - found = 1 - exit - } - END { - if (found) { - exit 0 - } - exit 1 - } - ' tools/policy/sdk-manifest.toml; then - echo "$message" >&2 - echo "expected '$text' in [sdks.$sdk] of tools/policy/sdk-manifest.toml" >&2 - exit 1 - fi -} - require_no_files_under() { path="$1" message="$2" From 660f75391485ce12bccf754057e9d873ea0cd2fd Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 05:57:29 +0000 Subject: [PATCH 161/308] docs: retire stale release helper references --- .../internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 14 ++++++++++++++ docs/maintainers/release-setup.md | 11 ++++++----- docs/maintainers/release.md | 7 ++++--- tools/policy/check-docs.sh | 16 ++++++++++++++++ 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 22194446..de759eda 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,20 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Ran the low-reference helper scan as part of the P2 cleanup pass. + `tools/dev/bun.sh tools/policy/list-helper-reference-candidates.mjs + --max-refs 0` found no unreferenced tracked helper entrypoints, and the + `--max-refs 1` review showed the flagged CI/release/docs helpers were live + workflow, docs, or release.py entrypoints except for stale maintained-doc + references to the retired `tools/release/sync_release_pr.py` path. Updated + maintainer release docs to the pinned Bun command + `tools/dev/bun.sh tools/release/sync-release-pr.mjs --check`, and + `tools/policy/check-docs.sh` now rejects retired Python release-helper paths + in maintained docs. Fresh checks passed: `tools/dev/bun.sh + tools/policy/list-helper-reference-candidates.mjs --max-refs 0`, `bash + tools/policy/check-policy-tools.sh`, `bash tools/policy/check-tooling-stack.sh`, + `bash tools/policy/check-docs.sh`, `bash tools/policy/check-repo-structure.sh`, + `tools/release/release.py check`, and `git diff --check`. - 2026-06-27: Replaced brittle raw-string SDK manifest assertions in `tools/policy/check-sdk-parity.sh` with a parsed Bun contract checker. The new `tools/policy/check-sdk-manifest.mjs` verifies the exact Rust, WASIX Rust, diff --git a/docs/maintainers/release-setup.md b/docs/maintainers/release-setup.md index f89e89a5..6e5a6116 100644 --- a/docs/maintainers/release-setup.md +++ b/docs/maintainers/release-setup.md @@ -64,11 +64,12 @@ and open/update PRs. Do not use the default `GITHUB_TOKEN` for this path, because PR workflows triggered by the default token do not run as normal human-authored PR checks. After release-please runs, the workflow looks for the open generated release PR, -checks out that PR branch, runs `tools/release/sync_release_pr.py`, and commits -derived compatibility files and lockfile updates back to the same PR when -needed. If no release PR exists, the sync step exits cleanly. Run -`tools/release/sync_release_pr.py --check` locally after manual version -experiments; it is also part of `tools/release/release.py check`. +checks out that PR branch, runs +`tools/dev/bun.sh tools/release/sync-release-pr.mjs`, and commits derived +compatibility files and lockfile updates back to the same PR when needed. If no +release PR exists, the sync step exits cleanly. Run +`tools/dev/bun.sh tools/release/sync-release-pr.mjs --check` locally after +manual version experiments; it is also part of `tools/release/release.py check`. The publish job still needs the repository-scoped `GITHUB_TOKEN` for GitHub release asset uploads, artifact attestations, release-please release creation, diff --git a/docs/maintainers/release.md b/docs/maintainers/release.md index e92ae410..f8252587 100644 --- a/docs/maintainers/release.md +++ b/docs/maintainers/release.md @@ -167,9 +167,10 @@ products that are runtime-compatible with those artifacts through normal Moon dependencies. The extension runtime contract is shared by native and WASIX; changes to that contract correctly affect extension artifacts and runtime lanes through the normal Moon graph. Runtime compatibility versions in extension -`release.toml` files are derived by `sync_release_pr.py --check`; they record -which runtime product versions an exact extension artifact was built against, -but release-please still owns the extension product version, changelog, and tag. +`release.toml` files are derived by +`tools/dev/bun.sh tools/release/sync-release-pr.mjs --check`; they record which +runtime product versions an exact extension artifact was built against, but +release-please still owns the extension product version, changelog, and tag. Exact extension CI writes an internal staging manifest with local paths and a public release manifest without local CI paths. Release verification reads the diff --git a/tools/policy/check-docs.sh b/tools/policy/check-docs.sh index e7630ce7..c44379ef 100755 --- a/tools/policy/check-docs.sh +++ b/tools/policy/check-docs.sh @@ -134,6 +134,22 @@ if git grep -n -F "${retired_docs_args[@]}" -- docs src tools .github .moon | fi rm -f /tmp/docs-retired-grep.$$ +retired_tool_docs_grep=( + 'tools/release/sync_release_pr.py' + 'tools/release/artifact_target_matrix.py' +) +retired_tool_docs_args=() +for retired_tool_doc in "${retired_tool_docs_grep[@]}"; do + retired_tool_docs_args+=(-e "$retired_tool_doc") +done +if git grep -n -F "${retired_tool_docs_args[@]}" -- docs/architecture docs/maintainers src/docs README.md | + grep -v '^tools/policy/check-docs\.sh:' >/tmp/docs-retired-tool-grep.$$ 2>/dev/null; then + cat /tmp/docs-retired-tool-grep.$$ >&2 + rm -f /tmp/docs-retired-tool-grep.$$ + fail "maintained docs must not point at retired Python release helpers" +fi +rm -f /tmp/docs-retired-tool-grep.$$ + if git grep -n \ -e 'f0rr0/oliphaunt-oxide' \ -e 'github.com/f0rr0/oliphaunt-oxide' \ From df396441415af133f4151c40f22105e3fe3268b7 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 06:07:01 +0000 Subject: [PATCH 162/308] chore: add source dead-code candidate scan --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 20 +- tools/policy/check-repo-structure.sh | 1 + tools/policy/check-tooling-stack.sh | 2 + .../list-source-reference-candidates.mjs | 262 ++++++++++++++++++ 4 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 tools/policy/list-source-reference-candidates.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index de759eda..a2004bf0 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -64,7 +64,7 @@ until the current-state gates here are checked with fresh local evidence. ### P2: Cleanup and Tooling Migration -- [ ] Run targeted dead-code detection for Rust, TypeScript/JavaScript, shell, +- [x] 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 @@ -78,6 +78,24 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Added source-module dead-code candidate scanning to complement + the helper-entrypoint scanner. Web/tooling research confirmed Knip as the + full JS/TS unused file/export/dependency option, cargo-machete as the fast + stable Rust unused-dependency option, and cargo-udeps as nightly-dependent; + this pass adds repo-native `tools/policy/list-source-reference-candidates.mjs` + first so routine checks stay Bun-based and do not add another external + maintainer tool. The scanner reviews non-test Rust SDK/WASIX source plus + TypeScript/JavaScript SDK source modules by tracked-text references, is + required by repo structure policy, and runs from `check-tooling-stack.sh` with + `--max-refs 0`. Fresh checks passed: `tools/dev/bun.sh + tools/policy/list-source-reference-candidates.mjs --max-refs 0`, + `tools/dev/bun.sh tools/policy/list-source-reference-candidates.mjs + --surface typescript --max-refs 1 --json`, `tools/dev/bun.sh + tools/policy/list-source-reference-candidates.mjs --surface rust --max-refs + 1`, the bad `--surface` negative smoke, `bash + tools/policy/check-policy-tools.sh`, `bash tools/policy/check-tooling-stack.sh`, + `bash tools/policy/check-repo-structure.sh`, `bash tools/policy/check-docs.sh`, + `tools/release/release.py check`, and `git diff --check`. - 2026-06-27: Ran the low-reference helper scan as part of the P2 cleanup pass. `tools/dev/bun.sh tools/policy/list-helper-reference-candidates.mjs --max-refs 0` found no unreferenced tracked helper entrypoints, and the diff --git a/tools/policy/check-repo-structure.sh b/tools/policy/check-repo-structure.sh index b53064b7..93444429 100755 --- a/tools/policy/check-repo-structure.sh +++ b/tools/policy/check-repo-structure.sh @@ -210,6 +210,7 @@ require_file tools/dev/doctor.sh require_file tools/policy/check-policy-tools.sh require_file tools/policy/check-final-source-architecture.mjs require_file tools/policy/list-helper-reference-candidates.mjs +require_file tools/policy/list-source-reference-candidates.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 diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 8d8b815d..9b07ce4a 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -41,6 +41,7 @@ require_file examples/tools/check-examples.mjs require_file tools/graph/cache-witness.mjs require_file tools/policy/check-final-source-architecture.mjs require_file tools/policy/list-helper-reference-candidates.mjs +require_file tools/policy/list-source-reference-candidates.mjs require_file tools/policy/check-python-entrypoints.mjs require_file tools/policy/check-rust-helper-crates.mjs require_file tools/policy/check-sdk-manifest.mjs @@ -264,6 +265,7 @@ grep -Fq 'install_cargo_tool ripgrep rg "$RIPGREP_VERSION"' tools/dev/bootstrap- bun tools/policy/check-python-entrypoints.mjs bun tools/policy/check-rust-helper-crates.mjs bun tools/policy/check-sdk-manifest.mjs +bun tools/policy/list-source-reference-candidates.mjs --max-refs 0 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 diff --git a/tools/policy/list-source-reference-candidates.mjs b/tools/policy/list-source-reference-candidates.mjs new file mode 100644 index 00000000..e54f4cf6 --- /dev/null +++ b/tools/policy/list-source-reference-candidates.mjs @@ -0,0 +1,262 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; +import { basename, extname } from "node:path"; + +const args = process.argv.slice(2); +const TEXT_SEARCH_EXTENSIONS = new Set([ + ".bash", + ".c", + ".cjs", + ".cpp", + ".gradle", + ".h", + ".hpp", + ".java", + ".js", + ".json", + ".jsonc", + ".kt", + ".lock", + ".m", + ".md", + ".mdx", + ".mjs", + ".mm", + ".podspec", + ".ps1", + ".rs", + ".sh", + ".swift", + ".toml", + ".ts", + ".tsx", + ".txt", + ".xml", + ".yaml", + ".yml", + ".zsh", +]); + +function fail(message) { + console.error(`list-source-reference-candidates.mjs: ${message}`); + process.exit(1); +} + +function usage() { + console.log(`usage: tools/policy/list-source-reference-candidates.mjs [--max-refs N] [--json] [--surface all|typescript|rust] + +Lists tracked SDK/runtime source modules with few textual references. The output +is advisory: each candidate still needs manual review because public entrypoints, +package exports, generated code, and platform bridges can be intentionally +referenced indirectly.`); +} + +let maxRefs = 0; +let json = false; +let surface = "all"; +for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (arg === "--max-refs") { + const raw = args[index + 1]; + if (!raw || raw.startsWith("--")) { + fail("--max-refs requires a numeric value"); + } + maxRefs = Number(raw); + if (!Number.isInteger(maxRefs) || maxRefs < 0) { + fail("--max-refs must be a non-negative integer"); + } + index += 1; + } else if (arg === "--json") { + json = true; + } else if (arg === "--surface") { + surface = args[index + 1] ?? ""; + if (!["all", "typescript", "rust"].includes(surface)) { + fail("--surface must be one of: all, typescript, rust"); + } + index += 1; + } else if (arg === "--help" || arg === "-h") { + usage(); + process.exit(0); + } else { + fail(`unknown argument: ${arg}`); + } +} + +function run(command, commandArgs) { + const result = spawnSync(command, commandArgs, { encoding: "buffer" }); + if (result.error) { + fail(result.error.message); + } + if (result.status !== 0) { + fail(result.stderr.toString("utf8").trim() || `${command} ${commandArgs.join(" ")} failed`); + } + return result.stdout; +} + +const root = run("git", ["rev-parse", "--show-toplevel"]).toString("utf8").trim(); +if (!root) { + fail("must run inside the Oliphaunt git checkout"); +} +process.chdir(root); + +function gitLsFiles() { + return run("git", ["ls-files", "-z"]) + .toString("utf8") + .split("\0") + .filter(Boolean) + .sort(); +} + +async function fileText(path) { + try { + return await Bun.file(path).text(); + } catch (error) { + fail(`failed to read ${path}: ${error.message}`); + } +} + +function isTypeScriptSource(path) { + if (!/\.(ts|tsx|js|mjs|cjs)$/u.test(path)) { + return false; + } + if ( + path.includes("/__tests__/") || + path.includes("/generated/") || + path.endsWith(".d.ts") || + path.endsWith(".config.ts") || + path.endsWith(".config.js") || + path.endsWith(".config.mjs") + ) { + return false; + } + return ( + path.startsWith("src/sdks/js/src/") || + path.startsWith("src/sdks/react-native/src/") || + path.startsWith("src/shared/js-core/src/") + ); +} + +function isRustSource(path) { + if (!path.endsWith(".rs")) { + return false; + } + if ( + path.includes("/tests/") || + path.includes("/generated/") || + path.endsWith("/lib.rs") || + path.endsWith("/mod.rs") + ) { + return false; + } + return ( + path.startsWith("src/sdks/rust/src/") || + path.startsWith("src/bindings/wasix-rust/crates/oliphaunt-wasix/src/") + ); +} + +function sourceKind(path) { + if (isTypeScriptSource(path)) { + return "typescript"; + } + if (isRustSource(path)) { + return "rust"; + } + return null; +} + +function isTextSearchPath(path) { + return TEXT_SEARCH_EXTENSIONS.has(extname(path).toLowerCase()); +} + +function countOccurrences(text, pattern) { + if (!pattern) { + return 0; + } + let count = 0; + let offset = 0; + for (;;) { + const index = text.indexOf(pattern, offset); + if (index === -1) { + return count; + } + count += 1; + offset = index + pattern.length; + } +} + +function referencePatterns(path) { + const name = basename(path); + const ext = extname(name); + const stem = ext ? name.slice(0, -ext.length) : name; + const withoutExtension = path.slice(0, -extname(path).length); + const patterns = new Set([path, withoutExtension, name, stem]); + if (path.endsWith(".ts") || path.endsWith(".tsx")) { + patterns.add(`${stem}.js`); + } + if (path.endsWith(".rs")) { + patterns.add(stem.replaceAll("-", "_")); + } + return [...patterns].filter((pattern) => pattern.length > 1); +} + +const trackedFiles = gitLsFiles(); +const corpus = await Promise.all( + trackedFiles + .filter((path) => isTextSearchPath(path)) + .map(async (path) => ({ + path, + text: await fileText(path), + })), +); +const sourceFiles = trackedFiles + .map((path) => ({ path, kind: sourceKind(path) })) + .filter((entry) => entry.kind !== null && (surface === "all" || entry.kind === surface)); + +const candidates = []; +for (const sourceFile of sourceFiles) { + const patternCounts = referencePatterns(sourceFile.path).map((pattern) => { + let references = 0; + for (const file of corpus) { + if (file.path === sourceFile.path) { + continue; + } + references += countOccurrences(file.text, pattern); + } + return { pattern, references }; + }); + const strongestReferenceCount = Math.max(...patternCounts.map((entry) => entry.references)); + if (strongestReferenceCount <= maxRefs) { + candidates.push({ + path: sourceFile.path, + kind: sourceFile.kind, + strongestReferenceCount, + patternCounts, + }); + } +} + +candidates.sort((left, right) => { + const byReferences = left.strongestReferenceCount - right.strongestReferenceCount; + if (byReferences !== 0) { + return byReferences; + } + const byKind = left.kind.localeCompare(right.kind); + if (byKind !== 0) { + return byKind; + } + return left.path.localeCompare(right.path); +}); + +if (json) { + console.log(JSON.stringify({ maxRefs, surface, candidates }, null, 2)); +} else { + console.log(`Low-reference source candidates (surface=${surface}, maxRefs=${maxRefs}):`); + if (candidates.length === 0) { + console.log(" none"); + } + for (const candidate of candidates) { + console.log( + ` ${candidate.path} kind=${candidate.kind} refs=${candidate.strongestReferenceCount}`, + ); + } +} From f0a1ade4e49cf0a936ab2429eb349ceaf5a36d93 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 06:47:55 +0000 Subject: [PATCH 163/308] fix: harden local registry tool asset staging --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 12 + tools/release/check_consumer_shape.py | 17 +- tools/release/local_registry_publish.py | 299 +++++++++++++++--- 3 files changed, 284 insertions(+), 44 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index a2004bf0..69482a4a 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,18 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Hardened default local-registry publishing for the split + runtime/tools artifact graph. The publisher now prefers + `target/local-registry-current`, stages native runtime/tools assets only as a + complete host-target set, lets strict Cargo prune only non-host target deps, + and ignores malformed Cargo scratch archives from `target/package/tmp-crate` + while keeping real artifact roots strict. Fresh checks passed: + `tools/release/local_registry_publish.py publish --surface cargo --strict`, + `tools/release/local_registry_publish.py publish --surface npm --strict`, + `python3 tools/release/check_consumer_shape.py`, `bash + tools/policy/check-sdk-parity.sh`, `bash tools/policy/check-tooling-stack.sh`, + `bash tools/policy/check-repo-structure.sh`, `bash + tools/policy/check-docs.sh`, and `git diff --check`. - 2026-06-27: Added source-module dead-code candidate scanning to complement the helper-entrypoint scanner. Web/tooling research confirmed Knip as the full JS/TS unused file/export/dependency option, cargo-machete as the fast diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 11cb4d5e..390ebf3f 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -497,6 +497,18 @@ def check_liboliphaunt(findings: list[Finding]) -> None: 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 "DEFAULT_CURRENT_ARTIFACT_ROOT" in local_registry_publisher + and "copy_release_asset_set" in local_registry_publisher + and "native_split_release_assets_ready" in local_registry_publisher + and "native_npm_release_assets_ready" in local_registry_publisher + and "native_split_release_asset_missing_message" in local_registry_publisher + and "native_npm_release_asset_missing_message" in local_registry_publisher + and "stage_release_asset_npm_packages(roots, registry_root, dry_run, result, strict)" in local_registry_publisher + and "cargo_dependency_name_matches_host_target" in local_registry_publisher + and "host target artifact dependencies" in local_registry_publisher + and "NON_PUBLISHABLE_LOCAL_CARGO_CRATE_PREFIXES" in local_registry_publisher + and "is_default_cargo_tmp_crate_artifact" in local_registry_publisher + and "ignored malformed Cargo scratch artifact" in local_registry_publisher and "NATIVE_RUNTIME_TOOL_STEMS" in native_optimizer and "NATIVE_TOOLS_TOOL_STEMS" in native_optimizer and not native_runtime_package_split_failures @@ -1972,10 +1984,11 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: 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 "is missing local registry inputs for host target artifact dependencies" in local_registry_publisher + and "cargo_dependency_name_matches_host_target" 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.", + "Strict local Cargo publishing must fail when release-shaped host target runtime/tools-AOT artifact crates are missing; non-host local pruning must also remove stale feature dep entries.", "tools/release/local_registry_publish.py", severity="P0", ) diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index 766d4f2b..17ed3dbb 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -41,6 +41,7 @@ DEFAULT_RUN_ID = "28049923289" DEFAULT_REPO = "f0rr0/oliphaunt" DEFAULT_REGISTRY_ROOT = ROOT / "target" / "local-registries" +DEFAULT_CURRENT_ARTIFACT_ROOT = ROOT / "target" / "local-registry-current" 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" @@ -54,6 +55,9 @@ "oliphaunt-wasix-aot-x86_64-pc-windows-msvc", "oliphaunt-wasix-aot-x86_64-unknown-linux-gnu", } +NON_PUBLISHABLE_LOCAL_CARGO_CRATE_PREFIXES = ( + "oliphaunt-perf-", +) def local_publish_aggregate_artifacts() -> list[str]: return [ @@ -131,6 +135,7 @@ def add_skip(self, message: str) -> None: def discover_roots(artifact_roots: Iterable[Path]) -> list[Path]: explicit_roots = list(artifact_roots) roots = explicit_roots or [ + DEFAULT_CURRENT_ARTIFACT_ROOT, DEFAULT_ARTIFACT_ROOT, ROOT / "target" / "sdk-artifacts", ROOT / "target" / "package" / "tmp-crate", @@ -239,11 +244,12 @@ def copy_release_assets( destination: Path, patterns: tuple[str, ...], ) -> list[Path]: - candidates: list[Path] = [] + selected: dict[str, tuple[Path, Path]] = {} destination_resolved = destination.resolve() for root in roots: if not root.is_dir(): continue + root_candidates: list[Path] = [] for pattern in patterns: for path in root.rglob(pattern): if not path.is_file(): @@ -253,37 +259,171 @@ def copy_release_assets( continue except ValueError: pass - candidates.append(path) - if not candidates: + root_candidates.append(path) + for path in sorted(root_candidates): + existing = selected.get(path.name) + if existing is None: + selected[path.name] = (path, root) + continue + existing_path, existing_root = existing + if existing_root.resolve() != root.resolve(): + continue + if file_sha256(existing_path) != file_sha256(path): + raise RuntimeError( + f"conflicting release asset {path.name} within {rel(root)}: " + f"{rel(existing_path)} and {rel(path)} differ" + ) + if not selected: return [] shutil.rmtree(destination, ignore_errors=True) destination.mkdir(parents=True, exist_ok=True) copied: list[Path] = [] - for source in sorted(candidates): + for source, _root in sorted(selected.values(), key=lambda item: item[0].name): 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 release_asset_candidate(root: Path, name: str, destination: Path) -> Path | None: + destination_resolved = destination.resolve() + if root.is_file() and root.name == name: + return root + if not root.is_dir(): + return None + + candidates: list[Path] = [] + for path in root.rglob(name): + if not path.is_file(): + continue + try: + path.resolve().relative_to(destination_resolved) + continue + except ValueError: + pass + candidates.append(path) + if not candidates: + return None + + selected = sorted(candidates)[0] + for candidate in candidates[1:]: + if file_sha256(candidate) != file_sha256(selected): + raise RuntimeError( + f"conflicting release asset {name} within {rel(root)}: " + f"{rel(selected)} and {rel(candidate)} differ" + ) + return selected + + +def copy_release_asset_set( + roots: list[Path], + destination: Path, + names: tuple[str, ...], +) -> list[Path]: + for root in roots: + selected: list[Path] = [] + for name in names: + candidate = release_asset_candidate(root, name, destination) + if candidate is None: + break + selected.append(candidate) + if len(selected) != len(names): + continue + + shutil.rmtree(destination, ignore_errors=True) + destination.mkdir(parents=True, exist_ok=True) + copied: list[Path] = [] + for source in selected: + target = destination / source.name + shutil.copy2(source, target) + copied.append(target) + return copied + return [] + + 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 release_asset_dir_has_exact_files(asset_dir: Path, names: tuple[str, ...]) -> bool: + return asset_dir.is_dir() and all((asset_dir / name).is_file() for name in names) + + +def missing_release_asset_names(asset_dir: Path, names: tuple[str, ...]) -> list[str]: + return [name for name in names if not (asset_dir / name).is_file()] + + 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 native_release_asset_name(version: str, target: str, kind: str) -> str: + matches = [ + artifact.asset_name(version) + for artifact in product_metadata.artifact_targets( + product="liboliphaunt-native", + kind=kind, + published_only=True, + ) + if artifact.target == target + and ( + "rust-native-direct" in artifact.surfaces + or "typescript-native-direct" in artifact.surfaces + ) + ] + if len(matches) != 1: + raise RuntimeError( + f"expected exactly one published liboliphaunt-native {kind} asset for {target}, got {matches}" + ) + return matches[0] + + +def native_split_release_asset_names(version: str, target: str) -> tuple[str, str]: + return ( + native_release_asset_name(version, target, "native-runtime"), + native_release_asset_name(version, target, "native-tools"), + ) + + +def native_npm_release_asset_names(version: str, target: str) -> tuple[str, str, str]: + return ( + *native_split_release_asset_names(version, target), + f"liboliphaunt-{version}-icu-data.tar.gz", + ) + + +def native_split_release_assets_ready(asset_dir: Path, version: str, target: str) -> tuple[bool, list[str]]: + required = native_split_release_asset_names(version, target) + missing = missing_release_asset_names(asset_dir, required) + return release_asset_dir_has_exact_files(asset_dir, required), missing + + +def native_npm_release_assets_ready(asset_dir: Path, version: str, target: str) -> tuple[bool, list[str]]: + required = native_npm_release_asset_names(version, target) + missing = missing_release_asset_names(asset_dir, required) + return release_asset_dir_has_exact_files(asset_dir, required), missing + + +def native_split_release_asset_missing_message(asset_dir: Path, version: str, target: str, missing: list[str]) -> str: + required = ", ".join(native_split_release_asset_names(version, target)) + return ( + f"native split release asset staging for {target} requires runtime and tools assets " + f"({required}) under {rel(asset_dir)}; missing {', '.join(missing)}" + ) + + +def native_npm_release_asset_missing_message(asset_dir: Path, version: str, target: str, missing: list[str]) -> str: + required = ", ".join(native_npm_release_asset_names(version, target)) + return ( + f"native npm artifact staging for {target} requires runtime, tools, and ICU assets " + f"({required}) under {rel(asset_dir)}; missing {', '.join(missing)}" + ) + + def host_npm_target() -> str | None: machine = host_platform.machine().lower() if sys.platform == "linux" and machine in {"x86_64", "amd64"}: @@ -1100,6 +1240,7 @@ def npm_tarball_priority(path: Path, registry_root: Path) -> tuple[int, float, s (ROOT / "target" / "release" / "npm-packages", 100), (ROOT / "target" / "sdk-artifacts", 90), (registry_root / "npm-extension-packages", 80), + (DEFAULT_CURRENT_ARTIFACT_ROOT, 60), (DEFAULT_ARTIFACT_ROOT, 30), ]: try: @@ -1144,6 +1285,7 @@ def stage_release_asset_npm_packages( registry_root: Path, dry_run: bool, result: SurfaceResult, + strict: bool, ) -> list[Path]: if dry_run: result.staged.append("dry-run generated liboliphaunt and broker npm artifact packages") @@ -1159,18 +1301,37 @@ def stage_release_asset_npm_packages( lib_asset_dir = ROOT / "target" / "liboliphaunt" / "release-assets" lib_version = release.current_product_version("liboliphaunt-native") lib_patterns = (f"liboliphaunt-{lib_version}-*", f"oliphaunt-tools-{lib_version}-*") - copied_lib = copy_release_assets(roots, lib_asset_dir, lib_patterns) + copied_lib = ( + [] + if target is None + else copy_release_asset_set(roots, lib_asset_dir, native_npm_release_asset_names(lib_version, target)) + ) 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( - path - for _package_name, path in release.liboliphaunt_npm_tarballs( - lib_version, - validate_assets=False, - targets=targets, - ) - ) + if target is None: + result.add_skip("current host does not map to a supported native npm artifact target") + else: + ready, missing = native_npm_release_assets_ready(lib_asset_dir, lib_version, target) + if 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, + ) + ) + else: + message = native_npm_release_asset_missing_message( + lib_asset_dir, + lib_version, + target, + missing, + ) + result.add_skip(message) + if strict: + raise RuntimeError(message) else: result.add_skip("no liboliphaunt release assets found for native npm artifact packages") @@ -1205,7 +1366,7 @@ def stage_release_asset_npm_packages( 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) + generated_tarballs = stage_release_asset_npm_packages(roots, registry_root, dry_run, result, strict) extension_target = host_npm_target() extension_tarball_root = stage_extension_npm_packages( roots, @@ -1327,6 +1488,22 @@ def cargo_package_names_from_roots(roots: list[Path]) -> set[str]: return names +def cargo_dependency_name_matches_host_target(name: str) -> bool: + host_target = host_cargo_release_target() + if host_target is None: + return True + host_triple = cargo_target_triple(host_target) + host_markers = [host_target] + if host_triple is not None: + host_markers.append(host_triple) + return any( + name.endswith(f"-{marker}") + or f"-{marker}-" in name + or f"-aot-{marker}" in name + for marker in host_markers + ) + + def prune_missing_local_artifact_target_dependencies( manifest: Path, available_package_names: set[str], @@ -1371,10 +1548,16 @@ def prune_missing_local_artifact_target_dependencies( return 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) + host_missing_packages = sorted( + package for package in missing_packages if cargo_dependency_name_matches_host_target(package) ) + if not host_missing_packages: + strict = False + else: + raise RuntimeError( + f"{rel(manifest)} is missing local registry inputs for host target artifact dependencies: " + + ", ".join(host_missing_packages) + ) pruned_text = prune_missing_feature_dependencies( "\n".join(output).rstrip() + "\n", set(missing_packages), @@ -2414,6 +2597,7 @@ def cargo_crate_priority(path: Path, registry_root: Path) -> tuple[int, str]: (ROOT / "target/oliphaunt-wasix/cargo-artifacts-check", 90), (ROOT / "target/local-registry-generated", 80), (ROOT / "target/oliphaunt-wasix/cargo-artifacts", 70), + (DEFAULT_CURRENT_ARTIFACT_ROOT, 60), (ROOT / "target/package/tmp-registry", 40), (ROOT / "target/package/tmp-crate", 30), ]: @@ -2426,11 +2610,20 @@ def cargo_crate_priority(path: Path, registry_root: Path) -> tuple[int, str]: return priority, str(path) +def is_default_cargo_tmp_crate_artifact(path: Path) -> bool: + try: + path.resolve().relative_to((ROOT / "target/package/tmp-crate").resolve()) + except ValueError: + return False + return True + + def stage_release_asset_cargo_packages( roots: list[Path], registry_root: Path, dry_run: bool, result: SurfaceResult, + strict: bool, ) -> list[Path]: if dry_run: result.staged.append("dry-run generated release-asset Cargo artifact crates") @@ -2448,7 +2641,11 @@ def stage_release_asset_cargo_packages( lib_version = release.current_product_version("liboliphaunt-native") 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) + copied_lib_assets = ( + [] + if host_target is None + else copy_release_asset_set(roots, lib_asset_dir, native_split_release_asset_names(lib_version, host_target)) + ) 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") @@ -2456,23 +2653,35 @@ def stage_release_asset_cargo_packages( 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" - ) - run( - [ - "tools/dev/bun.sh", - "tools/release/package-liboliphaunt-cargo-artifacts.mjs", - "--version", + ready, missing = native_split_release_assets_ready(lib_asset_dir, lib_version, host_target) + if not ready: + message = native_split_release_asset_missing_message( + lib_asset_dir, lib_version, - "--output-dir", - str(lib_output_dir), - "--target", host_target, - ] - ) - generated_roots.append(lib_output_dir) + missing, + ) + result.add_skip(message) + if strict: + raise RuntimeError(message) + else: + if copied_lib_assets: + result.staged.append( + f"staged {len(copied_lib_assets)} liboliphaunt release asset(s) for Cargo" + ) + run( + [ + "tools/dev/bun.sh", + "tools/release/package-liboliphaunt-cargo-artifacts.mjs", + "--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") @@ -2544,7 +2753,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) + release_asset_roots = stage_release_asset_cargo_packages(roots, registry_root, dry_run, result, strict) if release_asset_roots: roots = [*roots, *release_asset_roots] generated_roots = stage_cargo_source_crates(roots, registry_root, dry_run, result, strict) @@ -2586,9 +2795,15 @@ def publish_cargo(roots: list[Path], registry_root: Path, dry_run: bool, strict: 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)): + if crate_path.name.startswith(NON_PUBLISHABLE_LOCAL_CARGO_CRATE_PREFIXES): + result.add_skip(f"ignored non-publishable local Cargo crate artifact {crate_path.name}") + continue try: package = cargo_metadata_for_crate(crate_path) except RuntimeError as error: + if is_default_cargo_tmp_crate_artifact(crate_path) and "does not contain Cargo.toml" in str(error): + result.add_skip(f"ignored malformed Cargo scratch artifact {rel(crate_path)}") + continue result.add_skip(str(error)) if strict: raise From b0f1018454680f6bfc80dd8ac1b7e491d7b5ff76 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 06:52:37 +0000 Subject: [PATCH 164/308] chore: classify helper migration inventory --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 15 +++++- tools/policy/check-python-entrypoints.mjs | 54 ++++++++++++++----- tools/policy/check-rust-helper-crates.mjs | 47 +++++++++++----- tools/policy/python-entrypoints.allowlist | 21 ++++---- tools/policy/rust-helper-crates.allowlist | 5 +- 5 files changed, 104 insertions(+), 38 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 69482a4a..717560a9 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -67,7 +67,7 @@ until the current-state gates here are checked with fresh local evidence. - [x] 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 +- [x] 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. - [x] Fix or refresh the measured `oliphaunt-js` coverage lane; the current @@ -78,6 +78,19 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Tightened the remaining Python and Rust helper inventories from + path-only allowlists into machine-checked migration decision records. Python + entries now carry a domain, decision, and rationale for the nine remaining + release/local-registry/WASIX-packager/extension-model tools; Rust helper + crates carry the same decision shape for `tools/xtask` and + `tools/perf/runner`. This confirms there are no low-risk wrapper scripts left + in the tracked Python/Rust helper surface; the next Python reduction is a + deliberate release-graph, local-registry, WASIX packager, or extension-model + port. Fresh checks passed: `tools/dev/bun.sh + tools/policy/check-python-entrypoints.mjs --list`, `tools/dev/bun.sh + tools/policy/check-python-entrypoints.mjs --json`, `tools/dev/bun.sh + tools/policy/check-rust-helper-crates.mjs --list`, and `bash + tools/policy/check-tooling-stack.sh`. - 2026-06-27: Hardened default local-registry publishing for the split runtime/tools artifact graph. The publisher now prefers `target/local-registry-current`, stages native runtime/tools assets only as a diff --git a/tools/policy/check-python-entrypoints.mjs b/tools/policy/check-python-entrypoints.mjs index 99df0f6e..b493b9c6 100644 --- a/tools/policy/check-python-entrypoints.mjs +++ b/tools/policy/check-python-entrypoints.mjs @@ -5,6 +5,12 @@ import { readFileSync, statSync } from "node:fs"; const ALLOWLIST = "tools/policy/python-entrypoints.allowlist"; const PYTHON_PATHSPEC = ":(glob)**/*.py"; const args = process.argv.slice(2); +const MIGRATION_DECISIONS = new Set([ + "defer-extension-model-port", + "defer-local-registry-port", + "defer-release-graph-port", + "defer-wasix-packager-port", +]); function fail(message) { console.error(`check-python-entrypoints.mjs: ${message}`); @@ -48,34 +54,49 @@ function parseAllowlist() { const text = readFileSync(ALLOWLIST, "utf8"); const entries = []; for (const [index, rawLine] of text.split(/\r?\n/).entries()) { - const line = rawLine.trim(); + const line = rawLine.trimEnd(); 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}`); + const fields = line.split("\t"); + if (fields.length !== 4) { + fail(`${ALLOWLIST}:${index + 1} must use pathdomainmigration-decisionrationale`); } - entries.push(line); + const [path, domain, migrationDecision, rationale] = fields; + if (path.startsWith("/") || path.includes("..") || !path.endsWith(".py")) { + fail(`${ALLOWLIST}:${index + 1} is not a repo-relative Python path: ${path}`); + } + if (!/^[a-z][a-z0-9-]*$/u.test(domain)) { + fail(`${ALLOWLIST}:${index + 1} has invalid domain ${JSON.stringify(domain)}`); + } + if (!MIGRATION_DECISIONS.has(migrationDecision)) { + fail(`${ALLOWLIST}:${index + 1} has unsupported migration decision ${JSON.stringify(migrationDecision)}`); + } + if (rationale.length < 24) { + fail(`${ALLOWLIST}:${index + 1} needs a concrete migration rationale`); + } + entries.push({ path, domain, migrationDecision, rationale }); } return entries; } function assertSortedUnique(entries) { - const sorted = [...entries].sort(); - const sortedText = sorted.join("\n"); - if (entries.join("\n") !== sortedText) { + const paths = entries.map((entry) => entry.path); + const sorted = [...paths].sort(); + if (paths.join("\n") !== sorted.join("\n")) { 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]}`); + if (entries[index].path === entries[index - 1].path) { + fail(`${ALLOWLIST} contains duplicate entry: ${entries[index].path}`); } } } const trackedPython = gitLsFiles(PYTHON_PATHSPEC); -const allowlistedPython = parseAllowlist(); -assertSortedUnique(allowlistedPython); +const allowlistedEntries = parseAllowlist(); +assertSortedUnique(allowlistedEntries); +const allowlistedPython = allowlistedEntries.map((entry) => entry.path); const tracked = new Set(trackedPython); const allowed = new Set(allowlistedPython); @@ -100,9 +121,16 @@ if (missing.length > 0 || stale.length > 0) { function inventoryEntry(path) { const text = readFileSync(path, "utf8"); + const allowlistEntry = allowlistedEntries.find((entry) => entry.path === path); + if (allowlistEntry === undefined) { + fail(`internal error: ${path} missing from parsed allowlist`); + } const lineCount = text.length === 0 ? 0 : text.split(/\r?\n/u).length - (text.endsWith("\n") ? 1 : 0); return { path, + domain: allowlistEntry.domain, + migrationDecision: allowlistEntry.migrationDecision, + rationale: allowlistEntry.rationale, lineCount, byteSize: statSync(path).size, }; @@ -115,7 +143,9 @@ if (json) { } else if (list) { console.log(`Python entrypoint inventory verified (${trackedPython.length} tracked files):`); for (const entry of inventory) { - console.log(` ${entry.path} lines=${entry.lineCount} bytes=${entry.byteSize}`); + console.log( + ` ${entry.path} domain=${entry.domain} decision=${entry.migrationDecision} lines=${entry.lineCount} bytes=${entry.byteSize}`, + ); } } else { console.log(`Python entrypoint inventory verified (${trackedPython.length} tracked files).`); diff --git a/tools/policy/check-rust-helper-crates.mjs b/tools/policy/check-rust-helper-crates.mjs index 34a7074e..81006cc2 100644 --- a/tools/policy/check-rust-helper-crates.mjs +++ b/tools/policy/check-rust-helper-crates.mjs @@ -5,6 +5,7 @@ import { readFileSync } from "node:fs"; const ALLOWLIST = "tools/policy/rust-helper-crates.allowlist"; const RUST_HELPER_PATHSPEC = ":(glob)tools/**/Cargo.toml"; const args = process.argv.slice(2); +const MIGRATION_DECISIONS = new Set(["keep-rust-domain-tool"]); function fail(message) { console.error(`check-rust-helper-crates.mjs: ${message}`); @@ -45,29 +46,44 @@ function parseAllowlist() { const text = readFileSync(ALLOWLIST, "utf8"); const entries = []; for (const [index, rawLine] of text.split(/\r?\n/).entries()) { - const line = rawLine.trim(); + const line = rawLine.trimEnd(); if (!line || line.startsWith("#")) { continue; } - if (line.startsWith("/") || line.includes("..") || !line.endsWith("/Cargo.toml")) { - fail(`${ALLOWLIST}:${index + 1} is not a repo-relative Cargo.toml path: ${line}`); + const fields = line.split("\t"); + if (fields.length !== 4) { + fail(`${ALLOWLIST}:${index + 1} must use pathdomainmigration-decisionrationale`); } - if (!line.startsWith("tools/")) { - fail(`${ALLOWLIST}:${index + 1} must stay under tools/: ${line}`); + const [path, domain, migrationDecision, rationale] = fields; + if (path.startsWith("/") || path.includes("..") || !path.endsWith("/Cargo.toml")) { + fail(`${ALLOWLIST}:${index + 1} is not a repo-relative Cargo.toml path: ${path}`); } - entries.push(line); + if (!path.startsWith("tools/")) { + fail(`${ALLOWLIST}:${index + 1} must stay under tools/: ${path}`); + } + if (!/^[a-z][a-z0-9-]*$/u.test(domain)) { + fail(`${ALLOWLIST}:${index + 1} has invalid domain ${JSON.stringify(domain)}`); + } + if (!MIGRATION_DECISIONS.has(migrationDecision)) { + fail(`${ALLOWLIST}:${index + 1} has unsupported migration decision ${JSON.stringify(migrationDecision)}`); + } + if (rationale.length < 24) { + fail(`${ALLOWLIST}:${index + 1} needs a concrete migration rationale`); + } + entries.push({ path, domain, migrationDecision, rationale }); } return entries; } function assertSortedUnique(entries) { - const sorted = [...entries].sort(); - if (entries.join("\n") !== sorted.join("\n")) { + const paths = entries.map((entry) => entry.path); + const sorted = [...paths].sort(); + if (paths.join("\n") !== sorted.join("\n")) { 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]}`); + if (entries[index].path === entries[index - 1].path) { + fail(`${ALLOWLIST} contains duplicate entry: ${entries[index].path}`); } } } @@ -83,8 +99,9 @@ function assertHelperCratePolicy(path) { } const trackedRustHelpers = gitLsFiles(RUST_HELPER_PATHSPEC); -const allowlistedRustHelpers = parseAllowlist(); -assertSortedUnique(allowlistedRustHelpers); +const allowlistedEntries = parseAllowlist(); +assertSortedUnique(allowlistedEntries); +const allowlistedRustHelpers = allowlistedEntries.map((entry) => entry.path); const tracked = new Set(trackedRustHelpers); const allowed = new Set(allowlistedRustHelpers); @@ -114,7 +131,11 @@ for (const path of trackedRustHelpers) { if (list) { console.log(`Rust helper crate inventory verified (${trackedRustHelpers.length} tracked crates):`); for (const path of trackedRustHelpers) { - console.log(` ${path}`); + const entry = allowlistedEntries.find((candidate) => candidate.path === path); + if (entry === undefined) { + fail(`internal error: ${path} missing from parsed allowlist`); + } + console.log(` ${path} domain=${entry.domain} decision=${entry.migrationDecision}`); } } else { console.log(`Rust helper crate inventory verified (${trackedRustHelpers.length} tracked crates).`); diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index d62f6bae..a9c168d0 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -1,11 +1,12 @@ # Intentional Python tooling inventory. -# New Python files should be ported to Bun or deliberately added here. -src/extensions/tools/check-extension-model.py -tools/policy/check-release-policy.py -tools/release/check_artifact_targets.py -tools/release/check_consumer_shape.py -tools/release/check_release_metadata.py -tools/release/local_registry_publish.py -tools/release/package_liboliphaunt_wasix_cargo_artifacts.py -tools/release/product_metadata.py -tools/release/release.py +# Format: pathdomainmigration-decisionrationale +# New Python files should be ported to Bun or deliberately added here with a specific migration decision. +src/extensions/tools/check-extension-model.py extensions defer-extension-model-port generates and validates multi-language extension catalog, SDK metadata, docs, and evidence from one model +tools/policy/check-release-policy.py release-policy defer-release-graph-port guards CI and release policy against product metadata and the Bun CI planner during release-graph migration +tools/release/check_artifact_targets.py release-metadata defer-release-graph-port validates release target coverage across workflow producers, product metadata, and package artifact handlers +tools/release/check_consumer_shape.py release-consumer-shape defer-release-graph-port validates cross-SDK package/runtime/install shape from generated release fixtures and source invariants +tools/release/check_release_metadata.py release-metadata defer-release-graph-port validates release metadata and publish-step wiring against the Python release graph while it remains canonical +tools/release/local_registry_publish.py local-registry defer-local-registry-port publishes local Cargo, npm, Maven, and Swift registries from current release artifacts for e2e example validation +tools/release/package_liboliphaunt_wasix_cargo_artifacts.py wasix-cargo-artifacts defer-wasix-packager-port generates split WASIX runtime, tools, ICU, and extension Cargo artifact crates with size-limit enforcement +tools/release/product_metadata.py release-metadata defer-release-graph-port owns the canonical product metadata API consumed by Python release tools and shell callers +tools/release/release.py release-orchestrator defer-release-graph-port owns protected release planning, validation, registry checks, publish dry-runs, and publish dispatch diff --git a/tools/policy/rust-helper-crates.allowlist b/tools/policy/rust-helper-crates.allowlist index af4ae21c..d43e802b 100644 --- a/tools/policy/rust-helper-crates.allowlist +++ b/tools/policy/rust-helper-crates.allowlist @@ -1,4 +1,5 @@ # Intentional Rust helper crate inventory. +# Format: pathdomainmigration-decisionrationale # New Rust helper crates under tools/ should stay product/runtime-critical or move to Bun. -tools/perf/runner/Cargo.toml -tools/xtask/Cargo.toml +tools/perf/runner/Cargo.toml performance keep-rust-domain-tool executes native Postgres, SQLite, and Oliphaunt SDK performance workloads through Rust database clients and process measurement code +tools/xtask/Cargo.toml wasix-assets keep-rust-domain-tool owns WASIX asset parsing, archive/hash validation, source-spine checks, AOT packaging, and release workspace staging From ac10c9b898d03f10c619d146a04b7269e288562c Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 07:05:33 +0000 Subject: [PATCH 165/308] fix: align local registry cargo validation --- tools/release/check_release_metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 5d8ffcbb..8f73b6db 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -396,7 +396,7 @@ def validate_local_registry_publisher() -> None: 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 + or "stage_release_asset_cargo_packages(roots, registry_root, dry_run, result, strict)" not in publisher or "strict=strict" not in publisher or "prune_missing_feature_dependencies" not in publisher ): From 104adb42a0024b0aa63ee5039e170e57a6152783 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 07:14:02 +0000 Subject: [PATCH 166/308] fix: deduplicate local extension manifests --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 10 ++++++ .../examples-ci-release-validation.md | 6 ++++ tools/release/check_release_metadata.py | 22 +++++++++++++ tools/release/local_registry_publish.py | 31 ++++++++++++++++--- 4 files changed, 64 insertions(+), 5 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 717560a9..a625a9a9 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,16 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Removed duplicate native extension Cargo packaging work from + local-registry publishing. Default artifact roots can expose the same + `extension-artifacts.json` rows from both downloaded local-registry artifacts + and canonical `target/extension-artifacts`; discovery now preserves root + priority while deduplicating by product/version/sql name. Fresh checks passed: + `python3 tools/release/check_release_metadata.py`, a targeted + `package_native_extension_cargo_crates(...)` smoke that found 39 unique + extension manifests and generated 54 unique native extension crates, and + `python3 -m py_compile tools/release/local_registry_publish.py + tools/release/check_release_metadata.py`. - 2026-06-27: Tightened the remaining Python and Rust helper inventories from path-only allowlists into machine-checked migration decision records. Python entries now carry a domain, decision, and rationale for the nine remaining diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index 90713b17..c73a5564 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -229,3 +229,9 @@ the release/tooling surface after the runtime tool crate split. - 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. +- Local-registry native extension Cargo packaging now deduplicates + `extension-artifacts.json` rows by product/version/sql name before generating + crates. This keeps downloaded local-registry artifacts and canonical + `target/extension-artifacts` outputs from triggering duplicate packaging work; + a targeted smoke found 39 unique extension manifests and generated 54 unique + native extension crates, including the PostGIS aggregator plus 15 part crates. diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 8f73b6db..f75c4dac 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -6,6 +6,7 @@ import json import re import sys +import tempfile import tomllib from pathlib import Path from typing import NoReturn @@ -418,6 +419,27 @@ def validate_local_registry_publisher() -> None: 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") + with tempfile.TemporaryDirectory(prefix="oliphaunt-extension-manifest-dedupe-") as tmp: + root = Path(tmp) + first = root / "first" / "oliphaunt-extension-demo" + second = root / "second" / "oliphaunt-extension-demo" + for directory in (first, second): + directory.mkdir(parents=True) + (directory / "extension-artifacts.json").write_text( + json.dumps( + { + "schema": "oliphaunt-extension-ci-artifacts-v1", + "product": "oliphaunt-extension-demo", + "version": "0.1.0", + "sqlName": "demo", + } + ) + + "\n", + encoding="utf-8", + ) + manifests = local_registry_publish.discover_extension_manifests([first.parent, second.parent]) + if manifests != [first / "extension-artifacts.json"]: + fail("local registry extension manifest discovery must deduplicate product/version/sql rows by root priority") def validate_rust() -> None: diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index 17ed3dbb..b6dd9986 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -487,14 +487,35 @@ def extension_npm_payload_package(sql_name: str, target: str, index: int) -> str def discover_extension_manifests(roots: list[Path]) -> list[Path]: - manifests: list[Path] = [] + manifests: dict[tuple[str, ...], Path] = {} + seen_paths: set[Path] = set() for root in roots: if root.is_file() and root.name == "extension-artifacts.json": - manifests.append(root) + candidates = [root] + elif root.is_dir(): + candidates = sorted(path for path in root.rglob("extension-artifacts.json") if path.is_file()) + else: continue - if root.is_dir(): - manifests.extend(path for path in root.rglob("extension-artifacts.json") if path.is_file()) - return sorted(set(manifests)) + for manifest in candidates: + resolved = manifest.resolve() + if resolved in seen_paths: + continue + seen_paths.add(resolved) + manifests.setdefault(extension_manifest_identity(manifest), manifest) + return list(manifests.values()) + + +def extension_manifest_identity(manifest: Path) -> tuple[str, ...]: + try: + data = json.loads(manifest.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return ("path", str(manifest.resolve())) + product = data.get("product") + version = data.get("version") + sql_name = data.get("sqlName") + if all(isinstance(value, str) and value for value in [product, version, sql_name]): + return ("extension", str(product), str(version), str(sql_name)) + return ("path", str(manifest.resolve())) def safe_package_path(package_name: str) -> str: From 3f93459d920ab05bbc17385b3e927cd8bd5f8356 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 07:39:12 +0000 Subject: [PATCH 167/308] chore: harden helper reference scan --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 37 +++++++++++++- .../examples-ci-release-validation.md | 48 ++++++++++++++++--- .../list-helper-reference-candidates.mjs | 39 ++++++++++++++- 3 files changed, 114 insertions(+), 10 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index a625a9a9..33ecb8df 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,39 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Refreshed local runner, release/local-registry, and P2 tooling + evidence after the split runtime/tools package verification. Current web + research still points to upstream `nektos/act` as the practical local Linux + GitHub Actions runner because it executes workflow jobs through Docker runner + images; local checks confirmed `act` v0.2.89, `act -l` parsing for CI, + Release, and mobile E2E workflows, and a `release-intent` CI dry run with + `ghcr.io/catthehacker/ubuntu:act-latest`. The full Linux CI lane remains + open because it should run from a committed disposable worktree, and this + evidence does not claim macOS, Windows, iOS, or Android device/simulator + lanes are validated by Linux-local `act`. +- 2026-06-27: Hardened the helper dead-code scanner so low-reference + candidates account for path-suffix references as well as full-path and + basename references. This avoids treating nested helpers as weaker candidates + when callers use stable suffixes such as `tools/check-fumadocs-source.mjs`. + Fresh checks passed: `tools/dev/bun.sh + tools/policy/list-helper-reference-candidates.mjs --help`, `tools/dev/bun.sh + tools/policy/list-helper-reference-candidates.mjs --max-refs 0`, + `tools/dev/bun.sh tools/policy/list-helper-reference-candidates.mjs + --max-refs 1 --json`, and the unknown-argument failure path. +- 2026-06-27: Revalidated the current split tools package surface with strict + local Cargo publication and release gates. Fresh checks passed: + `cargo check -p oliphaunt-tools --locked`, `cargo check -p + oliphaunt-wasix-tools --locked`, `cargo test -p oliphaunt-tools --locked`, + `python3 tools/release/check_release_metadata.py`, `python3 + tools/release/check_consumer_shape.py`, `tools/dev/bun.sh + tools/policy/check-wasix-release-dependency-invariants.mjs`, `bash + tools/policy/check-sdk-parity.sh`, `python3 + tools/release/check_artifact_targets.py`, + `tools/release/local_registry_publish.py publish --surface cargo --strict`, + `tools/release/local_registry_publish.py publish --surface npm --strict`, + `tools/release/release.py check`, and `git diff --check`. A generated crate + sweep over `target/local-registries` found 836 `.crate` files and no crate + above the 10 MiB crates.io limit. - 2026-06-27: Removed duplicate native extension Cargo packaging work from local-registry publishing. Default artifact roots can expose the same `extension-artifacts.json` rows from both downloaded local-registry artifacts @@ -176,8 +209,8 @@ until the current-state gates here are checked with fresh local evidence. stale `tools/policy/check-repo.sh` umbrella wrapper. The new `tools/policy/list-helper-reference-candidates.mjs` scans live tracked shell, Python, and JavaScript helper entrypoints and reports low-reference - candidates with both full-path and basename reference counts. The report is - advisory so legitimate human-facing entrypoints do not block CI, while + candidates with full-path, path-suffix, and basename reference counts. The + report is advisory so legitimate human-facing entrypoints do not block CI, while `check-repo-structure.sh` rejects the retired wrapper path. Fresh checks passed: `tools/dev/bun.sh tools/policy/list-helper-reference-candidates.mjs --help`, `tools/dev/bun.sh tools/policy/list-helper-reference-candidates.mjs diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index c73a5564..b0026f53 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -49,7 +49,7 @@ the release/tooling surface after the runtime tool crate split. - [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 +- [x] Document local runner limitations instead of pretending macOS, Windows, iOS, or Android lanes were validated on Linux. ## P1: SDK Consistency @@ -72,16 +72,52 @@ the release/tooling surface after the runtime tool crate split. ## 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 +- [x] Run dead-code scans for Rust, TypeScript, shell, and release scripts. +- [x] Remove generated or stale example build outputs if they are tracked accidentally. +- [x] 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 +- [x] 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. +- [x] Keep build/runtime-critical Rust and platform shell where they remain idiomatic. ## Current Evidence +- On 2026-06-27, current local-runner research and local checks still support + `act` as the pragmatic Linux GitHub Actions runner for this repository: + the upstream `nektos/act` project describes running workflows locally through + Docker containers, and its runner docs map GitHub runner labels to local + images. Fresh local checks passed with `act` v0.2.89: `act -l` parsed the CI, + Release, and mobile E2E workflows, and + `act workflow_dispatch -W .github/workflows/ci.yml -j release-intent --dryrun + -P ubuntu-latest=ghcr.io/catthehacker/ubuntu:act-latest` selected the + expected Linux CI job. Full Linux lane execution remains open because it + should run from a committed disposable worktree, while macOS, Windows, iOS, + and Android device/simulator lanes remain outside what a Linux-local `act` + run proves. +- On 2026-06-27, the P2 helper/dead-code tooling pass was refreshed. The + helper scanner now counts stable path-suffix references in addition to + full-path and basename references, so nested tools such as + `src/docs/tools/check-fumadocs-source.mjs` are not treated as weaker + candidates merely because callers use a shorter repository-local suffix. + Fresh scans reported no unreferenced helper entrypoints with + `tools/dev/bun.sh tools/policy/list-helper-reference-candidates.mjs + --max-refs 0`, and the tracked-file sweep found no accidentally tracked + generated example output directories; the tracked lockfiles and + `coverage/baseline.toml` are intentional policy inputs. +- On 2026-06-27, the remaining Python and Rust helper inventories were + rechecked. `tools/dev/bun.sh tools/policy/check-python-entrypoints.mjs --list` + verified the nine remaining Python entrypoints, all deferred release, + local-registry, WASIX-packager, or extension-modeling ports rather than + low-risk wrappers. `tools/dev/bun.sh + tools/policy/check-rust-helper-crates.mjs --list` verified the only Rust + helper crates are `tools/perf/runner` and `tools/xtask`, both retained as + domain tools. +- On 2026-06-27, strict local Cargo and npm publication were rerun against the + current split runtime/tools package surface with + `tools/release/local_registry_publish.py publish --surface cargo --strict` + and `tools/release/local_registry_publish.py publish --surface npm --strict`. + A generated crate sweep over `target/local-registries` found no `.crate` + above the 10 MiB crates.io limit. - 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 diff --git a/tools/policy/list-helper-reference-candidates.mjs b/tools/policy/list-helper-reference-candidates.mjs index a4844bae..d794202c 100644 --- a/tools/policy/list-helper-reference-candidates.mjs +++ b/tools/policy/list-helper-reference-candidates.mjs @@ -110,23 +110,58 @@ function externalReferenceCount(path, pattern) { return grepFixed(pattern).filter((line) => !line.startsWith(`${path}:`)).length; } +function referenceSuffixes(path) { + const parts = path.split("/"); + if (parts.length <= 2) { + return []; + } + const suffixes = []; + for (let index = 1; index < parts.length - 1; index += 1) { + suffixes.push(parts.slice(index).join("/")); + } + return suffixes; +} + +function strongestSuffixReference(path) { + let best = { pattern: null, references: 0 }; + for (const pattern of referenceSuffixes(path)) { + const references = externalReferenceCount(path, pattern); + if (references > best.references) { + best = { pattern, references }; + } + } + return best; +} + const candidates = trackedHelpers() .map((path) => { const pathReferences = externalReferenceCount(path, path); const basenameReferences = externalReferenceCount(path, basename(path)); + const suffixReference = strongestSuffixReference(path); return { path, basename: basename(path), pathReferences, basenameReferences, + suffixPattern: suffixReference.pattern, + suffixReferences: suffixReference.references, }; }) - .filter((candidate) => candidate.pathReferences <= maxRefs && candidate.basenameReferences <= maxRefs) + .filter( + (candidate) => + candidate.pathReferences <= maxRefs && + candidate.basenameReferences <= maxRefs && + candidate.suffixReferences <= maxRefs, + ) .sort((left, right) => { const byPathReferences = left.pathReferences - right.pathReferences; if (byPathReferences !== 0) { return byPathReferences; } + const bySuffixReferences = left.suffixReferences - right.suffixReferences; + if (bySuffixReferences !== 0) { + return bySuffixReferences; + } const byBasenameReferences = left.basenameReferences - right.basenameReferences; if (byBasenameReferences !== 0) { return byBasenameReferences; @@ -143,7 +178,7 @@ if (json) { } for (const candidate of candidates) { console.log( - ` ${candidate.path} pathRefs=${candidate.pathReferences} basenameRefs=${candidate.basenameReferences}`, + ` ${candidate.path} pathRefs=${candidate.pathReferences} suffixRefs=${candidate.suffixReferences} basenameRefs=${candidate.basenameReferences}`, ); } } From 71407e43da72449f880bb9044b7f5449bbf7b53c Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 07:55:42 +0000 Subject: [PATCH 168/308] chore: retire product metadata version cli --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 27 ++++++++++++++++--- .../examples-ci-release-validation.md | 12 ++++++++- tools/policy/check-python-entrypoints.mjs | 8 +++--- tools/policy/python-entrypoints.allowlist | 2 +- tools/release/product_metadata.py | 19 +++---------- 5 files changed, 44 insertions(+), 24 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 33ecb8df..e27545ef 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -134,6 +134,27 @@ until the current-state gates here are checked with fresh local evidence. tools/policy/check-python-entrypoints.mjs --json`, `tools/dev/bun.sh tools/policy/check-rust-helper-crates.mjs --list`, and `bash tools/policy/check-tooling-stack.sh`. +- 2026-06-27: Retired the stale direct + `tools/release/product_metadata.py version` CLI after confirming real product + version callers already use the Bun helper `tools/release/product-version.mjs`. + `product_metadata.py` remains as a Python compatibility module for the + unported release tools, but direct execution now fails with module-only + guidance instead of exposing a second version-read path. The Python inventory + checker now reports a tooling inventory rather than overstating every tracked + Python module as an entrypoint. Fresh checks passed: + `tools/dev/bun.sh tools/release/product-version.mjs version + liboliphaunt-native`, the expected failing `python3 + tools/release/product_metadata.py version liboliphaunt-native` guidance path, + `python3 -m py_compile tools/release/product_metadata.py`, `tools/dev/bun.sh + tools/policy/check-python-entrypoints.mjs --list`, `bash + tools/policy/check-tooling-stack.sh`, `bash + tools/policy/check-policy-tools.sh`, `bash tools/policy/check-docs.sh`, + `tools/release/release.py check`, + `tools/release/local_registry_publish.py publish --surface cargo --strict`, + `tools/release/local_registry_publish.py publish --surface npm --strict`, + and `git diff --check`. A generated crate sweep over 836 `.crate` files + found no crate above the 10 MiB crates.io limit; the largest observed crate + was 10,212,312 bytes. - 2026-06-27: Hardened default local-registry publishing for the split runtime/tools artifact graph. The publisher now prefers `target/local-registry-current`, stages native runtime/tools assets only as a @@ -194,7 +215,7 @@ until the current-state gates here are checked with fresh local evidence. path. - 2026-06-27: Made the remaining Python helper inventory machine-readable for the Bun migration pass. `tools/policy/check-python-entrypoints.mjs --list` - now prints line and byte counts per tracked Python entrypoint, and `--json` + now prints line and byte counts per tracked Python tooling file, and `--json` emits the same nine-file inventory for future prioritization. The current remaining Python surface is all release or extension-modeling code, ranging from `tools/release/product_metadata.py` at 1,101 lines to @@ -248,7 +269,7 @@ until the current-state gates here are checked with fresh local evidence. `tools/xtask`, rejects stale or unlisted helper crates, and requires each to remain unpublished with empty default features so routine policy checks do not compile optional runtime-heavy paths. `check-tooling-stack.sh` now runs the - inventory beside the Python entrypoint inventory. Fresh checks passed: + inventory beside the Python tooling inventory. Fresh checks passed: `tools/dev/bun.sh tools/policy/check-rust-helper-crates.mjs`, `tools/dev/bun.sh tools/policy/check-rust-helper-crates.mjs --list`, `tools/dev/bun.sh tools/policy/check-rust-helper-crates.mjs --help`, an @@ -301,7 +322,7 @@ until the current-state gates here are checked with fresh local evidence. `bash tools/policy/check-docs.sh`. - 2026-06-27: Tightened the Python tooling inventory audit. `tools/policy/check-python-entrypoints.mjs` now rejects unknown flags and - makes `--list` print the validated tracked Python entrypoints instead of only + makes `--list` print the validated tracked Python tooling files instead of only a count, giving the remaining migration pass concrete file-level evidence for the current 9 intentional Python scripts. Fresh checks passed: `tools/dev/bun.sh diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index b0026f53..d7395835 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -106,12 +106,22 @@ the release/tooling surface after the runtime tool crate split. `coverage/baseline.toml` are intentional policy inputs. - On 2026-06-27, the remaining Python and Rust helper inventories were rechecked. `tools/dev/bun.sh tools/policy/check-python-entrypoints.mjs --list` - verified the nine remaining Python entrypoints, all deferred release, + verified the nine remaining Python tooling files, all deferred release, local-registry, WASIX-packager, or extension-modeling ports rather than low-risk wrappers. `tools/dev/bun.sh tools/policy/check-rust-helper-crates.mjs --list` verified the only Rust helper crates are `tools/perf/runner` and `tools/xtask`, both retained as domain tools. +- On 2026-06-27, the stale direct `tools/release/product_metadata.py version` + CLI was retired. Product version reads remain on the Bun helper + `tools/release/product-version.mjs`, and direct execution of + `product_metadata.py` now fails with module-only guidance instead of exposing + a second version-read path. Fresh validation passed for the Bun version + helper, the expected failing Python guidance path, Python compile, tooling + inventory, policy tooling, docs, `tools/release/release.py check`, and strict + local Cargo/npm publication. A sweep of 836 generated `.crate` files found no + crate above the 10 MiB crates.io limit; the largest observed crate was + 10,212,312 bytes. - On 2026-06-27, strict local Cargo and npm publication were rerun against the current split runtime/tools package surface with `tools/release/local_registry_publish.py publish --surface cargo --strict` diff --git a/tools/policy/check-python-entrypoints.mjs b/tools/policy/check-python-entrypoints.mjs index b493b9c6..8a0325f4 100644 --- a/tools/policy/check-python-entrypoints.mjs +++ b/tools/policy/check-python-entrypoints.mjs @@ -105,7 +105,7 @@ 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:"); + console.error("tracked Python files missing from the intentional tooling inventory:"); for (const path of missing) { console.error(` ${path}`); } @@ -116,7 +116,7 @@ if (missing.length > 0 || stale.length > 0) { console.error(` ${path}`); } } - fail("update the inventory or port the Python file to Bun"); + fail("update the tooling inventory or port the Python file to Bun"); } function inventoryEntry(path) { @@ -141,12 +141,12 @@ const inventory = trackedPython.map(inventoryEntry); if (json) { console.log(JSON.stringify({ count: inventory.length, entries: inventory }, null, 2)); } else if (list) { - console.log(`Python entrypoint inventory verified (${trackedPython.length} tracked files):`); + console.log(`Python tooling inventory verified (${trackedPython.length} tracked files):`); for (const entry of inventory) { console.log( ` ${entry.path} domain=${entry.domain} decision=${entry.migrationDecision} lines=${entry.lineCount} bytes=${entry.byteSize}`, ); } } else { - console.log(`Python entrypoint inventory verified (${trackedPython.length} tracked files).`); + console.log(`Python tooling inventory verified (${trackedPython.length} tracked files).`); } diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index a9c168d0..68ba769f 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -8,5 +8,5 @@ tools/release/check_consumer_shape.py release-consumer-shape defer-release-graph tools/release/check_release_metadata.py release-metadata defer-release-graph-port validates release metadata and publish-step wiring against the Python release graph while it remains canonical tools/release/local_registry_publish.py local-registry defer-local-registry-port publishes local Cargo, npm, Maven, and Swift registries from current release artifacts for e2e example validation tools/release/package_liboliphaunt_wasix_cargo_artifacts.py wasix-cargo-artifacts defer-wasix-packager-port generates split WASIX runtime, tools, ICU, and extension Cargo artifact crates with size-limit enforcement -tools/release/product_metadata.py release-metadata defer-release-graph-port owns the canonical product metadata API consumed by Python release tools and shell callers +tools/release/product_metadata.py release-metadata defer-release-graph-port owns the Python compatibility product metadata API consumed by release tools while direct version reads use Bun tools/release/release.py release-orchestrator defer-release-graph-port owns protected release planning, validation, registry checks, publish dry-runs, and publish dispatch diff --git a/tools/release/product_metadata.py b/tools/release/product_metadata.py index 4aaa6ed0..e70f6d88 100644 --- a/tools/release/product_metadata.py +++ b/tools/release/product_metadata.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 """Shared release product metadata. Release identity comes from release-please manifest-mode config. Product-local @@ -1084,18 +1083,8 @@ def read_current_version(product: str, graph: dict | None = None) -> str: return version -def ensure_semver(product: str, version: str) -> str: - if not re.fullmatch(r"[0-9]+[.][0-9]+[.][0-9]+(?:[-+][0-9A-Za-z][0-9A-Za-z.-]*)?", version): - fail(f"{product} version is not semver-like: {version!r}") - return version - - -def main(argv: list[str]) -> int: - if len(argv) == 2 and argv[0] == "version": - print(ensure_semver(argv[1], read_current_version(argv[1]))) - return 0 - fail("usage: tools/release/product_metadata.py version ") - - if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) + fail( + "tools/release/product_metadata.py is a Python compatibility module; " + "use tools/dev/bun.sh tools/release/product-version.mjs version for version reads" + ) From 86798e62b2d1249fdd0c8772a8587a1331ae733e Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 08:18:58 +0000 Subject: [PATCH 169/308] docs: refresh local ci validation evidence --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 27 +++++++++++++++++++ .../examples-ci-release-validation.md | 21 +++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index e27545ef..d2949bea 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,33 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Re-ran the complementary strict npm local-registry publication + after the current Cargo split verification. Fresh check passed: + `tools/release/local_registry_publish.py publish --surface npm --strict`. + The run optimized the root native npm payload with `--tool-set runtime` and + the split tools npm payload with `--tool-set tools`, published/replaced + `@oliphaunt/liboliphaunt-linux-x64-gnu`, + `@oliphaunt/tools-linux-x64-gnu`, `@oliphaunt/icu`, `@oliphaunt/ts`, broker, + node-direct optional packages, and native extension package/payload families + through Verdaccio. Direct source inspection confirmed the root npm runtime + package contains only `runtime/bin/initdb`, `runtime/bin/pg_ctl`, and + `runtime/bin/postgres`, while the split tools package contains only + `runtime/bin/pg_dump` and `runtime/bin/psql`. +- 2026-06-27: Re-ran Linux-local CI evidence from disposable worktrees at + `71407e43da72449f880bb9044b7f5449bbf7b53c`. Local prerequisites were + `act` v0.2.89 and Docker 29.5.3, and `act -l` parsed the CI, Release, and + mobile E2E workflows. The PR-shaped + `act pull_request -e /tmp/oliphaunt-act-events/pr71-current.json -W + .github/workflows/ci.yml -j release-intent + -P ubuntu-latest=ghcr.io/catthehacker/ubuntu:act-latest` run succeeded. + The `affected` job reached successful CI planning, emitted the full builder + job set, and produced `check_count=21`, `policy_count=64`, and + `test_count=7`; it then failed only in `Upload build plan` because the + local `act` artifact server rejected `actions/upload-artifact@v7` with + `unknown field "mime_type"`. Current upstream `nektos/act` issues report + the same artifact protocol mismatch for `upload-artifact@v7`, so this is a + local-runner compatibility limit rather than evidence that the GitHub-hosted + CI upload step is broken. - 2026-06-27: Refreshed local runner, release/local-registry, and P2 tooling evidence after the split runtime/tools package verification. Current web research still points to upstream `nektos/act` as the practical local Linux diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index d7395835..808072f9 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -82,6 +82,27 @@ the release/tooling surface after the runtime tool crate split. ## Current Evidence +- On 2026-06-27, strict npm local-registry publication was rerun against the + current split runtime/tools package surface with + `tools/release/local_registry_publish.py publish --surface npm --strict`. + The run published/replaced the JS SDK package, native root runtime package, + split native tools package, ICU package, broker/node-direct packages, and + native extension package/payload families through Verdaccio. Direct generated + source inspection confirmed `@oliphaunt/liboliphaunt-linux-x64-gnu` contains + only `runtime/bin/initdb`, `runtime/bin/pg_ctl`, and `runtime/bin/postgres`, + while `@oliphaunt/tools-linux-x64-gnu` contains only `runtime/bin/pg_dump` + and `runtime/bin/psql`. +- On 2026-06-27, Linux-local CI evidence was refreshed from disposable + worktrees at `71407e43da72449f880bb9044b7f5449bbf7b53c`. `act` v0.2.89, + Docker 29.5.3, and `act -l` parsed CI, Release, and mobile E2E workflows. + The PR-shaped `release-intent` job succeeded. The `affected` job completed + CI planning, emitted the builder job set, and produced `check_count=21`, + `policy_count=64`, and `test_count=7`, then failed only when + `actions/upload-artifact@v7` hit the local `act` artifact server with + `unknown field "mime_type"`. Current upstream `nektos/act` issues document + the same protocol mismatch, so Linux-local `act` still cannot prove + downstream artifact-dependent CI jobs without either upstream support or a + deliberate local-only artifact handoff. - On 2026-06-27, current local-runner research and local checks still support `act` as the pragmatic Linux GitHub Actions runner for this repository: the upstream `nektos/act` project describes running workflows locally through From 1f8a584ff90be1e43499f48bc490eb8bcf26e214 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 08:40:18 +0000 Subject: [PATCH 170/308] refactor: derive python release metadata from bun graph --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 20 +++ .../examples-ci-release-validation.md | 11 ++ tools/release/product_metadata.py | 145 +++--------------- 3 files changed, 52 insertions(+), 124 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index d2949bea..826fca06 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -115,6 +115,26 @@ until the current-state gates here are checked with fresh local evidence. open because it should run from a committed disposable worktree, and this evidence does not claim macOS, Windows, iOS, or Android device/simulator lanes are validated by Linux-local `act`. +- 2026-06-27: Reduced the remaining Python release compatibility layer in + `tools/release/product_metadata.py`. Version files, changelog paths, tag + prefixes, derived version files, and extension artifact target rows now read + from the canonical Bun `release_graph_query.mjs` output instead of carrying a + second Python `release-please-config.json` parser and a bespoke + `extension-targets` subprocess path. `tools/dev/bun.sh + tools/policy/check-python-entrypoints.mjs --json` now reports + `product_metadata.py` at 987 lines while the remaining tracked Python surface + stays limited to the nine explicit release/extension-modeling files. Fresh + checks passed: `python3 -m py_compile` for all remaining Python release and + policy helpers, `tools/dev/bun.sh tools/release/release_graph_query.mjs + graph`, a targeted `product_metadata` API smoke, `python3 + tools/release/check_release_metadata.py`, `python3 + tools/release/check_artifact_targets.py`, focused `python3 + tools/release/check_consumer_shape.py --products-json ...`, `python3 + tools/policy/check-release-policy.py`, `bash + tools/policy/check-tooling-stack.sh`, `tools/release/release.py check`, + `tools/release/local_registry_publish.py publish --surface cargo --strict`, + `tools/release/local_registry_publish.py publish --surface npm --strict`, + `bash tools/policy/check-docs.sh`, and `git diff --check`. - 2026-06-27: Hardened the helper dead-code scanner so low-reference candidates account for path-suffix references as well as full-path and basename references. This avoids treating nested helpers as weaker candidates diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index 808072f9..68637d17 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -133,6 +133,17 @@ the release/tooling surface after the runtime tool crate split. tools/policy/check-rust-helper-crates.mjs --list` verified the only Rust helper crates are `tools/perf/runner` and `tools/xtask`, both retained as domain tools. +- On 2026-06-27, the Python release compatibility layer was narrowed further. + `tools/release/product_metadata.py` no longer parses + `release-please-config.json` for version files, changelog paths, or tag + prefixes, and its extension-target lookup now uses the same cached Bun + `release_graph_query.mjs` helper as other artifact target reads. The tracked + Python inventory remains nine files, with `product_metadata.py` reduced to + 987 lines. Fresh checks passed for Python compile, release graph output, + targeted product metadata reads, release metadata, artifact targets, focused + consumer-shape checks, release policy, tooling-stack policy, + `tools/release/release.py check`, strict local Cargo publication, strict + local npm publication, docs policy, and `git diff --check`. - On 2026-06-27, the stale direct `tools/release/product_metadata.py version` CLI was retired. Product version reads remain on the Bun helper `tools/release/product-version.mjs`, and direct execution of diff --git a/tools/release/product_metadata.py b/tools/release/product_metadata.py index e70f6d88..821a4f10 100644 --- a/tools/release/product_metadata.py +++ b/tools/release/product_metadata.py @@ -20,7 +20,6 @@ ROOT = Path(__file__).resolve().parents[2] -RELEASE_PLEASE_CONFIG_PATH = ROOT / "release-please-config.json" EXTENSION_CLASSES = {"contrib", "external", "first-party"} EXTENSION_VERSIONING_BY_CLASS = { "contrib": "postgres-bound", @@ -60,15 +59,6 @@ def fail(message: str) -> NoReturn: raise SystemExit(2) -def _read_json(path: Path) -> dict[str, Any]: - if not path.is_file(): - fail(f"missing {path.relative_to(ROOT)}") - value = json.loads(path.read_text(encoding="utf-8")) - if not isinstance(value, dict): - fail(f"{path.relative_to(ROOT)} must contain a JSON object") - return value - - def _read_toml(path: Path) -> dict[str, Any]: if not path.is_file(): fail(f"missing {path.relative_to(ROOT)}") @@ -78,39 +68,6 @@ def _read_toml(path: Path) -> dict[str, Any]: return value -@lru_cache(maxsize=1) -def _release_please_config() -> dict[str, Any]: - return _read_json(RELEASE_PLEASE_CONFIG_PATH) - - -@lru_cache(maxsize=1) -def _packages() -> dict[str, dict[str, Any]]: - packages = _release_please_config().get("packages") - if not isinstance(packages, dict) or not packages: - fail("release-please-config.json must define packages") - parsed: dict[str, dict[str, Any]] = {} - for package_path, package_config in packages.items(): - if not isinstance(package_path, str) or not package_path: - fail("release-please package paths must be non-empty strings") - if not isinstance(package_config, dict): - fail(f"{package_path} release-please config must be an object") - parsed[package_path] = package_config - return parsed - - -@lru_cache(maxsize=1) -def _release_please_packages_by_component() -> dict[str, tuple[str, dict[str, Any]]]: - packages: dict[str, tuple[str, dict[str, Any]]] = {} - for package_path, package_config in _packages().items(): - component = package_config.get("component") - if not isinstance(component, str) or not component: - fail(f"{package_path}.component must be a non-empty string") - if component in packages: - fail(f"duplicate release-please component {component}") - packages[component] = (package_path, package_config) - return packages - - def package_path(product: str) -> str: value = product_config(product).get("path") if not isinstance(value, str) or not value: @@ -131,20 +88,6 @@ def moon_release_metadata(product: str) -> dict[str, Any]: return release -def _package_config(product: str) -> dict[str, Any]: - package = _release_please_packages_by_component().get(product) - if package is None: - fail(f"unknown release-please component {product!r}") - package_path_from_release_please, config = package - moon_package_path = package_path(product) - if package_path_from_release_please != moon_package_path: - fail( - f"{product} release-please path {package_path_from_release_please!r} must match " - f"Moon package path {moon_package_path!r}" - ) - return config - - def _release_metadata_path(product: str) -> Path: return ROOT / package_path(product) / "release.toml" @@ -600,23 +543,14 @@ def extension_artifact_targets( family: str | None = None, published_only: bool = False, ) -> tuple[SimpleNamespace, ...]: - args = ["tools/dev/bun.sh", "tools/release/release_graph_query.mjs", "extension-targets"] + args: list[str] = [] if product is not None: args.extend(["--product", product]) if family is not None: args.extend(["--family", family]) if published_only: args.append("--published-only") - try: - output = subprocess.check_output(args, cwd=ROOT, text=True, stderr=subprocess.PIPE) - except subprocess.CalledProcessError as error: - detail = (error.stderr or "").strip() - if detail: - fail(f"release graph extension target query failed: {detail}") - fail(f"release graph extension target query failed with exit code {error.returncode}") - rows = json.loads(output) - if not isinstance(rows, list) or not all(isinstance(row, dict) for row in rows): - fail("release graph extension-targets query must return a JSON object list") + rows = _release_graph_query_rows("extension-targets", tuple(args)) return tuple(SimpleNamespace(**row) for row in rows) @@ -849,48 +783,22 @@ def validate_all_extension_metadata(graph: dict | None = None) -> None: validate_extension_metadata(product, graph) -def _package_relative_path(product: str, relative: str, context: str) -> str: - path = Path(relative) - if path.is_absolute() or ".." in path.parts: - fail(f"{context} must stay inside release package path: {relative!r}") - return (Path(package_path(product)) / path).as_posix() - - -def _canonical_version_file(product: str) -> str: - package_config = _package_config(product) - release_type = package_config.get("release-type") - version_file = package_config.get("version-file") - if isinstance(version_file, str) and version_file: - return _package_relative_path(product, version_file, f"{product}.version-file") - if release_type == "rust": - return _package_relative_path(product, "Cargo.toml", f"{product}.rust") - if release_type in {"node", "expo"}: - return _package_relative_path(product, "package.json", f"{product}.node") - fail(f"{product} release-please config must declare version-file for release type {release_type!r}") - - -def _extra_version_files(product: str) -> list[str]: - files: list[str] = [] - package_config = _package_config(product) - extra_files = package_config.get("extra-files", []) - if not isinstance(extra_files, list): - fail(f"{product}.extra-files must be a list") - for index, entry in enumerate(extra_files): - context = f"{product}.extra-files[{index}]" - if isinstance(entry, str): - files.append(_package_relative_path(product, 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") - files.append(_package_relative_path(product, path, f"{context}.path")) - return files +def _graph_string(config: dict[str, Any], key: str, product: str) -> str: + value = config.get(key) + if not isinstance(value, str) or not value: + fail(f"release graph product {product}.{key} must be a non-empty string") + return value + + +def _graph_string_list(config: dict[str, Any], key: str, product: str) -> list[str]: + value = config.get(key) + if not isinstance(value, list) or not value or not all(isinstance(item, str) and item for item in value): + fail(f"release graph product {product}.{key} must be a non-empty string list") + return list(value) def version_files(product: str, graph: dict | None = None) -> list[str]: - files = [_canonical_version_file(product), *_extra_version_files(product)] + files = _graph_string_list(product_config(product, graph), "version_files", product) for path in files: if not (ROOT / path).is_file(): fail(f"{product} version file does not exist: {path}") @@ -898,32 +806,21 @@ def version_files(product: str, graph: dict | None = None) -> list[str]: def derived_version_files(product: str, graph: dict | None = None) -> list[str]: - return string_list(_release_metadata(product), "derived_version_files", product) + value = product_config(product, graph).get("derived_version_files", []) + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + fail(f"release graph product {product}.derived_version_files must be a string list") + return list(value) def changelog_path(product: str, graph: dict | None = None) -> str: - package_config = _package_config(product) - relative = package_config.get("changelog-path", "CHANGELOG.md") - if not isinstance(relative, str) or not relative: - fail(f"{product}.changelog-path must be a non-empty string") - path = _package_relative_path(product, relative, f"{product}.changelog-path") + path = _graph_string(product_config(product, graph), "changelog_path", product) if not (ROOT / path).is_file(): fail(f"{product} changelog does not exist: {path}") return path def tag_prefix(product: str, graph: dict | None = None) -> str: - config = _release_please_config() - package_config = _package_config(product) - component = package_config.get("component") - if component != product: - fail(f"{product} release-please component must match product id") - if config.get("include-v-in-tag") is not True: - fail("release-please must include v in product tags") - separator = config.get("tag-separator") - if separator != "-": - fail("release-please tag-separator must be '-'") - return f"{product}{separator}v" + return _graph_string(product_config(product, graph), "tag_prefix", product) def parser_for_version_file(product: str, path: str) -> str: From 5a7322bc37321b0e737ee14fff84e19796b913fc Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 09:30:23 +0000 Subject: [PATCH 171/308] fix: isolate registry-backed examples --- examples/electron-wasix/package.json | 2 +- examples/electron-wasix/pnpm-workspace.yaml | 11 + examples/electron-wasix/src-wasix/Cargo.lock | 20 +- examples/electron/package.json | 2 +- examples/electron/pnpm-workspace.yaml | 11 + examples/tauri-wasix/package.json | 2 +- examples/tauri-wasix/pnpm-workspace.yaml | 10 + examples/tauri-wasix/src-tauri/Cargo.lock | 22 +- examples/tauri/package.json | 2 +- examples/tauri/pnpm-workspace.yaml | 10 + examples/tauri/src-tauri/Cargo.lock | 52 +- examples/tools/check-examples.mjs | 26 +- examples/tools/run-electron-driver-smoke.sh | 123 ++- examples/tools/run-tauri-webdriver-smoke.sh | 2 +- examples/tools/with-local-registries.sh | 1 + pnpm-lock.yaml | 729 +----------------- pnpm-workspace.yaml | 4 - .../tauri-sqlx-vanilla/src-tauri/Cargo.lock | 22 +- 18 files changed, 267 insertions(+), 784 deletions(-) create mode 100644 examples/electron-wasix/pnpm-workspace.yaml create mode 100644 examples/electron/pnpm-workspace.yaml create mode 100644 examples/tauri-wasix/pnpm-workspace.yaml create mode 100644 examples/tauri/pnpm-workspace.yaml diff --git a/examples/electron-wasix/package.json b/examples/electron-wasix/package.json index 35c3a854..a2729c19 100644 --- a/examples/electron-wasix/package.json +++ b/examples/electron-wasix/package.json @@ -16,7 +16,7 @@ "@types/node": "^24.10.1", "@types/pg": "^8.15.6", "electron": "^39.2.5", - "typescript": "catalog:", + "typescript": "^5.9.3", "vite": "^6.0.3" } } diff --git a/examples/electron-wasix/pnpm-workspace.yaml b/examples/electron-wasix/pnpm-workspace.yaml new file mode 100644 index 00000000..95321cf2 --- /dev/null +++ b/examples/electron-wasix/pnpm-workspace.yaml @@ -0,0 +1,11 @@ +packages: + - "." + +minimumReleaseAge: 1440 +autoInstallPeers: false +updateNotifier: false +verifyDepsBeforeRun: false + +allowBuilds: + electron: true + esbuild: true diff --git a/examples/electron-wasix/src-wasix/Cargo.lock b/examples/electron-wasix/src-wasix/Cargo.lock index b2440035..57cea178 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 = "b9ac07fe50bbf572ac9416f716e34e573f73c75087cb8d0dc191cf97b480f4fc" 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 = "c8d0735405cc50843b67768d967efc19379c4463ec281b2aca5f3341b35793c7" 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 = "5fd2f2dbd950f455b4c291ea2d167d05630da35abff4c4539f5a54c1faba9ab3" 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 = "42ca786d09b8abea189ad69223910a885b2242e284b79aaba5f87bb27a78b482" 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 = "a813fb560bf766f17233f41ae60abd7463dd6a13b019792b614550c64be77e29" +checksum = "8b3d9c241cb2b4e1204551e8c165fd20699033b8b9787d780b322177a3043345" 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 = "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 = "de3740322fd9e45afb920dde3719519dd887d542a1dbb63d681c56cb22efc394" 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 = "d2f4564e0ba42fdb0ec0ccde6652a856af4d074d3fd05be45935e11fa483538e" 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 = "98dc7362843ca0b98c4eb327784b2da8bc3df79b5b3cca28fbf58ab95885d308" 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 = "fa8e29897165555820f439532fc2ef1f4e25464ab5bbefdab9674b2d02198d2b" dependencies = [ "serde_json", "sha2 0.10.9", diff --git a/examples/electron/package.json b/examples/electron/package.json index dc687cb1..498df1bc 100644 --- a/examples/electron/package.json +++ b/examples/electron/package.json @@ -20,7 +20,7 @@ "@types/node": "^24.10.1", "@types/pg": "^8.15.6", "electron": "^39.2.5", - "typescript": "catalog:", + "typescript": "^5.9.3", "vite": "^6.0.3" } } diff --git a/examples/electron/pnpm-workspace.yaml b/examples/electron/pnpm-workspace.yaml new file mode 100644 index 00000000..95321cf2 --- /dev/null +++ b/examples/electron/pnpm-workspace.yaml @@ -0,0 +1,11 @@ +packages: + - "." + +minimumReleaseAge: 1440 +autoInstallPeers: false +updateNotifier: false +verifyDepsBeforeRun: false + +allowBuilds: + electron: true + esbuild: true diff --git a/examples/tauri-wasix/package.json b/examples/tauri-wasix/package.json index d513d048..267f5db3 100644 --- a/examples/tauri-wasix/package.json +++ b/examples/tauri-wasix/package.json @@ -14,7 +14,7 @@ }, "devDependencies": { "@tauri-apps/cli": "^2", - "typescript": "catalog:", + "typescript": "^5.9.3", "vite": "^6.0.3" } } diff --git a/examples/tauri-wasix/pnpm-workspace.yaml b/examples/tauri-wasix/pnpm-workspace.yaml new file mode 100644 index 00000000..4f6ad997 --- /dev/null +++ b/examples/tauri-wasix/pnpm-workspace.yaml @@ -0,0 +1,10 @@ +packages: + - "." + +minimumReleaseAge: 1440 +autoInstallPeers: false +updateNotifier: false +verifyDepsBeforeRun: false + +allowBuilds: + esbuild: true diff --git a/examples/tauri-wasix/src-tauri/Cargo.lock b/examples/tauri-wasix/src-tauri/Cargo.lock index 739ffce6..9250cdbf 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 = "b9ac07fe50bbf572ac9416f716e34e573f73c75087cb8d0dc191cf97b480f4fc" 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 = "c8d0735405cc50843b67768d967efc19379c4463ec281b2aca5f3341b35793c7" 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 = "5fd2f2dbd950f455b4c291ea2d167d05630da35abff4c4539f5a54c1faba9ab3" 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 = "42ca786d09b8abea189ad69223910a885b2242e284b79aaba5f87bb27a78b482" 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 = "a813fb560bf766f17233f41ae60abd7463dd6a13b019792b614550c64be77e29" +checksum = "8b3d9c241cb2b4e1204551e8c165fd20699033b8b9787d780b322177a3043345" 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 = "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 = "de3740322fd9e45afb920dde3719519dd887d542a1dbb63d681c56cb22efc394" 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 = "d2f4564e0ba42fdb0ec0ccde6652a856af4d074d3fd05be45935e11fa483538e" 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 = "98dc7362843ca0b98c4eb327784b2da8bc3df79b5b3cca28fbf58ab95885d308" 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 = "fa8e29897165555820f439532fc2ef1f4e25464ab5bbefdab9674b2d02198d2b" dependencies = [ "serde_json", "sha2 0.10.9", @@ -5432,7 +5432,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.4", "once_cell", "rustix", "windows-sys 0.61.2", diff --git a/examples/tauri/package.json b/examples/tauri/package.json index b5a621be..a89c8722 100644 --- a/examples/tauri/package.json +++ b/examples/tauri/package.json @@ -14,7 +14,7 @@ }, "devDependencies": { "@tauri-apps/cli": "^2", - "typescript": "catalog:", + "typescript": "^5.9.3", "vite": "^6.0.3" } } diff --git a/examples/tauri/pnpm-workspace.yaml b/examples/tauri/pnpm-workspace.yaml new file mode 100644 index 00000000..4f6ad997 --- /dev/null +++ b/examples/tauri/pnpm-workspace.yaml @@ -0,0 +1,10 @@ +packages: + - "." + +minimumReleaseAge: 1440 +autoInstallPeers: false +updateNotifier: false +verifyDepsBeforeRun: false + +allowBuilds: + esbuild: true diff --git a/examples/tauri/src-tauri/Cargo.lock b/examples/tauri/src-tauri/Cargo.lock index 44eaf6e5..52dae76c 100644 --- a/examples/tauri/src-tauri/Cargo.lock +++ b/examples/tauri/src-tauri/Cargo.lock @@ -1714,9 +1714,15 @@ 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 = "339fb30e364733e12d691126243e8cf6e17472cf7f0625e69ba0b2d7ed296e4e" +checksum = "dbbed43b4d8c1a57433def7020f33c01a2b10eba72edfad7b77c80be516e8eb8" 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", ] @@ -1724,7 +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 = "a19bbcad796b3aaee8a3ba3c0f4f46d7a148aa5e7958ca57498a4837f0c06d4a" +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" [[package]] name = "libredox" @@ -2102,7 +2144,7 @@ dependencies = [ name = "oliphaunt" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "bf38854611fdfe97264113f7746b4a7cd61e7fb8b2346e4436e5bca1fa4ba8da" +checksum = "257632f4bc615373db636064ee35adfc3d047e35de7e16eff864337ef0a791d1" dependencies = [ "crossbeam-channel", "flate2", @@ -2188,7 +2230,7 @@ dependencies = [ name = "oliphaunt-tools" version = "0.1.0" source = "registry+file:///home/sid/dev/pglite-oxide/target/local-registries/cargo/index" -checksum = "d03f050c7e2307a0b41a082369ab69f2da478d65f5cfd26ec30bf56816333c82" +checksum = "b54e77e320cbf1428d26f09f71a1fd87d695c97f36ee9813f3cecc9aecd6f974" dependencies = [ "oliphaunt-tools-linux-x64-gnu", ] @@ -2207,7 +2249,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 = "834e7c11f46fb5b5f87cefca8106ce533eba734ace5818e21d011be92fbacdaf" +checksum = "c069918c5c037a145fc0b0453f7f90ea06a26556344b3b096c3ab09f82864c03" [[package]] name = "once_cell" diff --git a/examples/tools/check-examples.mjs b/examples/tools/check-examples.mjs index ca2920b0..2eadbd34 100644 --- a/examples/tools/check-examples.mjs +++ b/examples/tools/check-examples.mjs @@ -126,27 +126,45 @@ requireFile("examples/tools/electron-test-driver.mjs"); requireText("examples/tools/run-tauri-webdriver-smoke.sh", String.raw`cargo install tauri-driver --locked --version 2\.0\.6`); requireText( "examples/tools/run-tauri-webdriver-smoke.sh", - String.raw`pnpm --filter "\./\$app_dir" install --no-frozen-lockfile`, + String.raw`pnpm --dir "\$app_dir" install --no-frozen-lockfile`, ); requireText( "examples/tools/run-electron-driver-smoke.sh", - String.raw`pnpm --filter "\./\$app_dir" install --no-frozen-lockfile`, + String.raw`pnpm --dir "\$app_dir" install --no-frozen-lockfile`, ); requireText( "examples/tools/run-electron-driver-smoke.sh", String.raw`assert_npm_package "@oliphaunt/tools-linux-x64-gnu" "0\.1\.0"`, ); +requireText("examples/tools/run-electron-driver-smoke.sh", String.raw`OLIPHAUNT_WASIX_TODO_SIDECAR`); +requireText("examples/tools/run-electron-driver-smoke.sh", String.raw`src-wasix/Cargo\.toml`); requireText("examples/tools/tauri-webdriver-smoke.mjs", "tauri webdriver todo smoke passed"); requireText("examples/tools/electron-driver-smoke.mjs", "electron driver todo smoke passed"); requireText("examples/tools/electron-test-driver.mjs", "installElectronTodoTestDriver"); +rejectText("pnpm-workspace.yaml", '"examples/electron"'); +rejectText("pnpm-workspace.yaml", '"examples/tauri"'); +rejectText("pnpm-workspace.yaml", '"examples/tauri-wasix"'); +rejectText("pnpm-workspace.yaml", '"examples/electron-wasix"'); +rejectText("pnpm-lock.yaml", "examples/electron:"); +rejectText("pnpm-lock.yaml", "examples/tauri:"); +rejectText("pnpm-lock.yaml", "examples/tauri-wasix:"); +rejectText("pnpm-lock.yaml", "examples/electron-wasix:"); for (const example of ["tauri", "tauri-wasix", "electron", "electron-wasix"]) { requireFile(`examples/${example}/package.json`); + requireFile(`examples/${example}/pnpm-workspace.yaml`); requireFile(`examples/${example}/README.md`); requireFile(`examples/${example}/.npmrc`); requireText(`examples/${example}/.npmrc`, String.raw`^registry=http://127\.0\.0\.1:4873/$`); requireText(`examples/${example}/.npmrc`, String.raw`^link-workspace-packages=false$`); requireText(`examples/${example}/.npmrc`, String.raw`^prefer-workspace-packages=false$`); } +for (const example of ["electron", "electron-wasix"]) { + requireText(`examples/${example}/pnpm-workspace.yaml`, String.raw`electron: true`); + requireText(`examples/${example}/pnpm-workspace.yaml`, String.raw`esbuild: true`); +} +for (const example of ["tauri", "tauri-wasix"]) { + requireText(`examples/${example}/pnpm-workspace.yaml`, String.raw`esbuild: true`); +} requireFile("examples/tauri/src-tauri/Cargo.toml"); requireFile("examples/tauri-wasix/src-tauri/Cargo.toml"); requireFile("examples/electron-wasix/src-wasix/Cargo.toml"); @@ -202,6 +220,10 @@ rejectText( String.raw`tcp_addr\(\)\.is_none\(\)`, ); rejectText("examples/electron/package.json", '"@oliphaunt/ts": "workspace:\\*"'); +rejectText("examples/electron/package.json", '"typescript": "catalog:"'); +rejectText("examples/tauri/package.json", '"typescript": "catalog:"'); +rejectText("examples/tauri-wasix/package.json", '"typescript": "catalog:"'); +rejectText("examples/electron-wasix/package.json", '"typescript": "catalog:"'); rejectText("examples/tauri/src-tauri/Cargo.toml", 'path = "../../../src/sdks/rust'); rejectText("examples/tauri-wasix/src-tauri/Cargo.toml", 'path = "../../../src/bindings/wasix-rust'); rejectText("examples/electron-wasix/src-wasix/Cargo.toml", 'path = "../../../src/bindings/wasix-rust'); diff --git a/examples/tools/run-electron-driver-smoke.sh b/examples/tools/run-electron-driver-smoke.sh index a1345250..036f12bc 100755 --- a/examples/tools/run-electron-driver-smoke.sh +++ b/examples/tools/run-electron-driver-smoke.sh @@ -26,12 +26,22 @@ 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' + local resolver_package="${3:-}" + examples/tools/with-local-registries.sh pnpm --dir "$app_dir" exec node - "$package_name" "$expected_version" "$resolver_package" <<'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 [packageName, expectedVersion, resolverPackage] = process.argv.slice(2); +const resolvePaths = [process.cwd()]; +if (resolverPackage) { + const resolverPackageJson = require.resolve(`${resolverPackage}/package.json`, { + paths: [process.cwd()], + }); + resolvePaths.unshift(path.dirname(resolverPackageJson)); +} +const packageJson = require.resolve(`${packageName}/package.json`, { + paths: resolvePaths, +}); const data = JSON.parse(fs.readFileSync(packageJson, 'utf8')); if (data.version !== expectedVersion) { throw new Error(`${packageName} resolved version ${data.version}, expected ${expectedVersion}`); @@ -43,24 +53,119 @@ if (!normalized.includes('/node_modules/')) { NODE } -electron="$root/node_modules/electron/dist/electron" +electron_relative_path() { + local platform="$1" + local arch="$2" + case "$platform/$arch" in + linux/*) + printf '%s\n' "electron" + ;; + darwin/*) + printf '%s\n' "Electron.app/Contents/MacOS/Electron" + ;; + win32/*) + printf '%s\n' "electron.exe" + ;; + *) + fail "unsupported Electron e2e platform: $platform/$arch" + ;; + esac +} + +repair_electron_install() { + local electron_pkg="$1" + local platform="$2" + local arch="$3" + local relative_path="$4" + local electron_path="$electron_pkg/dist/$relative_path" + + if [ -x "$electron_path" ]; then + return + fi + command -v unzip >/dev/null 2>&1 || fail "missing unzip required to repair Electron binary install" + + local version + version="$(node -e 'process.stdout.write(require(process.argv[1]).version)' "$electron_pkg/package.json")" + local archive_name="electron-v$version-$platform-$arch.zip" + local archive="" + for cache_root in "${electron_config_cache:-}" "$HOME/.cache/electron"; do + if [ -n "$cache_root" ] && [ -d "$cache_root" ]; then + archive="$(find "$cache_root" -name "$archive_name" -type f | sort | tail -n 1)" + [ -n "$archive" ] && break + fi + done + if [ -z "$archive" ]; then + fail "Electron installed without $relative_path and cached $archive_name was not found" + fi + + rm -rf "$electron_pkg/dist" + mkdir -p "$electron_pkg/dist" + unzip -q "$archive" -d "$electron_pkg/dist" + printf '%s' "$relative_path" > "$electron_pkg/path.txt" + if [ -f "$electron_pkg/dist/electron.d.ts" ]; then + mv "$electron_pkg/dist/electron.d.ts" "$electron_pkg/electron.d.ts" + fi +} + +wasix_sidecar_env=() +prepare_wasix_sidecar() { + if [ ! -f "$app_dir/src-wasix/Cargo.toml" ]; then + return + fi + + local scratch="$root/target/e2e/electron-sidecars/${app_dir//\//-}" + rm -rf "$scratch" + mkdir -p "$scratch" + cp -R "$root/$app_dir/src-wasix/." "$scratch/" + rm -f "$scratch/Cargo.lock" + + examples/tools/with-local-registries.sh cargo build \ + --quiet \ + --manifest-path "$scratch/Cargo.toml" \ + --target-dir "$scratch/target" + + local package_name + package_name="$( + awk -F'"' ' + $0 ~ /^\[package\]/ { in_package = 1; next } + $0 ~ /^\[/ && $0 !~ /^\[package\]/ { in_package = 0 } + in_package && $1 ~ /^name = / { print $2; exit } + ' "$scratch/Cargo.toml" + )" + if [ -z "$package_name" ]; then + fail "could not read package name from $scratch/Cargo.toml" + fi + local sidecar="$scratch/target/debug/$package_name" + if [ ! -x "$sidecar" ]; then + fail "missing built WASIX sidecar: $sidecar" + fi + wasix_sidecar_env=("OLIPHAUNT_WASIX_TODO_SIDECAR=$sidecar") +} + +examples/tools/with-local-registries.sh pnpm --dir "$app_dir" install --no-frozen-lockfile +electron_pkg="$root/$app_dir/node_modules/electron" +electron_platform="$(node -p 'process.platform')" +electron_arch="$(node -p 'process.arch')" +electron_relative="$(electron_relative_path "$electron_platform" "$electron_arch")" +repair_electron_install "$electron_pkg" "$electron_platform" "$electron_arch" "$electron_relative" +electron="$electron_pkg/dist/$electron_relative" if [ ! -x "$electron" ]; then - fail "missing Electron executable at $electron; run pnpm install" + fail "missing Electron executable at $electron after example 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/liboliphaunt-linux-x64-gnu" "0.1.0" "@oliphaunt/ts" + assert_npm_package "@oliphaunt/tools-linux-x64-gnu" "0.1.0" "@oliphaunt/ts" assert_npm_package "@oliphaunt/extension-hstore" "0.1.0" fi examples/tools/with-local-registries.sh pnpm --dir "$app_dir" build +prepare_wasix_sidecar run_smoke=( env "OLIPHAUNT_E2E_ELECTRON=$electron" "OLIPHAUNT_E2E_ELECTRON_APP=$root/$app_dir" + "${wasix_sidecar_env[@]}" examples/tools/with-local-registries.sh node "$root/examples/tools/electron-driver-smoke.mjs" diff --git a/examples/tools/run-tauri-webdriver-smoke.sh b/examples/tools/run-tauri-webdriver-smoke.sh index 88691494..086c7396 100755 --- a/examples/tools/run-tauri-webdriver-smoke.sh +++ b/examples/tools/run-tauri-webdriver-smoke.sh @@ -30,7 +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" install --no-frozen-lockfile examples/tools/with-local-registries.sh pnpm --dir "$app_dir" tauri build --debug package_name="$( diff --git a/examples/tools/with-local-registries.sh b/examples/tools/with-local-registries.sh index 0d557261..7a128e29 100755 --- a/examples/tools/with-local-registries.sh +++ b/examples/tools/with-local-registries.sh @@ -34,5 +34,6 @@ export PNPM_CONFIG_MINIMUM_RELEASE_AGE=0 export PNPM_CONFIG_LOCKFILE=false export PNPM_CONFIG_STORE_DIR="$root/target/local-registries/pnpm-store" export PNPM_CONFIG_PREFER_OFFLINE=false +export electron_config_cache="$root/target/local-registries/electron-cache" exec "$@" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dcae7227..636631ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,100 +26,6 @@ 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: 0.1.0 - version: 0.1.0 - 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 - 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: - 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 - 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': @@ -896,10 +802,6 @@ 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==} @@ -1807,73 +1709,6 @@ 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'} @@ -2500,20 +2335,12 @@ 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==} @@ -2701,9 +2528,6 @@ 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==} @@ -2728,9 +2552,6 @@ 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==} @@ -2746,9 +2567,6 @@ 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==} @@ -2764,9 +2582,6 @@ 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: @@ -2781,9 +2596,6 @@ 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==} @@ -2796,9 +2608,6 @@ 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} @@ -3228,9 +3037,6 @@ packages: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} - boolean@3.2.0: - resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} - bplist-creator@0.1.0: resolution: {integrity: sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==} @@ -3261,9 +3067,6 @@ 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==} @@ -3271,14 +3074,6 @@ 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'} @@ -3366,9 +3161,6 @@ 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'} @@ -3522,10 +3314,6 @@ 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==} @@ -3536,10 +3324,6 @@ 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'} @@ -3567,9 +3351,6 @@ 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==} @@ -3593,11 +3374,6 @@ 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==} @@ -3609,9 +3385,6 @@ 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'} @@ -3624,10 +3397,6 @@ 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==} @@ -3666,9 +3435,6 @@ 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==} @@ -4101,11 +3867,6 @@ 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==} @@ -4132,9 +3893,6 @@ 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'} @@ -4218,10 +3976,6 @@ 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} @@ -4371,10 +4125,6 @@ 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'} @@ -4396,16 +4146,13 @@ 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: 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'} @@ -4422,10 +4169,6 @@ 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==} @@ -4525,17 +4268,10 @@ 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'} @@ -4819,9 +4555,6 @@ 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 @@ -4831,9 +4564,6 @@ 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 @@ -4849,10 +4579,6 @@ 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 @@ -4969,10 +4695,6 @@ 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==} @@ -5018,10 +4740,6 @@ 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'} @@ -5286,14 +5004,6 @@ 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'} @@ -5440,10 +5150,6 @@ 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} @@ -5531,10 +5237,6 @@ 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'} @@ -5585,43 +5287,6 @@ 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==} @@ -5660,22 +5325,6 @@ 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'} @@ -5712,9 +5361,6 @@ 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'} @@ -5734,10 +5380,6 @@ 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'} @@ -5978,9 +5620,6 @@ 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'} @@ -6005,17 +5644,10 @@ 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'} @@ -6057,9 +5689,6 @@ 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 @@ -6081,10 +5710,6 @@ 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'} @@ -6210,13 +5835,6 @@ 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==} @@ -6324,10 +5942,6 @@ 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'} @@ -6429,10 +6043,6 @@ 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'} @@ -6544,10 +6154,6 @@ 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'} @@ -6595,6 +6201,7 @@ 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: @@ -6808,10 +6415,6 @@ 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'} @@ -6832,9 +6435,6 @@ 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'} @@ -7380,20 +6980,6 @@ 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 @@ -8300,56 +7886,6 @@ 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': {} @@ -9141,18 +8677,12 @@ 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 @@ -9291,13 +8821,6 @@ 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 @@ -9323,8 +8846,6 @@ 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': @@ -9339,10 +8860,6 @@ 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 @@ -9359,12 +8876,6 @@ 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 @@ -9386,10 +8897,6 @@ 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': {} @@ -9400,11 +8907,6 @@ 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 @@ -9902,9 +9404,6 @@ snapshots: transitivePeerDependencies: - supports-color - boolean@3.2.0: - optional: true - bplist-creator@0.1.0: dependencies: stream-buffers: 2.2.0 @@ -9942,24 +9441,10 @@ 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 @@ -10051,10 +9536,6 @@ 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: {} @@ -10197,10 +9678,6 @@ 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: {} @@ -10209,8 +9686,6 @@ 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 @@ -10233,9 +9708,6 @@ snapshots: detect-node-es@1.1.0: {} - detect-node@2.1.0: - optional: true - devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -10258,24 +9730,12 @@ 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 @@ -10285,8 +9745,6 @@ snapshots: entities@6.0.1: {} - env-paths@2.2.1: {} - error-stack-parser@2.1.4: dependencies: stackframe: 1.3.4 @@ -10394,9 +9852,6 @@ 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 @@ -11054,16 +10509,6 @@ 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: {} @@ -11092,10 +10537,6 @@ 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 @@ -11175,12 +10616,6 @@ 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 @@ -11320,10 +10755,6 @@ 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 @@ -11357,16 +10788,6 @@ 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: {} @@ -11378,20 +10799,6 @@ 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: {} @@ -11560,8 +10967,6 @@ snapshots: html-void-elements@3.0.0: {} - http-cache-semantics@4.2.0: {} - http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -11570,11 +10975,6 @@ 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 @@ -11849,19 +11249,12 @@ 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 @@ -11880,8 +11273,6 @@ snapshots: kleur@3.0.3: {} - kysely@0.29.2: {} - lan-network@0.2.1: {} leven@3.1.0: {} @@ -11971,8 +11362,6 @@ snapshots: dependencies: js-tokens: 4.0.0 - lowercase-keys@2.0.0: {} - lru-cache@10.4.3: {} lru-cache@11.5.0: {} @@ -12020,11 +11409,6 @@ 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: @@ -12661,10 +12045,6 @@ 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: @@ -12785,8 +12165,6 @@ 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 @@ -12899,8 +12277,6 @@ 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 @@ -12950,43 +12326,6 @@ 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: {} @@ -13019,16 +12358,6 @@ 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: @@ -13067,11 +12396,6 @@ 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: {} @@ -13091,8 +12415,6 @@ snapshots: dependencies: inherits: 2.0.4 - quick-lru@5.1.1: {} - range-parser@1.2.1: {} raw-body@3.0.2: @@ -13514,8 +12836,6 @@ snapshots: require-from-string@2.0.2: {} - resolve-alpn@1.2.1: {} - resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -13540,25 +12860,11 @@ 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 @@ -13633,9 +12939,6 @@ snapshots: semiver@1.1.0: {} - semver-compare@1.0.0: - optional: true - semver@6.3.1: {} semver@7.8.1: {} @@ -13676,11 +12979,6 @@ 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 @@ -13849,11 +13147,6 @@ 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: {} @@ -13967,12 +13260,6 @@ 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 @@ -14062,9 +13349,6 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-fest@0.13.1: - optional: true - type-fest@0.21.3: {} type-fest@0.7.1: {} @@ -14193,8 +13477,6 @@ 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: @@ -14463,8 +13745,6 @@ snapshots: xmlbuilder@15.1.1: {} - xtend@4.0.2: {} - y18n@5.0.8: {} yallist@3.1.1: {} @@ -14483,11 +13763,6 @@ 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 eb499fc9..326ecb4d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -10,10 +10,6 @@ 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 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 fca68186..69b551b9 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 = "b9ac07fe50bbf572ac9416f716e34e573f73c75087cb8d0dc191cf97b480f4fc" 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 = "c8d0735405cc50843b67768d967efc19379c4463ec281b2aca5f3341b35793c7" 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 = "5fd2f2dbd950f455b4c291ea2d167d05630da35abff4c4539f5a54c1faba9ab3" 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 = "42ca786d09b8abea189ad69223910a885b2242e284b79aaba5f87bb27a78b482" 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 = "a813fb560bf766f17233f41ae60abd7463dd6a13b019792b614550c64be77e29" +checksum = "8b3d9c241cb2b4e1204551e8c165fd20699033b8b9787d780b322177a3043345" 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 = "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 = "de3740322fd9e45afb920dde3719519dd887d542a1dbb63d681c56cb22efc394" 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 = "d2f4564e0ba42fdb0ec0ccde6652a856af4d074d3fd05be45935e11fa483538e" 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 = "98dc7362843ca0b98c4eb327784b2da8bc3df79b5b3cca28fbf58ab95885d308" 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 = "fa8e29897165555820f439532fc2ef1f4e25464ab5bbefdab9674b2d02198d2b" dependencies = [ "serde_json", "sha2 0.10.9", @@ -5632,7 +5632,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.4", "once_cell", "rustix", "windows-sys 0.61.2", From 488ccf89fa593d8a01db6b4cff71b0d23ef21d5b Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 09:34:58 +0000 Subject: [PATCH 172/308] docs: record example registry validation --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 826fca06..0ce73947 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,28 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Isolated the registry-backed desktop examples from the root pnpm + workspace so root CI setup no longer resolves unpublished local-registry + example dependencies before Verdaccio is staged. Each root desktop example now + has its own one-package `pnpm-workspace.yaml`, keeps package-local + `pnpm --dir examples/... install` commands, and no longer uses root catalog + dependencies. Electron and Tauri smoke runners install from the example + directory; Electron resolves package-managed runtime/tool payloads from + `@oliphaunt/ts` and builds the WASIX sidecar from a scratch local-registry + Cargo lock to avoid stale same-version checksum state. Fresh checks passed: + root `pnpm install --frozen-lockfile`, `examples/tools/with-local-registries.sh + tools/dev/bun.sh tools/release/sync-example-lockfiles.mjs --check`, `bash + examples/tools/check-examples.sh`, `bash tools/policy/check-tooling-stack.sh`, + `bash tools/policy/check-docs.sh`, `bash + src/bindings/wasix-rust/tools/check-examples.sh`, `bash + src/bindings/wasix-rust/tools/check-package.sh`, + `tools/release/check_release_metadata.py`, + `tools/release/check_consumer_shape.py`, native Electron, WASIX Electron, + native Tauri, and WASIX Tauri GUI smokes. The strict dead-code scans were also + re-run after the fix; `tools/dev/bun.sh + tools/policy/list-helper-reference-candidates.mjs --max-refs 0` and + `tools/dev/bun.sh tools/policy/list-source-reference-candidates.mjs --max-refs + 0` both found no unreferenced tracked candidates. - 2026-06-27: Re-ran the complementary strict npm local-registry publication after the current Cargo split verification. Fresh check passed: `tools/release/local_registry_publish.py publish --surface npm --strict`. From 3bb42ccb0a867f7eb1ad2da7f9bd88495c56bbc7 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 09:49:27 +0000 Subject: [PATCH 173/308] refactor: derive wasix cargo packager contract --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 24 ++++++ .../fixtures/consumer-shape/products.json | 4 + tools/release/check_consumer_shape.py | 9 +-- tools/release/check_release_metadata.py | 4 + ...kage_liboliphaunt_wasix_cargo_artifacts.py | 81 +++++-------------- tools/release/product_metadata.py | 32 ++++++++ 6 files changed, 86 insertions(+), 68 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 0ce73947..2b6043da 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,30 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Removed another WASIX runtime/tools package-graph duplication from + the remaining Python compatibility layer. The WASIX Cargo artifact packager now + reads schema, runtime/tools/ICU package names, AOT target package maps, tool + payload files, forbidden root-runtime tools, and extension AOT target coverage + from the canonical Bun contract exposed by + `tools/release/wasix-cargo-artifact-contract.mjs` through + `product_metadata.py`; the packager keeps only local packaging mechanics such + as split thresholds and crate generation. Consumer-shape and release metadata + checks now require those accessors so the literal package/tool matrix cannot be + reintroduced in the packager. Fresh checks passed: `python3 -m py_compile` for + touched release helpers, a targeted packager/product-metadata contract import + parity smoke, `tools/dev/bun.sh tools/release/release_graph_query.mjs + wasix-cargo-artifact-contract`, `python3 + tools/release/check_release_metadata.py`, focused and full `python3 + tools/release/check_consumer_shape.py`, `python3 + tools/release/check_artifact_targets.py`, `tools/dev/bun.sh + tools/policy/check-wasix-release-dependency-invariants.mjs`, `bash + tools/policy/check-tooling-stack.sh`, `bash tools/policy/check-docs.sh`, `bash + examples/tools/check-examples.sh`, `tools/release/local_registry_publish.py + publish --surface cargo --strict`, `tools/release/release.py check`, and + `git diff --check`. A fresh sweep over 836 local-registry `.crate` files found + no crate above the 10 MiB crates.io limit; the largest crates were the split + WASIX PostGIS AOT part crates at 10,212,312 bytes, below the 10,485,760 byte + limit. - 2026-06-27: Isolated the registry-backed desktop examples from the root pnpm workspace so root CI setup no longer resolves unpublished local-registry example dependencies before Verdaccio is staged. Each root desktop example now diff --git a/src/shared/fixtures/consumer-shape/products.json b/src/shared/fixtures/consumer-shape/products.json index 696ee212..4c6652fe 100644 --- a/src/shared/fixtures/consumer-shape/products.json +++ b/src/shared/fixtures/consumer-shape/products.json @@ -42,6 +42,7 @@ "src/runtimes/liboliphaunt/wasix/release.toml", "src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml", "src/runtimes/liboliphaunt/wasix/crates/assets/README.md", + "tools/release/wasix-cargo-artifact-contract.mjs", "tools/release/package_liboliphaunt_wasix_cargo_artifacts.py" ], "requiredText": { @@ -59,6 +60,9 @@ "tools/release/package_liboliphaunt_wasix_cargo_artifacts.py": [ "CRATES_IO_MAX_BYTES", "validate_crate_size", + "product_metadata.wasix_cargo_artifact_schema()" + ], + "tools/release/wasix-cargo-artifact-contract.mjs": [ "oliphaunt-liboliphaunt-wasix-cargo-artifacts-v2" ] } diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 390ebf3f..29b3c6a4 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -1943,11 +1943,10 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: 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 "product_metadata.wasix_core_runtime_archive_files()" in wasix_packager_source + and "product_metadata.wasix_tools_payload_files()" in wasix_packager_source + and "product_metadata.wasix_forbidden_runtime_archive_tool_files()" in wasix_packager_source + and "product_metadata.wasix_tools_aot_artifacts()" 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 f75c4dac..7f98d1ad 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1502,6 +1502,10 @@ 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 "product_metadata.wasix_core_runtime_archive_files()" not in wasix_packager_source + or "product_metadata.wasix_tools_payload_files()" not in wasix_packager_source + or "product_metadata.wasix_forbidden_runtime_archive_tool_files()" not in wasix_packager_source + or "product_metadata.wasix_tools_aot_artifacts()" 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 publishable tools crates while keeping only postgres/initdb in root runtime crates") diff --git a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py index 8af03fd5..109b0f29 100644 --- a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py +++ b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py @@ -21,83 +21,38 @@ ROOT = Path(__file__).resolve().parents[2] PRODUCT = "liboliphaunt-wasix" -SCHEMA = "oliphaunt-liboliphaunt-wasix-cargo-artifacts-v2" +SCHEMA = product_metadata.wasix_cargo_artifact_schema() 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" -ICU_PAYLOAD_ARCHIVE = "icu-data.tar.zst" -TOOLS_PAYLOAD_FILES = ( - "bin/pg_dump.wasix.wasm", - "bin/psql.wasix.wasm", -) -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", -) -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", - "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"))', -} -EXPECTED_EXTENSION_AOT_TARGETS = frozenset(AOT_TARGET_TRIPLES.values()) +RUNTIME_PACKAGE = product_metadata.wasix_runtime_package_name() +TOOLS_PACKAGE = product_metadata.wasix_tools_package_name() +ICU_PACKAGE = product_metadata.wasix_icu_package_name() +ICU_PAYLOAD_ARCHIVE = product_metadata.wasix_icu_payload_archive_name() +TOOLS_PAYLOAD_FILES = product_metadata.wasix_tools_payload_files() +CORE_RUNTIME_ARCHIVE_FILES = product_metadata.wasix_core_runtime_archive_files() +FORBIDDEN_RUNTIME_ARCHIVE_TOOL_FILES = product_metadata.wasix_forbidden_runtime_archive_tool_files() +TOOLS_AOT_ARTIFACTS = product_metadata.wasix_tools_aot_artifacts() +AOT_PACKAGES = product_metadata.wasix_aot_packages() +TOOLS_AOT_PACKAGES = product_metadata.wasix_tools_aot_packages() +AOT_TARGET_TRIPLES = product_metadata.wasix_aot_target_triples() +AOT_TARGET_CFGS = product_metadata.wasix_aot_target_cfgs() +EXPECTED_EXTENSION_AOT_TARGETS = frozenset(product_metadata.wasix_expected_extension_aot_targets()) def public_cargo_package_names() -> tuple[str, ...]: - return ( - ICU_PACKAGE, - RUNTIME_PACKAGE, - TOOLS_PACKAGE, - *AOT_PACKAGES.values(), - *TOOLS_AOT_PACKAGES.values(), - ) + return product_metadata.wasix_public_cargo_package_names() def public_aot_cargo_dependencies() -> dict[str, str]: - return { - AOT_TARGET_CFGS[AOT_TARGET_TRIPLES[target]]: package - for target, package in AOT_PACKAGES.items() - } + return product_metadata.wasix_public_aot_cargo_dependencies() 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() - } + return product_metadata.wasix_public_tools_aot_cargo_dependencies() def public_tools_feature_dependencies() -> set[str]: - return { - f"dep:{TOOLS_PACKAGE}", - *(f"dep:{package}" for package in TOOLS_AOT_PACKAGES.values()), - } + return product_metadata.wasix_public_tools_feature_dependencies() @dataclass(frozen=True) diff --git a/tools/release/product_metadata.py b/tools/release/product_metadata.py index 821a4f10..8cfd7343 100644 --- a/tools/release/product_metadata.py +++ b/tools/release/product_metadata.py @@ -309,6 +309,38 @@ def wasix_cargo_artifact_schema() -> str: return _wasix_contract_string("schema") +def wasix_runtime_package_name() -> str: + return _wasix_contract_string("runtimePackage") + + +def wasix_tools_package_name() -> str: + return _wasix_contract_string("toolsPackage") + + +def wasix_icu_package_name() -> str: + return _wasix_contract_string("icuPackage") + + +def wasix_icu_payload_archive_name() -> str: + return _wasix_contract_string("icuPayloadArchive") + + +def wasix_aot_packages() -> dict[str, str]: + return _wasix_contract_string_map("aotPackages") + + +def wasix_tools_aot_packages() -> dict[str, str]: + return _wasix_contract_string_map("toolsAotPackages") + + +def wasix_aot_target_triples() -> dict[str, str]: + return _wasix_contract_string_map("aotTargetTriples") + + +def wasix_aot_target_cfgs() -> dict[str, str]: + return _wasix_contract_string_map("aotTargetCfgs") + + def wasix_public_cargo_package_names() -> tuple[str, ...]: return _wasix_contract_string_list("publicCargoPackageNames") From 646d44f0b4b5cb6425ed55d4c895f8b8c63923e1 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 10:09:28 +0000 Subject: [PATCH 174/308] refactor: centralize compatibility version metadata --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 32 ++++++++++ tools/release/check_release_metadata.py | 10 ++++ tools/release/product_metadata.py | 55 ++++++++---------- tools/release/release-graph.mjs | 58 +++++++++++++++++++ tools/release/release_graph_query.mjs | 16 +++++ tools/release/sync-release-pr.mjs | 45 +++----------- 6 files changed, 148 insertions(+), 68 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 2b6043da..ff36003e 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,38 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Moved compatibility-version metadata collection out of the + Python release compatibility layer and into the canonical Bun release graph. + `tools/release/release-graph.mjs` now exposes sorted + `compatibilityVersionEntries`, `tools/release/release_graph_query.mjs` + exposes `compatibility-version-entries [--require-source-product]`, + `tools/release/sync-release-pr.mjs` reuses the shared helper, and + `tools/release/product_metadata.py` only adapts the query rows to the legacy + tuple API. `tools/release/check_release_metadata.py` now rejects moving + `compatibility_versions` collection back to Python or reintroducing a separate + sync-release-pr implementation. A subagent review was attempted for the + remaining Python migration/dead-code pass, but the current session had reached + the agent thread limit, so this pass used local repo evidence instead. Strict + dead-code/reference scans still found no zero-reference helper or source + candidates. Fresh checks passed: `tools/dev/bun.sh + tools/release/release_graph_query.mjs compatibility-version-entries`, + `tools/dev/bun.sh tools/release/release_graph_query.mjs + compatibility-version-entries --require-source-product`, a Python + `product_metadata` compatibility-version API smoke, `python3 -m py_compile` + for touched Python helpers, `python3 + tools/release/check_release_metadata.py`, `python3 + tools/policy/check-release-policy.py`, `tools/dev/bun.sh + tools/release/sync-release-pr.mjs --check`, `python3 + tools/release/check_artifact_targets.py`, full `python3 + tools/release/check_consumer_shape.py`, `bash + tools/policy/check-tooling-stack.sh`, `tools/release/release.py check`, + `tools/release/local_registry_publish.py publish --surface cargo --strict`, + `tools/release/local_registry_publish.py publish --surface npm --strict`, + `bash examples/tools/check-examples.sh`, and `bash + tools/policy/check-policy-tools.sh`. The fresh Cargo local-registry sweep + covered 836 `.crate` files with no crate above the 10 MiB crates.io limit; + the largest remained the split WASIX PostGIS AOT part crates at 10,212,312 + bytes. - 2026-06-27: Removed another WASIX runtime/tools package-graph duplication from the remaining Python compatibility layer. The WASIX Cargo artifact packager now reads schema, runtime/tools/ICU package names, AOT target package maps, tool diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 7f98d1ad..9e0eb74f 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -244,6 +244,16 @@ def validate_graph_files(graph: dict) -> None: if not (ROOT / path).is_file(): fail(f"{product} release metadata path does not exist: {path}") product_metadata.validate_all_extension_metadata(graph) + product_metadata_source = read_text("tools/release/product_metadata.py") + release_graph_query = read_text("tools/release/release_graph_query.mjs") + sync_release_pr = read_text("tools/release/sync-release-pr.mjs") + if ( + '"compatibility-version-entries"' not in product_metadata_source + or "_release_metadata(product).get(\"compatibility_versions\"" in product_metadata_source + or "compatibility-version-entries [--require-source-product]" not in release_graph_query + or "compatibilityVersionEntries(graphProducts()" not in sync_release_pr + ): + fail("compatibility version metadata must be collected through the canonical Bun release graph query") def validate_exact_extension_registry_shape(graph: dict) -> None: diff --git a/tools/release/product_metadata.py b/tools/release/product_metadata.py index 8cfd7343..cc35f3fc 100644 --- a/tools/release/product_metadata.py +++ b/tools/release/product_metadata.py @@ -883,37 +883,32 @@ def product_version_specs(graph: dict | None = None) -> dict[str, tuple[str, str def _compatibility_version_entries(*, require_source_product: bool) -> dict[str, tuple[str | None, str, str]]: + rows = _release_graph_query_rows( + "compatibility-version-entries", + ("--require-source-product",) if require_source_product else (), + ) specs: dict[str, tuple[str | None, str, str]] = {} - known_products = set(product_ids()) if require_source_product else set() - for product in product_ids(): - raw_specs = _release_metadata(product).get("compatibility_versions", {}) - if not isinstance(raw_specs, dict): - fail(f"{product}.compatibility_versions must be a table when present") - for spec_id, spec in raw_specs.items(): - if not isinstance(spec_id, str) or not spec_id: - fail(f"{product}.compatibility_versions keys must be non-empty strings") - if not isinstance(spec, dict): - fail(f"{product}.compatibility_versions.{spec_id} must be a table") - source_product = spec.get("source_product") - if require_source_product: - if not isinstance(source_product, str) or not source_product: - fail(f"{product}.compatibility_versions.{spec_id}.source_product must be a non-empty string") - if source_product not in known_products: - fail( - f"{product}.compatibility_versions.{spec_id}.source_product " - f"must name a release product, got {source_product!r}" - ) - elif source_product is not None and not isinstance(source_product, str): - fail(f"{product}.compatibility_versions.{spec_id}.source_product must be a string when present") - path = spec.get("path") - parser = spec.get("parser") - if not isinstance(path, str) or not path: - fail(f"{product}.compatibility_versions.{spec_id}.path must be a non-empty string") - if not isinstance(parser, str) or not parser: - fail(f"{product}.compatibility_versions.{spec_id}.parser must be a non-empty string") - if not (ROOT / path).is_file(): - fail(f"{product}.compatibility_versions.{spec_id} path does not exist: {path}") - specs[spec_id] = (source_product if isinstance(source_product, str) else None, path, parser) + for row in rows: + spec_id = row.get("id") + product = row.get("product") + source_product = row.get("sourceProduct") + path = row.get("path") + parser = row.get("parser") + if not isinstance(spec_id, str) or not spec_id: + fail("compatibility-version-entries rows must declare a non-empty id") + if not isinstance(product, str) or not product: + fail(f"compatibility-version-entries {spec_id}.product must be a non-empty string") + if require_source_product and (not isinstance(source_product, str) or not source_product): + fail(f"compatibility-version-entries {spec_id}.sourceProduct must be a non-empty string") + if source_product is not None and not isinstance(source_product, str): + fail(f"compatibility-version-entries {spec_id}.sourceProduct must be a string or null") + if not isinstance(path, str) or not path: + fail(f"compatibility-version-entries {spec_id}.path must be a non-empty string") + if not isinstance(parser, str) or not parser: + fail(f"compatibility-version-entries {spec_id}.parser must be a non-empty string") + if not (ROOT / path).is_file(): + fail(f"compatibility-version-entries {spec_id} path does not exist: {path}") + specs[spec_id] = (source_product, path, parser) return specs diff --git a/tools/release/release-graph.mjs b/tools/release/release-graph.mjs index 71f102cf..9094e77e 100644 --- a/tools/release/release-graph.mjs +++ b/tools/release/release-graph.mjs @@ -388,6 +388,64 @@ export function loadGraph(prefix = "release-graph") { }; } +function assertObject(value, context, prefix) { + if (value === null || Array.isArray(value) || typeof value !== "object") { + fail(prefix, `${context} must be a table`); + } + return value; +} + +export function compatibilityVersionEntries(products, { requireSourceProduct = false, prefix = "release-graph" } = {}) { + const source = products ?? loadGraph(prefix).products; + const knownProducts = new Set(Object.keys(source)); + const entries = []; + for (const [product, config] of Object.entries(source).sort(([left], [right]) => compareText(left, right))) { + const rawSpecs = config.compatibility_versions ?? {}; + assertObject(rawSpecs, `${product}.compatibility_versions`, prefix); + for (const [specId, spec] of Object.entries(rawSpecs).sort(([left], [right]) => compareText(left, right))) { + if (!specId) { + fail(prefix, `${product}.compatibility_versions keys must be non-empty strings`); + } + assertObject(spec, `${product}.compatibility_versions.${specId}`, prefix); + const sourceProduct = spec.source_product; + if (requireSourceProduct) { + if (typeof sourceProduct !== "string" || sourceProduct.length === 0) { + fail(prefix, `${product}.compatibility_versions.${specId}.source_product must be a non-empty string`); + } + if (!knownProducts.has(sourceProduct)) { + fail( + prefix, + `${product}.compatibility_versions.${specId}.source_product must name a release product, got ${JSON.stringify( + sourceProduct, + )}`, + ); + } + } else if (sourceProduct !== undefined && typeof sourceProduct !== "string") { + fail(prefix, `${product}.compatibility_versions.${specId}.source_product must be a string when present`); + } + const specPath = spec.path; + const parser = spec.parser; + if (typeof specPath !== "string" || specPath.length === 0) { + fail(prefix, `${product}.compatibility_versions.${specId}.path must be a non-empty string`); + } + if (typeof parser !== "string" || parser.length === 0) { + fail(prefix, `${product}.compatibility_versions.${specId}.parser must be a non-empty string`); + } + if (!existsSync(path.join(ROOT, specPath))) { + fail(prefix, `${product}.compatibility_versions.${specId} path does not exist: ${specPath}`); + } + entries.push({ + id: specId, + product, + sourceProduct: typeof sourceProduct === "string" ? sourceProduct : null, + path: specPath, + parser, + }); + } + } + return entries; +} + export function tagMatchPattern(prefix) { return prefix ? `${prefix}[0-9]*` : "[0-9]*"; } diff --git a/tools/release/release_graph_query.mjs b/tools/release/release_graph_query.mjs index 85a15f27..499480c9 100644 --- a/tools/release/release_graph_query.mjs +++ b/tools/release/release_graph_query.mjs @@ -6,6 +6,7 @@ import { } from "./release-artifact-targets.mjs"; import { buildPlan, + compatibilityVersionEntries, compareText, loadGraph, normalizeFiles, @@ -252,6 +253,18 @@ function runWasixCargoArtifactContract() { printJson(wasixCargoArtifactContract()); } +function runCompatibilityVersionEntries(argv) { + let requireSourceProduct = false; + for (const value of argv) { + if (value === "--require-source-product") { + requireSourceProduct = true; + } else { + fail(`unknown argument ${value}`); + } + } + printJson(compatibilityVersionEntries(loadGraph(TOOL).products, { requireSourceProduct, prefix: TOOL })); +} + function usage() { return `usage: tools/release/release_graph_query.mjs [options] @@ -264,6 +277,7 @@ Commands: artifact-targets [--product PRODUCT] [--kind KIND] [--surface SURFACE] [--published-only] raw-artifact-targets [--product PRODUCT] [--kind KIND] [--surface SURFACE] [--published-only] extension-targets [--product PRODUCT] [--family native|wasix] [--published-only] + compatibility-version-entries [--require-source-product] wasix-cargo-artifact-contract `; } @@ -286,6 +300,8 @@ function main(argv) { runRawArtifactTargets(rest); } else if (command === "extension-targets") { runExtensionTargets(rest); + } else if (command === "compatibility-version-entries") { + runCompatibilityVersionEntries(rest); } else if (command === "wasix-cargo-artifact-contract") { runWasixCargoArtifactContract(); } else if (command === "--help" || command === "-h") { diff --git a/tools/release/sync-release-pr.mjs b/tools/release/sync-release-pr.mjs index 4a115ac8..e34826c9 100644 --- a/tools/release/sync-release-pr.mjs +++ b/tools/release/sync-release-pr.mjs @@ -18,7 +18,7 @@ import { exactExtensionProducts, extensionArtifactTargets, } from "./release-artifact-targets.mjs"; -import { loadGraph } from "./release-graph.mjs"; +import { compatibilityVersionEntries, loadGraph } from "./release-graph.mjs"; const PREFIX = "sync-release-pr.mjs"; const DEPENDENCY_TABLES = ["dependencies", "dev-dependencies", "build-dependencies"]; @@ -120,43 +120,12 @@ function packagePath(product) { } function compatibilityVersionLinks() { - const products = graphProducts(); - const known = new Set(Object.keys(products)); - const specs = {}; - for (const [product, config] of Object.entries(products)) { - const rawSpecs = config.compatibility_versions ?? {}; - if (rawSpecs === null || Array.isArray(rawSpecs) || typeof rawSpecs !== "object") { - fail(`${product}.compatibility_versions must be a table when present`); - } - for (const [specId, spec] of Object.entries(rawSpecs)) { - if (!specId) { - fail(`${product}.compatibility_versions keys must be non-empty strings`); - } - if (spec === null || Array.isArray(spec) || typeof spec !== "object") { - fail(`${product}.compatibility_versions.${specId} must be a table`); - } - const sourceProduct = spec.source_product; - if (typeof sourceProduct !== "string" || !sourceProduct) { - fail(`${product}.compatibility_versions.${specId}.source_product must be a non-empty string`); - } - if (!known.has(sourceProduct)) { - fail(`${product}.compatibility_versions.${specId}.source_product must name a release product, got ${JSON.stringify(sourceProduct)}`); - } - const specPath = spec.path; - const parser = spec.parser; - if (typeof specPath !== "string" || !specPath) { - fail(`${product}.compatibility_versions.${specId}.path must be a non-empty string`); - } - if (typeof parser !== "string" || !parser) { - fail(`${product}.compatibility_versions.${specId}.parser must be a non-empty string`); - } - if (!existsSync(path.join(ROOT, specPath))) { - fail(`${product}.compatibility_versions.${specId} path does not exist: ${specPath}`); - } - specs[specId] = [sourceProduct, specPath, parser]; - } - } - return specs; + return Object.fromEntries( + compatibilityVersionEntries(graphProducts(), { requireSourceProduct: true, prefix: PREFIX }).map((entry) => [ + entry.id, + [entry.sourceProduct, entry.path, entry.parser], + ]), + ); } function setJsonPath(data, dotted, expected, context) { From 12585aecf50427a53d7faef4c0ce8a94a8c99a72 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 10:39:19 +0000 Subject: [PATCH 175/308] refactor: share extension release metadata --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 35 +++ .../release/build-extension-ci-artifacts.mjs | 251 +----------------- tools/release/check-staged-artifacts.mjs | 215 +-------------- tools/release/check_release_metadata.py | 14 + tools/release/product_metadata.py | 209 +++------------ tools/release/release-artifact-targets.mjs | 248 ++++++++++++++--- tools/release/release_graph_query.mjs | 32 +++ 7 files changed, 338 insertions(+), 666 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index ff36003e..7915ff07 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,41 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Moved exact-extension release metadata and source identity + parsing out of the Python compatibility layer and the duplicate CI artifact + helpers. `tools/release/release-artifact-targets.mjs` now owns + `extensionMetadata`, `extensionSourceIdentity`, `extensionSqlName`, and the + shared graph-backed product version parser; `tools/release/release_graph_query.mjs` + exposes `extension-metadata [--product PRODUCT]`; and + `tools/release/product_metadata.py` adapts those query rows for legacy Python + callers. `tools/release/build-extension-ci-artifacts.mjs` and + `tools/release/check-staged-artifacts.mjs` now reuse the shared helper instead + of carrying local extension metadata/source identity implementations. A + subagent review was attempted for this slice, but the current session had + reached the agent thread limit, so the audit used local repo evidence instead. + Fresh checks passed: `tools/dev/bun.sh + tools/release/release_graph_query.mjs extension-metadata --product + oliphaunt-extension-unaccent`, full `extension-metadata` query count/parity + smoke across 39 exact-extension products, Python `product_metadata` + extension-metadata and source-identity smoke, `python3 -m py_compile` for + touched Python helpers, `tools/dev/bun.sh + tools/release/build-extension-ci-artifacts.mjs --help`, + `tools/dev/bun.sh tools/release/check-staged-artifacts.mjs --help`, scoped + unaccent extension artifact staging with native Linux x64 plus WASIX payloads, + `tools/dev/bun.sh tools/release/check-staged-artifacts.mjs --inspect-present + --require-extension-product oliphaunt-extension-unaccent`, `python3 + tools/release/check_release_metadata.py`, `python3 + tools/release/check_artifact_targets.py`, `python3 + src/extensions/tools/check-extension-model.py --check`, `python3 + tools/release/check_consumer_shape.py`, `tools/release/release.py check`, + `bash tools/policy/check-policy-tools.sh`, + `tools/release/local_registry_publish.py publish --surface cargo --strict`, + and `tools/release/local_registry_publish.py publish --surface npm --strict`. + The fresh Cargo local-registry sweep covered 836 `.crate` files with no crate + above the 10 MiB crates.io limit; the largest remained the split WASIX PostGIS + AOT part crates at 10,212,312 bytes. The strict npm publish also confirmed + separate `@oliphaunt/liboliphaunt-linux-x64-gnu` and + `@oliphaunt/tools-linux-x64-gnu` packages. - 2026-06-27: Moved compatibility-version metadata collection out of the Python release compatibility layer and into the canonical Bun release graph. `tools/release/release-graph.mjs` now exposes sorted diff --git a/tools/release/build-extension-ci-artifacts.mjs b/tools/release/build-extension-ci-artifacts.mjs index 89fa0c25..2b8b3a45 100644 --- a/tools/release/build-extension-ci-artifacts.mjs +++ b/tools/release/build-extension-ci-artifacts.mjs @@ -18,19 +18,15 @@ import { ROOT, compareText, currentProductVersion, + currentProductVersionSync, exactExtensionProducts, extensionArtifactTargets, + extensionMetadata, + extensionSourceIdentity, + extensionSqlName, } from "./release-artifact-targets.mjs"; -import { loadGraph } from "./release-graph.mjs"; const PREFIX = "build-extension-ci-artifacts.mjs"; -const EXTENSION_VERSIONING_BY_CLASS = { - contrib: "postgres-bound", - external: "upstream-bound", - "first-party": "repo-bound", -}; -const EXTENSION_RUNTIME_CONTRACT_PATH = "src/shared/extension-runtime-contract/contract.toml"; -const POSTGRES18_SOURCE_PATH = "src/postgres/versions/18/source.toml"; function fail(message) { console.error(`${PREFIX}: ${message}`); @@ -45,37 +41,10 @@ function sha256(file) { return createHash("sha256").update(readFileSync(file)).digest("hex"); } -function graphProducts() { - return loadGraph(PREFIX).products; -} - -function productConfig(product) { - const config = graphProducts()[product]; - if (!config) { - fail(`unknown release product ${product}`); - } - return config; -} - -function packagePath(product) { - return releaseMetadataRelativePath( - nonEmptyString(productConfig(product).path, `${product}.path`), - `${product}.path`, - ); -} - function extensionProducts() { return exactExtensionProducts(PREFIX); } -function extensionSqlName(product) { - const value = productConfig(product).extension_sql_name; - if (typeof value !== "string" || !value) { - fail(`${product} release metadata must declare extension_sql_name`); - } - return value; -} - function generatedExtensionRow(sqlName) { const metadata = path.join(ROOT, "src/extensions/generated/sdk/kotlin.json"); const data = JSON.parse(readFileSync(metadata, "utf8")); @@ -157,7 +126,7 @@ function publishedTargetIds(family) { } function nativeExtensionAssetIndexes(sqlName, product = undefined) { - const version = currentProductVersionSync("liboliphaunt-native"); + const version = currentProductVersionSync("liboliphaunt-native", PREFIX); const root = nativeReleaseAssetRoot(); const indexes = []; for (const target of publishedTargetIds("native")) { @@ -223,7 +192,7 @@ function nativeAssetsFor(sqlName, { product = undefined, required = false } = {} } function wasixArchiveFor(sqlName, { product = undefined, required = false } = {}) { - const version = currentProductVersionSync("liboliphaunt-wasix"); + const version = currentProductVersionSync("liboliphaunt-wasix", PREFIX); const root = wasixReleaseAssetRoot(); const indexes = []; for (const target of publishedTargetIds("wasix")) { @@ -376,139 +345,12 @@ function validateStagedTargets(product, assets, { requireNative, requireWasix, r } } -function extensionMetadata(product) { - const config = productConfig(product); - if (config.kind !== "exact-extension-artifact") { - fail(`${product} is not an exact-extension artifact product`); - } - const topLevelSqlName = config.extension_sql_name; - if (typeof topLevelSqlName !== "string" || !topLevelSqlName) { - fail(`${product} release metadata must declare extension_sql_name`); - } - const metadata = config.extension; - if (metadata === null || Array.isArray(metadata) || typeof metadata !== "object") { - fail(`${product} release metadata must declare [extension]`); - } - const sqlName = nonEmptyString(metadata.sql_name, `${product}.extension.sql_name`); - if (sqlName !== topLevelSqlName) { - fail(`${product}.extension.sql_name ${JSON.stringify(sqlName)} must match extension_sql_name ${JSON.stringify(topLevelSqlName)}`); - } - const extensionClass = nonEmptyString(metadata.class, `${product}.extension.class`); - if (!(extensionClass in EXTENSION_VERSIONING_BY_CLASS)) { - fail(`${product}.extension.class must be one of ${Object.keys(EXTENSION_VERSIONING_BY_CLASS).sort(compareText).join(", ")}`); - } - const versioning = nonEmptyString(metadata.versioning, `${product}.extension.versioning`); - const expectedVersioning = EXTENSION_VERSIONING_BY_CLASS[extensionClass]; - if (versioning !== expectedVersioning) { - fail(`${product}.extension.versioning must be ${JSON.stringify(expectedVersioning)} for class ${JSON.stringify(extensionClass)}, got ${JSON.stringify(versioning)}`); - } - const source = metadata.source; - if (source === null || Array.isArray(source) || typeof source !== "object") { - fail(`${product}.extension must declare [extension.source]`); - } - const sourcePath = releaseMetadataRelativePath(nonEmptyString(source.path, `${product}.extension.source.path`), `${product}.extension.source.path`); - const packageRoot = packagePath(product); - if (extensionClass === "contrib" && sourcePath !== POSTGRES18_SOURCE_PATH) { - fail(`${product}.extension.source.path must be ${JSON.stringify(POSTGRES18_SOURCE_PATH)} for contrib extensions`); - } - if (extensionClass === "external" && sourcePath !== `${packageRoot}/source.toml`) { - fail(`${product}.extension.source.path must be ${packageRoot}/source.toml for external extensions`); - } - if (extensionClass === "first-party" && !(sourcePath === packageRoot || sourcePath.startsWith(`${packageRoot}/`))) { - fail(`${product}.extension.source.path must stay inside ${packageRoot}/ for first-party extensions`); - } - - const compatibility = metadata.compatibility; - if (compatibility === null || Array.isArray(compatibility) || typeof compatibility !== "object") { - fail(`${product}.extension must declare [extension.compatibility]`); - } - const postgresMajor = nonEmptyString(compatibility.postgres_major, `${product}.extension.compatibility.postgres_major`); - if (postgresMajor !== "18") { - fail(`${product}.extension.compatibility.postgres_major must be '18', got ${JSON.stringify(postgresMajor)}`); - } - const contractPath = releaseMetadataRelativePath( - nonEmptyString(compatibility.extension_runtime_contract, `${product}.extension.compatibility.extension_runtime_contract`), - `${product}.extension.compatibility.extension_runtime_contract`, - ); - if (contractPath !== EXTENSION_RUNTIME_CONTRACT_PATH) { - fail(`${product}.extension.compatibility.extension_runtime_contract must be ${JSON.stringify(EXTENSION_RUNTIME_CONTRACT_PATH)}`); - } - const nativeProduct = nonEmptyString(compatibility.native_runtime_product, `${product}.extension.compatibility.native_runtime_product`); - const wasixProduct = nonEmptyString(compatibility.wasix_runtime_product, `${product}.extension.compatibility.wasix_runtime_product`); - if (nativeProduct !== "liboliphaunt-native") { - fail(`${product}.extension.compatibility.native_runtime_product must be 'liboliphaunt-native'`); - } - if (wasixProduct !== "liboliphaunt-wasix") { - fail(`${product}.extension.compatibility.wasix_runtime_product must be 'liboliphaunt-wasix'`); - } - const nativeVersion = nonEmptyString(compatibility.native_runtime_version, `${product}.extension.compatibility.native_runtime_version`); - const wasixVersion = nonEmptyString(compatibility.wasix_runtime_version, `${product}.extension.compatibility.wasix_runtime_version`); - const expectedNativeVersion = currentProductVersionSync(nativeProduct); - const expectedWasixVersion = currentProductVersionSync(wasixProduct); - if (nativeVersion !== expectedNativeVersion) { - fail(`${product}.extension.compatibility.native_runtime_version must be ${JSON.stringify(expectedNativeVersion)}, got ${JSON.stringify(nativeVersion)}`); - } - if (wasixVersion !== expectedWasixVersion) { - fail(`${product}.extension.compatibility.wasix_runtime_version must be ${JSON.stringify(expectedWasixVersion)}, got ${JSON.stringify(wasixVersion)}`); - } - return { - sqlName, - class: extensionClass, - versioning, - sourcePath, - compatibility: { - postgresMajor, - extensionRuntimeContract: contractPath, - nativeRuntimeProduct: nativeProduct, - nativeRuntimeVersion: nativeVersion, - wasixRuntimeProduct: wasixProduct, - wasixRuntimeVersion: wasixVersion, - }, - }; -} - -function extensionSourceIdentity(product) { - const metadata = extensionMetadata(product); - const source = Bun.TOML.parse(readFileSync(path.join(ROOT, metadata.sourcePath), "utf8")); - if (metadata.class === "contrib") { - const postgresql = source.postgresql; - if (postgresql === null || Array.isArray(postgresql) || typeof postgresql !== "object") { - fail(`${metadata.sourcePath} must declare [postgresql] for contrib extension products`); - } - return { - kind: "postgres-contrib", - name: "postgresql", - version: nonEmptyString(postgresql.version, `${metadata.sourcePath}.postgresql.version`), - url: nonEmptyString(postgresql.url, `${metadata.sourcePath}.postgresql.url`), - sha256: nonEmptyString(postgresql.sha256, `${metadata.sourcePath}.postgresql.sha256`), - }; - } - if (metadata.class === "external") { - return { - kind: "external", - name: nonEmptyString(source.name, `${metadata.sourcePath}.name`), - url: nonEmptyString(source.url, `${metadata.sourcePath}.url`), - branch: nonEmptyString(source.branch, `${metadata.sourcePath}.branch`), - commit: nonEmptyString(source.commit, `${metadata.sourcePath}.commit`), - }; - } - if (metadata.class === "first-party") { - return { - kind: "repo", - name: metadata.sqlName, - path: metadata.sourcePath, - version: currentProductVersionSync(product), - }; - } - fail(`${product}.extension.class has unsupported source identity class ${JSON.stringify(metadata.class)}`); -} - async function stageProduct(product, { outputRoot, requireNative, requireWasix, requireNativeTargets }) { const known = new Set(extensionProducts()); if (!known.has(product)) { fail(`unknown exact-extension product ${product}; expected one of: ${[...known].sort(compareText).join(", ")}`); } - const sqlName = extensionSqlName(product); + const sqlName = extensionSqlName(product, PREFIX); const extensionRow = generatedExtensionRow(sqlName); const version = await currentProductVersion(product, PREFIX); const productRoot = path.join(outputRoot, product); @@ -570,7 +412,7 @@ async function stageProduct(product, { outputRoot, requireNative, requireWasix, }; writeFileSync(path.join(productRoot, "extension-artifacts.json"), `${JSON.stringify(sortValue(manifest), null, 2)}\n`, "utf8"); - const releaseMetadata = extensionMetadata(product); + const releaseMetadata = extensionMetadata(product, PREFIX); const releaseData = { schema: "oliphaunt-extension-release-manifest-v1", product, @@ -578,7 +420,7 @@ async function stageProduct(product, { outputRoot, requireNative, requireWasix, sqlName, extensionClass: releaseMetadata.class, versioning: releaseMetadata.versioning, - sourceIdentity: extensionSourceIdentity(product), + sourceIdentity: extensionSourceIdentity(product, PREFIX), compatibility: releaseMetadata.compatibility, dependencies: manifest.dependencies, nativeModuleStem: manifest.nativeModuleStem, @@ -698,81 +540,6 @@ function sortValue(value) { return value; } -const versionCache = new Map(); - -function currentProductVersionSync(product) { - if (!versionCache.has(product)) { - const versionFile = productConfig(product).version_files?.[0]; - if (typeof versionFile !== "string" || !versionFile) { - fail(`${product} does not declare a canonical version file`); - } - const file = path.join(ROOT, versionFile); - const text = readFileSync(file, "utf8"); - const name = path.basename(file); - let version = ""; - if (name === "Cargo.toml") { - 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; - } - const match = inPackage ? /^version\s*=\s*"([^"]+)"/u.exec(line) : null; - if (match) { - version = match[1]; - break; - } - } - } else if (name === "package.json" || name === "jsr.json") { - const data = JSON.parse(text); - version = typeof data.version === "string" ? data.version : ""; - } else if (name === "gradle.properties") { - for (const rawLine of text.split(/\r?\n/u)) { - const line = rawLine.trim(); - if (!line || line.startsWith("#") || !line.includes("=")) { - continue; - } - const [key, ...rest] = line.split("="); - if (key.trim() === "VERSION_NAME") { - version = rest.join("=").trim(); - break; - } - } - } else if (name === "VERSION" || name === "LIBOLIPHAUNT_VERSION") { - version = text.trim(); - } else { - fail(`${product}.version_files has unsupported version file type: ${versionFile}`); - } - if (!version) { - fail(`${versionFile} does not define a release version for ${product}`); - } - versionCache.set(product, version); - } - return versionCache.get(product); -} - -function nonEmptyString(value, context) { - if (typeof value === "string" && value.length > 0) { - return value; - } - fail(`${context} must be a non-empty string`); -} - -function releaseMetadataRelativePath(value, context) { - const candidate = path.normalize(value).split(path.sep).join("/"); - if (path.isAbsolute(value) || candidate.split("/").includes("..")) { - fail(`${context} must be a repository-relative path: ${JSON.stringify(value)}`); - } - if (!existsSync(path.join(ROOT, candidate))) { - fail(`${context} path does not exist: ${candidate}`); - } - return candidate; -} - async function main(argv) { const args = parseArgs(argv); const envProducts = selectedProductsFromEnv(); diff --git a/tools/release/check-staged-artifacts.mjs b/tools/release/check-staged-artifacts.mjs index 0e382d37..3d2d93a0 100644 --- a/tools/release/check-staged-artifacts.mjs +++ b/tools/release/check-staged-artifacts.mjs @@ -16,6 +16,8 @@ import { currentProductVersion, exactExtensionProducts, extensionArtifactTargets, + extensionMetadata, + extensionSourceIdentity, } from "./release-artifact-targets.mjs"; import { loadGraph } from "./release-graph.mjs"; import { @@ -82,13 +84,6 @@ const SDK_RUNTIME_PAYLOAD_PATTERNS = [ const KOTLIN_ALLOWED_NATIVE_PAYLOADS = new Set(["liboliphaunt_kotlin_android.so"]); const KOTLIN_RELEASE_ABIS = new Set(["arm64-v8a", "x86_64"]); const BASELINE_POSTGRES_EXTENSIONS = new Set(["plpgsql"]); -const EXTENSION_VERSIONING_BY_CLASS = { - contrib: "postgres-bound", - external: "upstream-bound", - "first-party": "repo-bound", -}; -const EXTENSION_RUNTIME_CONTRACT_PATH = "src/shared/extension-runtime-contract/contract.toml"; -const POSTGRES18_SOURCE_PATH = "src/postgres/versions/18/source.toml"; function fail(message) { console.error(`${PREFIX}: ${message}`); @@ -376,208 +371,6 @@ function sdkProducts() { .sort(compareText); } -const versionCache = new Map(); - -function currentProductVersionSync(product) { - if (!versionCache.has(product)) { - const versionFile = productConfig(product).version_files?.[0]; - if (typeof versionFile !== "string" || !versionFile) { - fail(`${product} does not declare a canonical version file`); - } - const file = path.join(ROOT, versionFile); - const text = readFileSync(file, "utf8"); - const name = path.basename(file); - let version = ""; - if (name === "Cargo.toml") { - 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; - } - const match = inPackage ? /^version\s*=\s*"([^"]+)"/u.exec(line) : null; - if (match) { - version = match[1]; - break; - } - } - } else if (name === "package.json" || name === "jsr.json") { - const data = JSON.parse(text); - version = typeof data.version === "string" ? data.version : ""; - } else if (name === "gradle.properties") { - for (const rawLine of text.split(/\r?\n/u)) { - const line = rawLine.trim(); - if (!line || line.startsWith("#") || !line.includes("=")) { - continue; - } - const [key, ...rest] = line.split("="); - if (key.trim() === "VERSION_NAME") { - version = rest.join("=").trim(); - break; - } - } - } else if (name === "VERSION" || name === "LIBOLIPHAUNT_VERSION") { - version = text.trim(); - } else { - fail(`${product}.version_files has unsupported version file type: ${versionFile}`); - } - if (!version) { - fail(`${versionFile} does not define a release version for ${product}`); - } - versionCache.set(product, version); - } - return versionCache.get(product); -} - -function nonEmptyString(value, context) { - if (typeof value === "string" && value.length > 0) { - return value; - } - fail(`${context} must be a non-empty string`); -} - -function releaseMetadataRelativePath(value, context) { - const candidate = path.normalize(value).split(path.sep).join("/"); - if (path.isAbsolute(value) || candidate.split("/").includes("..")) { - fail(`${context} must be a repository-relative path: ${JSON.stringify(value)}`); - } - if (!existsSync(path.join(ROOT, candidate))) { - fail(`${context} path does not exist: ${candidate}`); - } - return candidate; -} - -function packagePath(product) { - return releaseMetadataRelativePath(nonEmptyString(productConfig(product).path, `${product}.path`), `${product}.path`); -} - -function extensionMetadata(product) { - const config = productConfig(product); - if (config.kind !== "exact-extension-artifact") { - fail(`${product} is not an exact-extension artifact product`); - } - const topLevelSqlName = nonEmptyString(config.extension_sql_name, `${product}.extension_sql_name`); - const metadata = config.extension; - if (metadata === null || Array.isArray(metadata) || typeof metadata !== "object") { - fail(`${product} release metadata must declare [extension]`); - } - const sqlName = nonEmptyString(metadata.sql_name, `${product}.extension.sql_name`); - if (sqlName !== topLevelSqlName) { - fail(`${product}.extension.sql_name ${JSON.stringify(sqlName)} must match extension_sql_name ${JSON.stringify(topLevelSqlName)}`); - } - const extensionClass = nonEmptyString(metadata.class, `${product}.extension.class`); - if (!(extensionClass in EXTENSION_VERSIONING_BY_CLASS)) { - fail(`${product}.extension.class must be one of ${Object.keys(EXTENSION_VERSIONING_BY_CLASS).sort(compareText).join(", ")}`); - } - const versioning = nonEmptyString(metadata.versioning, `${product}.extension.versioning`); - const expectedVersioning = EXTENSION_VERSIONING_BY_CLASS[extensionClass]; - if (versioning !== expectedVersioning) { - fail(`${product}.extension.versioning must be ${JSON.stringify(expectedVersioning)} for class ${JSON.stringify(extensionClass)}, got ${JSON.stringify(versioning)}`); - } - const source = metadata.source; - if (source === null || Array.isArray(source) || typeof source !== "object") { - fail(`${product}.extension must declare [extension.source]`); - } - const sourcePath = releaseMetadataRelativePath(nonEmptyString(source.path, `${product}.extension.source.path`), `${product}.extension.source.path`); - const packageRoot = packagePath(product); - if (extensionClass === "contrib" && sourcePath !== POSTGRES18_SOURCE_PATH) { - fail(`${product}.extension.source.path must be ${JSON.stringify(POSTGRES18_SOURCE_PATH)} for contrib extensions`); - } - if (extensionClass === "external" && sourcePath !== `${packageRoot}/source.toml`) { - fail(`${product}.extension.source.path must be ${packageRoot}/source.toml for external extensions`); - } - if (extensionClass === "first-party" && !(sourcePath === packageRoot || sourcePath.startsWith(`${packageRoot}/`))) { - fail(`${product}.extension.source.path must stay inside ${packageRoot}/ for first-party extensions`); - } - const compatibility = metadata.compatibility; - if (compatibility === null || Array.isArray(compatibility) || typeof compatibility !== "object") { - fail(`${product}.extension must declare [extension.compatibility]`); - } - const postgresMajor = nonEmptyString(compatibility.postgres_major, `${product}.extension.compatibility.postgres_major`); - if (postgresMajor !== "18") { - fail(`${product}.extension.compatibility.postgres_major must be '18', got ${JSON.stringify(postgresMajor)}`); - } - const contractPath = releaseMetadataRelativePath( - nonEmptyString(compatibility.extension_runtime_contract, `${product}.extension.compatibility.extension_runtime_contract`), - `${product}.extension.compatibility.extension_runtime_contract`, - ); - if (contractPath !== EXTENSION_RUNTIME_CONTRACT_PATH) { - fail(`${product}.extension.compatibility.extension_runtime_contract must be ${JSON.stringify(EXTENSION_RUNTIME_CONTRACT_PATH)}`); - } - const nativeProduct = nonEmptyString(compatibility.native_runtime_product, `${product}.extension.compatibility.native_runtime_product`); - const wasixProduct = nonEmptyString(compatibility.wasix_runtime_product, `${product}.extension.compatibility.wasix_runtime_product`); - if (nativeProduct !== "liboliphaunt-native") { - fail(`${product}.extension.compatibility.native_runtime_product must be 'liboliphaunt-native'`); - } - if (wasixProduct !== "liboliphaunt-wasix") { - fail(`${product}.extension.compatibility.wasix_runtime_product must be 'liboliphaunt-wasix'`); - } - const nativeVersion = nonEmptyString(compatibility.native_runtime_version, `${product}.extension.compatibility.native_runtime_version`); - const wasixVersion = nonEmptyString(compatibility.wasix_runtime_version, `${product}.extension.compatibility.wasix_runtime_version`); - const expectedNativeVersion = currentProductVersionSync(nativeProduct); - const expectedWasixVersion = currentProductVersionSync(wasixProduct); - if (nativeVersion !== expectedNativeVersion) { - fail(`${product}.extension.compatibility.native_runtime_version must be ${JSON.stringify(expectedNativeVersion)}, got ${JSON.stringify(nativeVersion)}`); - } - if (wasixVersion !== expectedWasixVersion) { - fail(`${product}.extension.compatibility.wasix_runtime_version must be ${JSON.stringify(expectedWasixVersion)}, got ${JSON.stringify(wasixVersion)}`); - } - return { - sqlName, - class: extensionClass, - versioning, - sourcePath, - compatibility: { - postgresMajor, - extensionRuntimeContract: contractPath, - nativeRuntimeProduct: nativeProduct, - nativeRuntimeVersion: nativeVersion, - wasixRuntimeProduct: wasixProduct, - wasixRuntimeVersion: wasixVersion, - }, - }; -} - -function extensionSourceIdentity(product) { - const metadata = extensionMetadata(product); - const source = Bun.TOML.parse(readFileSync(path.join(ROOT, metadata.sourcePath), "utf8")); - if (metadata.class === "contrib") { - const postgresql = source.postgresql; - if (postgresql === null || Array.isArray(postgresql) || typeof postgresql !== "object") { - fail(`${metadata.sourcePath} must declare [postgresql] for contrib extension products`); - } - return { - kind: "postgres-contrib", - name: "postgresql", - version: nonEmptyString(postgresql.version, `${metadata.sourcePath}.postgresql.version`), - url: nonEmptyString(postgresql.url, `${metadata.sourcePath}.postgresql.url`), - sha256: nonEmptyString(postgresql.sha256, `${metadata.sourcePath}.postgresql.sha256`), - }; - } - if (metadata.class === "external") { - return { - kind: "external", - name: nonEmptyString(source.name, `${metadata.sourcePath}.name`), - url: nonEmptyString(source.url, `${metadata.sourcePath}.url`), - branch: nonEmptyString(source.branch, `${metadata.sourcePath}.branch`), - commit: nonEmptyString(source.commit, `${metadata.sourcePath}.commit`), - }; - } - if (metadata.class === "first-party") { - return { - kind: "repo", - name: metadata.sqlName, - path: metadata.sourcePath, - version: currentProductVersionSync(product), - }; - } - fail(`${product}.extension.class has unsupported source identity class ${JSON.stringify(metadata.class)}`); -} - function publicAotCargoDependencies() { return Object.fromEntries( Object.entries(WASIX_AOT_PACKAGES).map(([target, name]) => [ @@ -971,14 +764,14 @@ async function checkExtensionProduct(product, { require, requireFullTargets }) { if (!setEquals(new Set(Object.keys(releaseData)), PUBLIC_EXTENSION_RELEASE_MANIFEST_KEYS)) { fail(`${rel(releaseManifest)} public manifest keys must be ${JSON.stringify([...PUBLIC_EXTENSION_RELEASE_MANIFEST_KEYS].sort(compareText))}, got ${JSON.stringify(Object.keys(releaseData).sort(compareText))}`); } - const metadata = extensionMetadata(product); + const metadata = extensionMetadata(product, PREFIX); if (releaseData.extensionClass !== metadata.class) { fail(`${rel(releaseManifest)} has stale extensionClass`); } if (releaseData.versioning !== metadata.versioning) { fail(`${rel(releaseManifest)} has stale versioning`); } - if (!deepEqual(releaseData.sourceIdentity, extensionSourceIdentity(product))) { + if (!deepEqual(releaseData.sourceIdentity, extensionSourceIdentity(product, PREFIX))) { fail(`${rel(releaseManifest)} has stale sourceIdentity`); } if (!deepEqual(releaseData.compatibility, metadata.compatibility)) { diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 9e0eb74f..7823cd34 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -246,7 +246,10 @@ def validate_graph_files(graph: dict) -> None: product_metadata.validate_all_extension_metadata(graph) product_metadata_source = read_text("tools/release/product_metadata.py") release_graph_query = read_text("tools/release/release_graph_query.mjs") + release_artifact_targets = read_text("tools/release/release-artifact-targets.mjs") sync_release_pr = read_text("tools/release/sync-release-pr.mjs") + build_extension_ci_artifacts = read_text("tools/release/build-extension-ci-artifacts.mjs") + check_staged_artifacts = read_text("tools/release/check-staged-artifacts.mjs") if ( '"compatibility-version-entries"' not in product_metadata_source or "_release_metadata(product).get(\"compatibility_versions\"" in product_metadata_source @@ -254,6 +257,17 @@ def validate_graph_files(graph: dict) -> None: or "compatibilityVersionEntries(graphProducts()" not in sync_release_pr ): fail("compatibility version metadata must be collected through the canonical Bun release graph query") + if ( + '"extension-metadata"' not in product_metadata_source + or "extension-metadata [--product PRODUCT]" not in release_graph_query + or "export function extensionMetadata(" not in release_artifact_targets + or "export function extensionSourceIdentity(" not in release_artifact_targets + or "function extensionMetadata(" in build_extension_ci_artifacts + or "function extensionSourceIdentity(" in build_extension_ci_artifacts + or "function extensionMetadata(" in check_staged_artifacts + or "function extensionSourceIdentity(" in check_staged_artifacts + ): + fail("extension metadata and source identity must be shared through release-artifact-targets and the Bun release graph query") def validate_exact_extension_registry_shape(graph: dict) -> None: diff --git a/tools/release/product_metadata.py b/tools/release/product_metadata.py index cc35f3fc..826013b8 100644 --- a/tools/release/product_metadata.py +++ b/tools/release/product_metadata.py @@ -20,14 +20,6 @@ ROOT = Path(__file__).resolve().parents[2] -EXTENSION_CLASSES = {"contrib", "external", "first-party"} -EXTENSION_VERSIONING_BY_CLASS = { - "contrib": "postgres-bound", - "external": "upstream-bound", - "first-party": "repo-bound", -} -EXTENSION_RUNTIME_CONTRACT_PATH = "src/shared/extension-runtime-contract/contract.toml" -POSTGRES18_SOURCE_PATH = "src/postgres/versions/18/source.toml" PUBLIC_EXTENSION_RELEASE_MANIFEST_KEYS = { "schema", "product", @@ -59,15 +51,6 @@ def fail(message: str) -> NoReturn: raise SystemExit(2) -def _read_toml(path: Path) -> dict[str, Any]: - if not path.is_file(): - fail(f"missing {path.relative_to(ROOT)}") - value = tomllib.loads(path.read_text(encoding="utf-8")) - if not isinstance(value, dict): - fail(f"{path.relative_to(ROOT)} must contain a TOML table") - return value - - def package_path(product: str) -> str: value = product_config(product).get("path") if not isinstance(value, str) or not value: @@ -88,26 +71,6 @@ def moon_release_metadata(product: str) -> dict[str, Any]: return release -def _release_metadata_path(product: str) -> Path: - return ROOT / package_path(product) / "release.toml" - - -def _release_metadata(product: str) -> dict[str, Any]: - metadata = _read_toml(_release_metadata_path(product)) - metadata_id = metadata.get("id") - if metadata_id != product: - fail(f"{_release_metadata_path(product).relative_to(ROOT)} must declare id = {product!r}") - return metadata - - -def _effective_release_metadata(product: str) -> dict[str, Any]: - metadata = dict(_release_metadata(product)) - 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") - return metadata - - def load_graph() -> dict[str, Any]: """Compatibility return value for callers that still accept a graph arg.""" @@ -658,152 +621,56 @@ def registry_package_names(product: str, package_kind: str) -> list[str]: return names -def _string_field(config: dict[str, Any], key: str, context: str) -> str: - value = config.get(key) +@lru_cache(maxsize=1) +def _extension_metadata_rows() -> tuple[dict[str, Any], ...]: + return _release_graph_query_rows("extension-metadata") + + +def _extension_metadata_row(product: str) -> dict[str, Any]: + matches = [row for row in _extension_metadata_rows() if row.get("product") == product] + if len(matches) != 1: + fail(f"release graph extension-metadata query must return one row for {product}, got {len(matches)}") + return dict(matches[0]) + + +def _metadata_string(row: dict[str, Any], key: str, product: str) -> str: + value = row.get(key) if not isinstance(value, str) or not value: - fail(f"{context}.{key} must be a non-empty string") + fail(f"extension-metadata {product}.{key} must be a non-empty string") return value -def _release_metadata_relative_path(path: str, context: str) -> str: - candidate = Path(path) - if candidate.is_absolute() or ".." in candidate.parts: - fail(f"{context} must be a repository-relative path: {path!r}") - if not (ROOT / candidate).is_file(): - fail(f"{context} path does not exist: {path}") - return candidate.as_posix() +def _metadata_object(row: dict[str, Any], key: str, product: str) -> dict[str, Any]: + value = row.get(key) + if not isinstance(value, dict): + fail(f"extension-metadata {product}.{key} must be an object") + return dict(value) def extension_metadata(product: str, graph: dict | None = None) -> dict[str, Any]: - config = product_config(product) - if config.get("kind") != "exact-extension-artifact": - fail(f"{product} is not an exact-extension artifact product") - metadata = _release_metadata(product) - top_level_sql_name = metadata.get("extension_sql_name") - if not isinstance(top_level_sql_name, str) or not top_level_sql_name: - fail(f"{product} release metadata must declare extension_sql_name") - - extension = metadata.get("extension") - if not isinstance(extension, dict): - fail(f"{product} release metadata must declare [extension]") - sql_name = _string_field(extension, "sql_name", f"{product}.extension") - if sql_name != top_level_sql_name: - fail( - f"{product}.extension.sql_name {sql_name!r} must match " - f"extension_sql_name {top_level_sql_name!r}" - ) - extension_class = _string_field(extension, "class", f"{product}.extension") - if extension_class not in EXTENSION_CLASSES: - fail(f"{product}.extension.class must be one of {sorted(EXTENSION_CLASSES)}, got {extension_class!r}") - versioning = _string_field(extension, "versioning", f"{product}.extension") - expected_versioning = EXTENSION_VERSIONING_BY_CLASS[extension_class] - if versioning != expected_versioning: - fail( - f"{product}.extension.versioning must be {expected_versioning!r} " - f"for class {extension_class!r}, got {versioning!r}" - ) - - source = extension.get("source") - if not isinstance(source, dict): - fail(f"{product}.extension must declare [extension.source]") - source_path = _release_metadata_relative_path( - _string_field(source, "path", f"{product}.extension.source"), - f"{product}.extension.source.path", - ) - package = package_path(product) - if extension_class == "contrib" and source_path != POSTGRES18_SOURCE_PATH: - fail(f"{product}.extension.source.path must be {POSTGRES18_SOURCE_PATH!r} for contrib extensions") - if extension_class == "external" and source_path != f"{package}/source.toml": - fail(f"{product}.extension.source.path must be {package}/source.toml for external extensions") - if extension_class == "first-party" and not ( - source_path == package or source_path.startswith(f"{package}/") - ): - fail(f"{product}.extension.source.path must stay inside {package}/ for first-party extensions") - - compatibility = extension.get("compatibility") - if not isinstance(compatibility, dict): - fail(f"{product}.extension must declare [extension.compatibility]") - postgres_major = _string_field(compatibility, "postgres_major", f"{product}.extension.compatibility") - if postgres_major != "18": - fail(f"{product}.extension.compatibility.postgres_major must be '18', got {postgres_major!r}") - contract_path = _release_metadata_relative_path( - _string_field(compatibility, "extension_runtime_contract", f"{product}.extension.compatibility"), - f"{product}.extension.compatibility.extension_runtime_contract", - ) - if contract_path != EXTENSION_RUNTIME_CONTRACT_PATH: - fail( - f"{product}.extension.compatibility.extension_runtime_contract must be " - f"{EXTENSION_RUNTIME_CONTRACT_PATH!r}" - ) - native_product = _string_field(compatibility, "native_runtime_product", f"{product}.extension.compatibility") - wasix_product = _string_field(compatibility, "wasix_runtime_product", f"{product}.extension.compatibility") - if native_product != "liboliphaunt-native": - fail(f"{product}.extension.compatibility.native_runtime_product must be 'liboliphaunt-native'") - if wasix_product != "liboliphaunt-wasix": - fail(f"{product}.extension.compatibility.wasix_runtime_product must be 'liboliphaunt-wasix'") - native_version = _string_field(compatibility, "native_runtime_version", f"{product}.extension.compatibility") - wasix_version = _string_field(compatibility, "wasix_runtime_version", f"{product}.extension.compatibility") - expected_native_version = read_current_version(native_product) - expected_wasix_version = read_current_version(wasix_product) - if native_version != expected_native_version: - fail( - f"{product}.extension.compatibility.native_runtime_version must be " - f"{expected_native_version!r}, got {native_version!r}" - ) - if wasix_version != expected_wasix_version: - fail( - f"{product}.extension.compatibility.wasix_runtime_version must be " - f"{expected_wasix_version!r}, got {wasix_version!r}" - ) - + row = _extension_metadata_row(product) + compatibility = _metadata_object(row, "compatibility", product) + for key in [ + "postgresMajor", + "extensionRuntimeContract", + "nativeRuntimeProduct", + "nativeRuntimeVersion", + "wasixRuntimeProduct", + "wasixRuntimeVersion", + ]: + if not isinstance(compatibility.get(key), str) or not compatibility[key]: + fail(f"extension-metadata {product}.compatibility.{key} must be a non-empty string") return { - "sqlName": sql_name, - "class": extension_class, - "versioning": versioning, - "sourcePath": source_path, - "compatibility": { - "postgresMajor": postgres_major, - "extensionRuntimeContract": contract_path, - "nativeRuntimeProduct": native_product, - "nativeRuntimeVersion": native_version, - "wasixRuntimeProduct": wasix_product, - "wasixRuntimeVersion": wasix_version, - }, + "sqlName": _metadata_string(row, "sqlName", product), + "class": _metadata_string(row, "class", product), + "versioning": _metadata_string(row, "versioning", product), + "sourcePath": _metadata_string(row, "sourcePath", product), + "compatibility": compatibility, } def extension_source_identity(product: str, graph: dict | None = None) -> dict[str, Any]: - metadata = extension_metadata(product) - source_path = metadata["sourcePath"] - source = _read_toml(ROOT / source_path) - extension_class = metadata["class"] - if extension_class == "contrib": - postgresql = source.get("postgresql") - if not isinstance(postgresql, dict): - fail(f"{source_path} must declare [postgresql] for contrib extension products") - return { - "kind": "postgres-contrib", - "name": "postgresql", - "version": _string_field(postgresql, "version", source_path), - "url": _string_field(postgresql, "url", source_path), - "sha256": _string_field(postgresql, "sha256", source_path), - } - if extension_class == "external": - return { - "kind": "external", - "name": _string_field(source, "name", source_path), - "url": _string_field(source, "url", source_path), - "branch": _string_field(source, "branch", source_path), - "commit": _string_field(source, "commit", source_path), - } - if extension_class == "first-party": - return { - "kind": "repo", - "name": metadata["sqlName"], - "path": source_path, - "version": read_current_version(product), - } - fail(f"{product}.extension.class has unsupported source identity class {extension_class!r}") + return _metadata_object(_extension_metadata_row(product), "sourceIdentity", product) def validate_extension_metadata(product: str, graph: dict | None = None) -> None: diff --git a/tools/release/release-artifact-targets.mjs b/tools/release/release-artifact-targets.mjs index 4504aeda..871fffad 100644 --- a/tools/release/release-artifact-targets.mjs +++ b/tools/release/release-artifact-targets.mjs @@ -1,5 +1,4 @@ import { existsSync, readFileSync } from "node:fs"; -import fs from "node:fs/promises"; import path from "node:path"; import { loadGraph } from "./release-graph.mjs"; @@ -94,6 +93,13 @@ const PRODUCT_PRESETS = { 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 EXTENSION_VERSIONING_BY_CLASS = { + contrib: "postgres-bound", + external: "upstream-bound", + "first-party": "repo-bound", +}; +const EXTENSION_RUNTIME_CONTRACT_PATH = "src/shared/extension-runtime-contract/contract.toml"; +const POSTGRES18_SOURCE_PATH = "src/postgres/versions/18/source.toml"; const graphCache = new Map(); @@ -651,49 +657,51 @@ function parseCargoVersion(text, file, prefix) { 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}`); - } -} +const versionCache = new Map(); -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; +export function currentProductVersionSync(product, prefix = "release-artifact-targets.mjs") { + const key = `${prefix}\0${product}`; + if (!versionCache.has(key)) { + const versionFile = productConfig(product, prefix).version_files?.[0]; + if (typeof versionFile !== "string" || !versionFile) { + fail(prefix, `${product} does not declare a canonical version file`); } - } else if (path.basename(versionFile) === "VERSION") { - const version = text.trim(); - if (version) { - return version; + const file = path.join(ROOT, versionFile); + const text = readFileSync(file, "utf8"); + const name = path.basename(file); + let version = ""; + if (name === "Cargo.toml") { + version = parseCargoVersion(text, file, prefix); + } else if (name === "package.json" || name === "jsr.json") { + const data = JSON.parse(text); + version = typeof data.version === "string" ? data.version : ""; + } else if (name === "gradle.properties") { + for (const rawLine of text.split(/\r?\n/u)) { + const line = rawLine.trim(); + if (!line || line.startsWith("#") || !line.includes("=")) { + continue; + } + const [property, ...rest] = line.split("="); + if (property.trim() === "VERSION_NAME") { + version = rest.join("=").trim(); + break; + } + } + } else if (name === "VERSION" || name === "LIBOLIPHAUNT_VERSION") { + version = text.trim(); + } else { + fail(prefix, `${product}.version_files has unsupported version file type: ${versionFile}`); + } + if (!version) { + fail(prefix, `${versionFile} does not define a release version for ${product}`); } + versionCache.set(key, version); } - fail(prefix, `${rel(file)} does not define a release version for ${product}`); + return versionCache.get(key); +} + +export async function currentProductVersion(product, prefix = "release-artifact-targets.mjs") { + return currentProductVersionSync(product, prefix); } export function expectedAssets(product, kind, version, prefix) { @@ -704,6 +712,14 @@ export function expectedAssets(product, kind, version, prefix) { return assets.sort(compareText); } +function productConfig(product, prefix) { + const config = graph(prefix).products[product]; + if (!config) { + fail(prefix, `unknown release product ${product}`); + } + return config; +} + export function exactExtensionProducts(prefix = "release-artifact-targets.mjs") { return Object.entries(graph(prefix).products) .filter(([, config]) => config.kind === "exact-extension-artifact") @@ -711,14 +727,162 @@ export function exactExtensionProducts(prefix = "release-artifact-targets.mjs") .sort(compareText); } -function extensionSqlName(product, prefix) { - const value = graph(prefix).products[product]?.extension_sql_name; +export function extensionSqlName(product, prefix = "release-artifact-targets.mjs") { + const value = productConfig(product, prefix).extension_sql_name; if (typeof value !== "string" || !value) { fail(prefix, `${product} release.toml must declare extension_sql_name`); } return value; } +function releaseMetadataRelativePath(value, context, prefix) { + const candidate = path.normalize(value).split(path.sep).join("/"); + if (path.isAbsolute(value) || candidate.split("/").includes("..")) { + fail(prefix, `${context} must be a repository-relative path: ${JSON.stringify(value)}`); + } + if (!existsSync(path.join(ROOT, candidate))) { + fail(prefix, `${context} path does not exist: ${candidate}`); + } + return candidate; +} + +function packagePath(product, prefix) { + return releaseMetadataRelativePath( + nonEmptyString(productConfig(product, prefix).path, `${product}.path`, prefix), + `${product}.path`, + prefix, + ); +} + +export function extensionMetadata(product, prefix = "release-artifact-targets.mjs") { + const config = productConfig(product, prefix); + if (config.kind !== "exact-extension-artifact") { + fail(prefix, `${product} is not an exact-extension artifact product`); + } + const topLevelSqlName = extensionSqlName(product, prefix); + const metadata = config.extension; + if (metadata === null || Array.isArray(metadata) || typeof metadata !== "object") { + fail(prefix, `${product} release metadata must declare [extension]`); + } + const sqlName = nonEmptyString(metadata.sql_name, `${product}.extension.sql_name`, prefix); + if (sqlName !== topLevelSqlName) { + fail(prefix, `${product}.extension.sql_name ${JSON.stringify(sqlName)} must match extension_sql_name ${JSON.stringify(topLevelSqlName)}`); + } + const extensionClass = nonEmptyString(metadata.class, `${product}.extension.class`, prefix); + if (!(extensionClass in EXTENSION_VERSIONING_BY_CLASS)) { + fail(prefix, `${product}.extension.class must be one of ${Object.keys(EXTENSION_VERSIONING_BY_CLASS).sort(compareText).join(", ")}`); + } + const versioning = nonEmptyString(metadata.versioning, `${product}.extension.versioning`, prefix); + const expectedVersioning = EXTENSION_VERSIONING_BY_CLASS[extensionClass]; + if (versioning !== expectedVersioning) { + fail(prefix, `${product}.extension.versioning must be ${JSON.stringify(expectedVersioning)} for class ${JSON.stringify(extensionClass)}, got ${JSON.stringify(versioning)}`); + } + const source = metadata.source; + if (source === null || Array.isArray(source) || typeof source !== "object") { + fail(prefix, `${product}.extension must declare [extension.source]`); + } + const sourcePath = releaseMetadataRelativePath( + nonEmptyString(source.path, `${product}.extension.source.path`, prefix), + `${product}.extension.source.path`, + prefix, + ); + const packageRoot = packagePath(product, prefix); + if (extensionClass === "contrib" && sourcePath !== POSTGRES18_SOURCE_PATH) { + fail(prefix, `${product}.extension.source.path must be ${JSON.stringify(POSTGRES18_SOURCE_PATH)} for contrib extensions`); + } + if (extensionClass === "external" && sourcePath !== `${packageRoot}/source.toml`) { + fail(prefix, `${product}.extension.source.path must be ${packageRoot}/source.toml for external extensions`); + } + if (extensionClass === "first-party" && !(sourcePath === packageRoot || sourcePath.startsWith(`${packageRoot}/`))) { + fail(prefix, `${product}.extension.source.path must stay inside ${packageRoot}/ for first-party extensions`); + } + + const compatibility = metadata.compatibility; + if (compatibility === null || Array.isArray(compatibility) || typeof compatibility !== "object") { + fail(prefix, `${product}.extension must declare [extension.compatibility]`); + } + const postgresMajor = nonEmptyString(compatibility.postgres_major, `${product}.extension.compatibility.postgres_major`, prefix); + if (postgresMajor !== "18") { + fail(prefix, `${product}.extension.compatibility.postgres_major must be '18', got ${JSON.stringify(postgresMajor)}`); + } + const contractPath = releaseMetadataRelativePath( + nonEmptyString(compatibility.extension_runtime_contract, `${product}.extension.compatibility.extension_runtime_contract`, prefix), + `${product}.extension.compatibility.extension_runtime_contract`, + prefix, + ); + if (contractPath !== EXTENSION_RUNTIME_CONTRACT_PATH) { + fail(prefix, `${product}.extension.compatibility.extension_runtime_contract must be ${JSON.stringify(EXTENSION_RUNTIME_CONTRACT_PATH)}`); + } + const nativeProduct = nonEmptyString(compatibility.native_runtime_product, `${product}.extension.compatibility.native_runtime_product`, prefix); + const wasixProduct = nonEmptyString(compatibility.wasix_runtime_product, `${product}.extension.compatibility.wasix_runtime_product`, prefix); + if (nativeProduct !== "liboliphaunt-native") { + fail(prefix, `${product}.extension.compatibility.native_runtime_product must be 'liboliphaunt-native'`); + } + if (wasixProduct !== "liboliphaunt-wasix") { + fail(prefix, `${product}.extension.compatibility.wasix_runtime_product must be 'liboliphaunt-wasix'`); + } + const nativeVersion = nonEmptyString(compatibility.native_runtime_version, `${product}.extension.compatibility.native_runtime_version`, prefix); + const wasixVersion = nonEmptyString(compatibility.wasix_runtime_version, `${product}.extension.compatibility.wasix_runtime_version`, prefix); + const expectedNativeVersion = currentProductVersionSync(nativeProduct, prefix); + const expectedWasixVersion = currentProductVersionSync(wasixProduct, prefix); + if (nativeVersion !== expectedNativeVersion) { + fail(prefix, `${product}.extension.compatibility.native_runtime_version must be ${JSON.stringify(expectedNativeVersion)}, got ${JSON.stringify(nativeVersion)}`); + } + if (wasixVersion !== expectedWasixVersion) { + fail(prefix, `${product}.extension.compatibility.wasix_runtime_version must be ${JSON.stringify(expectedWasixVersion)}, got ${JSON.stringify(wasixVersion)}`); + } + return { + sqlName, + class: extensionClass, + versioning, + sourcePath, + compatibility: { + postgresMajor, + extensionRuntimeContract: contractPath, + nativeRuntimeProduct: nativeProduct, + nativeRuntimeVersion: nativeVersion, + wasixRuntimeProduct: wasixProduct, + wasixRuntimeVersion: wasixVersion, + }, + }; +} + +export function extensionSourceIdentity(product, prefix = "release-artifact-targets.mjs") { + const metadata = extensionMetadata(product, prefix); + const source = Bun.TOML.parse(readFileSync(path.join(ROOT, metadata.sourcePath), "utf8")); + if (metadata.class === "contrib") { + const postgresql = source.postgresql; + if (postgresql === null || Array.isArray(postgresql) || typeof postgresql !== "object") { + fail(prefix, `${metadata.sourcePath} must declare [postgresql] for contrib extension products`); + } + return { + kind: "postgres-contrib", + name: "postgresql", + version: nonEmptyString(postgresql.version, `${metadata.sourcePath}.postgresql.version`, prefix), + url: nonEmptyString(postgresql.url, `${metadata.sourcePath}.postgresql.url`, prefix), + sha256: nonEmptyString(postgresql.sha256, `${metadata.sourcePath}.postgresql.sha256`, prefix), + }; + } + if (metadata.class === "external") { + return { + kind: "external", + name: nonEmptyString(source.name, `${metadata.sourcePath}.name`, prefix), + url: nonEmptyString(source.url, `${metadata.sourcePath}.url`, prefix), + branch: nonEmptyString(source.branch, `${metadata.sourcePath}.branch`, prefix), + commit: nonEmptyString(source.commit, `${metadata.sourcePath}.commit`, prefix), + }; + } + if (metadata.class === "first-party") { + return { + kind: "repo", + name: metadata.sqlName, + path: metadata.sourcePath, + version: currentProductVersionSync(product, prefix), + }; + } + fail(prefix, `${product}.extension.class has unsupported source identity class ${JSON.stringify(metadata.class)}`); +} + function wasixExtensionTargetId(runtimeTarget) { return runtimeTarget === "portable" ? "wasix-portable" : runtimeTarget; } diff --git a/tools/release/release_graph_query.mjs b/tools/release/release_graph_query.mjs index 499480c9..f7ed312d 100644 --- a/tools/release/release_graph_query.mjs +++ b/tools/release/release_graph_query.mjs @@ -2,6 +2,9 @@ import { allArtifactTargets, extensionArtifactTargets, + extensionMetadata, + extensionSourceIdentity, + exactExtensionProducts, rawArtifactTargetRows, } from "./release-artifact-targets.mjs"; import { @@ -265,6 +268,32 @@ function runCompatibilityVersionEntries(argv) { printJson(compatibilityVersionEntries(loadGraph(TOOL).products, { requireSourceProduct, prefix: TOOL })); } +function runExtensionMetadata(argv) { + let product; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === "--product") { + if (index + 1 >= argv.length) { + fail("--product requires a value"); + } + product = argv[index + 1]; + index += 1; + } else if (value.startsWith("--product=")) { + product = value.slice("--product=".length); + } else { + fail(`unknown argument ${value}`); + } + } + const products = product === undefined ? exactExtensionProducts(TOOL) : [product]; + printJson( + products.map((productId) => ({ + product: productId, + ...extensionMetadata(productId, TOOL), + sourceIdentity: extensionSourceIdentity(productId, TOOL), + })), + ); +} + function usage() { return `usage: tools/release/release_graph_query.mjs [options] @@ -277,6 +306,7 @@ Commands: artifact-targets [--product PRODUCT] [--kind KIND] [--surface SURFACE] [--published-only] raw-artifact-targets [--product PRODUCT] [--kind KIND] [--surface SURFACE] [--published-only] extension-targets [--product PRODUCT] [--family native|wasix] [--published-only] + extension-metadata [--product PRODUCT] compatibility-version-entries [--require-source-product] wasix-cargo-artifact-contract `; @@ -300,6 +330,8 @@ function main(argv) { runRawArtifactTargets(rest); } else if (command === "extension-targets") { runExtensionTargets(rest); + } else if (command === "extension-metadata") { + runExtensionMetadata(rest); } else if (command === "compatibility-version-entries") { runCompatibilityVersionEntries(rest); } else if (command === "wasix-cargo-artifact-contract") { From 47dbf2f6b9500eca8b4559000be62805a71ad5cb Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 10:58:08 +0000 Subject: [PATCH 176/308] refactor: query product versions from release graph --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 28 +++ tools/release/check_release_metadata.py | 9 + tools/release/product-version.mjs | 171 +----------------- tools/release/product_metadata.py | 80 ++------ tools/release/release_graph_query.mjs | 29 +++ 5 files changed, 85 insertions(+), 232 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 7915ff07..95e7fff3 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,34 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Moved current product version reads out of the remaining Python + version-file parser compatibility path and into the Bun release graph query. + `tools/release/product-version.mjs` now delegates to + `currentProductVersion`, `tools/release/release_graph_query.mjs` exposes + `product-versions [--product PRODUCT]`, and + `tools/release/product_metadata.py` adapts those rows for legacy Python + callers without local `re`/`tomllib` version parsing. A subagent review was + attempted for the next cleanup slice, but the current session had reached the + agent thread limit, so the audit used local repo evidence instead. Fresh + checks passed: focused `product-version.mjs` and `product-versions` smokes, + full `product-versions` query count/parity smoke across 49 products, Python + `product_metadata.read_current_version` smoke for native, WASIX, JS, and Rust + products, `python3 -m py_compile` for touched Python helpers, parser-removal + `rg` scan, `python3 tools/release/check_release_metadata.py`, `python3 + tools/release/check_consumer_shape.py`, `tools/dev/bun.sh + tools/release/check_release_versions.mjs`, `tools/dev/bun.sh + tools/release/check_github_release_assets.mjs --help`, + `tools/release/release.py check`, `bash + tools/policy/check-tooling-stack.sh`, `bash tools/policy/check-docs.sh`, + `bash examples/tools/check-examples.sh`, `bash + tools/policy/check-policy-tools.sh`, `tools/dev/bun.sh + tools/policy/check-python-entrypoints.mjs --json`, + `tools/release/local_registry_publish.py publish --surface cargo --strict`, + and `tools/release/local_registry_publish.py publish --surface npm --strict`. + The Python entrypoint inventory reported `product_metadata.py` at 827 lines, + and a fresh sweep over 836 local-registry `.crate` files found no crate above + the 10 MiB crates.io limit; the largest remained the split WASIX PostGIS AOT + part crates at 10,212,312 bytes. - 2026-06-27: Moved exact-extension release metadata and source identity parsing out of the Python compatibility layer and the duplicate CI artifact helpers. `tools/release/release-artifact-targets.mjs` now owns diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 7823cd34..44d1150c 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -268,6 +268,15 @@ def validate_graph_files(graph: dict) -> None: or "function extensionSourceIdentity(" in check_staged_artifacts ): fail("extension metadata and source identity must be shared through release-artifact-targets and the Bun release graph query") + if ( + '"product-versions"' not in product_metadata_source + or "product-versions [--product PRODUCT]" not in release_graph_query + or "currentProductVersionSync(" not in release_graph_query + or "parse_version_text(" in product_metadata_source + or "parse_toml_path(" in product_metadata_source + or "import tomllib" in product_metadata_source + ): + fail("current product version values must be read through the Bun release graph product-versions query") def validate_exact_extension_registry_shape(graph: dict) -> None: diff --git a/tools/release/product-version.mjs b/tools/release/product-version.mjs index 585adaa9..89a61d9f 100644 --- a/tools/release/product-version.mjs +++ b/tools/release/product-version.mjs @@ -1,165 +1,17 @@ #!/usr/bin/env bun -import { readFile } from "node:fs/promises"; -import path from "node:path"; +import { currentProductVersion } from "./release-artifact-targets.mjs"; -const ROOT = path.resolve(import.meta.dir, "../.."); -const CONFIG_PATH = path.join(ROOT, "release-please-config.json"); +const TOOL = "product-version.mjs"; function fail(message) { - console.error(`product-version.mjs: ${message}`); + console.error(`${TOOL}: ${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)}`); @@ -168,21 +20,10 @@ function ensureSemver(product, version) { } export 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}`); + if (typeof product !== "string" || product.length === 0) { + fail("product id must be a non-empty string"); } - return ensureSemver(product, version); + return ensureSemver(product, await currentProductVersion(product, TOOL)); } async function main(argv) { diff --git a/tools/release/product_metadata.py b/tools/release/product_metadata.py index 826013b8..4add6f4f 100644 --- a/tools/release/product_metadata.py +++ b/tools/release/product_metadata.py @@ -8,10 +8,8 @@ from __future__ import annotations import json -import re import subprocess import sys -import tomllib from dataclasses import dataclass from functools import lru_cache from pathlib import Path @@ -803,74 +801,22 @@ def release_owned_version_specs(graph: dict | None = None) -> dict[str, tuple[st } -def parse_cargo_version(text: str, path: str) -> str: - in_package = False - for line in text.splitlines(): - stripped = line.strip() - if stripped == "[package]": - in_package = True - continue - if in_package and stripped.startswith("["): - break - if in_package: - match = re.match(r'version\s*=\s*"([^"]+)"', stripped) - if match: - return match.group(1) - return "" - - -def parse_gradle_property(text: str, name: str) -> str: - for raw_line in text.splitlines(): - line = raw_line.strip() - if not line or line.startswith("#") or "=" not in line: - continue - key, value = line.split("=", 1) - if key.strip() == name: - return value.strip() - return "" - - -def parse_json_path(text: str, dotted: str) -> str: - value: object = json.loads(text) - for key in dotted.split("."): - if not isinstance(value, dict) or key not in value: - return "" - value = value[key] - return str(value) - - -def parse_toml_path(text: str, dotted: str) -> str: - value: object = tomllib.loads(text) - for key in dotted.split("."): - if not isinstance(value, dict) or key not in value: - return "" - value = value[key] - return str(value) - - -def parse_version_text(text: str, path: str, parser: str) -> str: - if parser == "raw": - return text.strip() - if parser == "cargo": - return parse_cargo_version(text, path) - if parser.startswith("gradle:"): - return parse_gradle_property(text, parser.split(":", 1)[1]) - if parser.startswith("json:"): - return parse_json_path(text, parser.split(":", 1)[1]) - if parser.startswith("toml:"): - return parse_toml_path(text, parser.split(":", 1)[1]) - if parser.startswith("rust-const:"): - name = re.escape(parser.split(":", 1)[1]) - match = re.search(rf'^\s*(?:pub\s+)?const\s+{name}\s*:\s*&str\s*=\s*"([^"]+)"\s*;', text, re.M) - return match.group(1) if match else "" - fail(f"unknown version parser {parser!r}") +@lru_cache(maxsize=1) +def _product_version_rows() -> tuple[dict[str, Any], ...]: + return _release_graph_query_rows("product-versions") + + +def _product_version_row(product: str) -> dict[str, Any]: + matches = [row for row in _product_version_rows() if row.get("product") == product] + if len(matches) != 1: + fail(f"release graph product-versions query must return one row for {product}, got {len(matches)}") + return dict(matches[0]) def read_current_version(product: str, graph: dict | None = None) -> str: - path, parser = canonical_version_spec(product) - version = parse_version_text((ROOT / path).read_text(encoding="utf-8"), path, parser) - if not version: - fail(f"{path} does not define a release version for {product}") + version = _product_version_row(product).get("version") + if not isinstance(version, str) or not version: + fail(f"release graph product-versions {product}.version must be a non-empty string") return version diff --git a/tools/release/release_graph_query.mjs b/tools/release/release_graph_query.mjs index f7ed312d..df5eff35 100644 --- a/tools/release/release_graph_query.mjs +++ b/tools/release/release_graph_query.mjs @@ -1,6 +1,7 @@ #!/usr/bin/env bun import { allArtifactTargets, + currentProductVersionSync, extensionArtifactTargets, extensionMetadata, extensionSourceIdentity, @@ -268,6 +269,31 @@ function runCompatibilityVersionEntries(argv) { printJson(compatibilityVersionEntries(loadGraph(TOOL).products, { requireSourceProduct, prefix: TOOL })); } +function runProductVersions(argv) { + let product; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === "--product") { + if (index + 1 >= argv.length) { + fail("--product requires a value"); + } + product = argv[index + 1]; + index += 1; + } else if (value.startsWith("--product=")) { + product = value.slice("--product=".length); + } else { + fail(`unknown argument ${value}`); + } + } + const products = product === undefined ? Object.keys(loadGraph(TOOL).products).sort(compareText) : [product]; + printJson( + products.map((productId) => ({ + product: productId, + version: currentProductVersionSync(productId, TOOL), + })), + ); +} + function runExtensionMetadata(argv) { let product; for (let index = 0; index < argv.length; index += 1) { @@ -307,6 +333,7 @@ Commands: raw-artifact-targets [--product PRODUCT] [--kind KIND] [--surface SURFACE] [--published-only] extension-targets [--product PRODUCT] [--family native|wasix] [--published-only] extension-metadata [--product PRODUCT] + product-versions [--product PRODUCT] compatibility-version-entries [--require-source-product] wasix-cargo-artifact-contract `; @@ -332,6 +359,8 @@ function main(argv) { runExtensionTargets(rest); } else if (command === "extension-metadata") { runExtensionMetadata(rest); + } else if (command === "product-versions") { + runProductVersions(rest); } else if (command === "compatibility-version-entries") { runCompatibilityVersionEntries(rest); } else if (command === "wasix-cargo-artifact-contract") { From cf0d165e069cfe79e7d327554242bcfc59e7af9c Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 11:14:43 +0000 Subject: [PATCH 177/308] fix: stage native client tools separately --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 28 +++++++++++++++++++ tools/release/check_consumer_shape.py | 11 ++++++++ tools/release/check_release_metadata.py | 8 ++++++ .../package-liboliphaunt-linux-assets.sh | 6 +++- .../package-liboliphaunt-macos-assets.sh | 6 +++- .../package-liboliphaunt-windows-assets.ps1 | 1 + 6 files changed, 58 insertions(+), 2 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 95e7fff3..affedda9 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,34 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Tightened the native `pg_dump`/`psql` tools split so root native + release staging no longer copies those tools and relies on pruning later. + Linux and macOS release asset packagers now exclude `/bin/pg_dump` and + `/bin/psql` from the root `liboliphaunt` runtime stage while copying them + into `oliphaunt-tools`; the Windows packager removes `pg_dump.exe` and + `psql.exe` from the root stage immediately after staging the tools package. + Release metadata and consumer-shape checks now require that explicit split + in addition to the existing Cargo artifact and npm package validation. Fresh + checks passed: `python3 -m py_compile` for touched Python checks, `python3 + tools/release/check_release_metadata.py`, `python3 + tools/release/check_consumer_shape.py`, synthetic + `optimize_native_runtime_payload.mjs` root/tools validation including a + negative root-with-`pg_dump` check, `tools/release/release.py check`, `bash + tools/policy/check-tooling-stack.sh`, `bash examples/tools/check-examples.sh`, + `bash tools/policy/check-policy-tools.sh`, + `tools/release/local_registry_publish.py publish --surface cargo --strict`, + and `tools/release/local_registry_publish.py publish --surface npm --strict`. + Generated native Cargo extraction trees contained exactly + `runtime/bin/initdb`, `runtime/bin/pg_ctl`, and `runtime/bin/postgres` for + root, and exactly `runtime/bin/pg_dump` plus `runtime/bin/psql` for + `oliphaunt-tools`. WASIX Cargo payload inspection found root portable payload + files `bin/initdb.wasix.wasm`, `manifest.json`, and + `oliphaunt.wasix.tar.zst`; the nested archive contained only + `oliphaunt/bin/initdb` and `oliphaunt/bin/postgres`; and + `oliphaunt-wasix-tools` contained exactly `bin/pg_dump.wasix.wasm` and + `bin/psql.wasix.wasm`. A fresh sweep over 836 local-registry `.crate` files + found no crate above the 10 MiB crates.io limit; the largest remained the + split WASIX PostGIS AOT part crates at 10,212,312 bytes. - 2026-06-27: Moved current product version reads out of the remaining Python version-file parser compatibility path and into the Bun release graph query. `tools/release/product-version.mjs` now delegates to diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 29b3c6a4..4d1c6160 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -468,6 +468,9 @@ def check_liboliphaunt(findings: list[Finding]) -> None: ) native_packager = read_text("tools/release/package-liboliphaunt-cargo-artifacts.mjs") native_optimizer = read_text("tools/release/optimize_native_runtime_payload.mjs") + native_linux_packager = read_text("tools/release/package-liboliphaunt-linux-assets.sh") + native_macos_packager = read_text("tools/release/package-liboliphaunt-macos-assets.sh") + native_windows_packager = read_text("tools/release/package-liboliphaunt-windows-assets.ps1") 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( @@ -484,6 +487,11 @@ def check_liboliphaunt(findings: list[Finding]) -> None: "liboliphaunt-native-tool-split", set(NATIVE_RUNTIME_TOOL_STEMS) == {"initdb", "pg_ctl", "postgres"} and set(NATIVE_TOOLS_TOOL_STEMS) == {"pg_dump", "psql"} + and "--exclude '/bin/pg_dump'" in native_linux_packager + and "--exclude '/bin/psql'" in native_linux_packager + and "--exclude '/bin/pg_dump'" in native_macos_packager + and "--exclude '/bin/psql'" in native_macos_packager + and 'Remove-Item -Force (Join-Path (Join-Path $Stage "runtime/bin") $Tool)' in native_windows_packager and "missing oliphaunt-tools native release asset" in native_packager and "extractArchive(toolsArchive, toolsRoot)" in native_packager and "validateToolsTargetPair" in native_packager @@ -516,6 +524,9 @@ def check_liboliphaunt(findings: list[Finding]) -> None: "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.mjs", + "tools/release/package-liboliphaunt-linux-assets.sh", + "tools/release/package-liboliphaunt-macos-assets.sh", + "tools/release/package-liboliphaunt-windows-assets.ps1", "tools/release/package-liboliphaunt-cargo-artifacts.mjs", "tools/release/release.py", *native_runtime_package_split_failures, diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 44d1150c..baa0b562 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1556,10 +1556,18 @@ 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.mjs") native_optimizer_source = read_text("tools/release/optimize_native_runtime_payload.mjs") + native_linux_packager_source = read_text("tools/release/package-liboliphaunt-linux-assets.sh") + native_macos_packager_source = read_text("tools/release/package-liboliphaunt-macos-assets.sh") + native_windows_packager_source = read_text("tools/release/package-liboliphaunt-windows-assets.ps1") if ( 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 "--exclude '/bin/pg_dump'" not in native_linux_packager_source + or "--exclude '/bin/psql'" not in native_linux_packager_source + or "--exclude '/bin/pg_dump'" not in native_macos_packager_source + or "--exclude '/bin/psql'" not in native_macos_packager_source + or 'Remove-Item -Force (Join-Path (Join-Path $Stage "runtime/bin") $Tool)' not in native_windows_packager_source or "missing oliphaunt-tools native release asset" not in native_packager_source or "extractArchive(toolsArchive, toolsRoot)" not in native_packager_source or "validateToolsTargetPair" not in native_packager_source diff --git a/tools/release/package-liboliphaunt-linux-assets.sh b/tools/release/package-liboliphaunt-linux-assets.sh index a3a78bf9..1c53a1f2 100755 --- a/tools/release/package-liboliphaunt-linux-assets.sh +++ b/tools/release/package-liboliphaunt-linux-assets.sh @@ -76,7 +76,11 @@ oliphaunt_assert_base_runtime_has_no_optional_extensions "$catalog_file" "$runti 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/" +rsync -a --delete \ + --exclude '/bin/pg_dump' \ + --exclude '/bin/psql' \ + --exclude 'share/icu/***' \ + "$runtime/" "$stage/runtime/" for tool in pg_dump psql; do cp -p "$runtime/bin/$tool" "$tools_stage/runtime/bin/" done diff --git a/tools/release/package-liboliphaunt-macos-assets.sh b/tools/release/package-liboliphaunt-macos-assets.sh index 44c98911..24033f0a 100755 --- a/tools/release/package-liboliphaunt-macos-assets.sh +++ b/tools/release/package-liboliphaunt-macos-assets.sh @@ -68,7 +68,11 @@ oliphaunt_assert_base_runtime_has_no_optional_extensions "$catalog_file" "$runti 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/" +rsync -a --delete \ + --exclude '/bin/pg_dump' \ + --exclude '/bin/psql' \ + --exclude 'share/icu/***' \ + "$runtime/" "$stage/runtime/" for tool in pg_dump psql; do cp -p "$runtime/bin/$tool" "$tools_stage/runtime/bin/" done diff --git a/tools/release/package-liboliphaunt-windows-assets.ps1 b/tools/release/package-liboliphaunt-windows-assets.ps1 index 4947d0ce..8e62fb17 100644 --- a/tools/release/package-liboliphaunt-windows-assets.ps1 +++ b/tools/release/package-liboliphaunt-windows-assets.ps1 @@ -140,6 +140,7 @@ Copy-Item -Recurse -Force (Join-Path $EmbeddedModules "*") (Join-Path $Stage "li 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) + Remove-Item -Force (Join-Path (Join-Path $Stage "runtime/bin") $Tool) } $StagedIcu = Join-Path $Stage "runtime/share/icu" if (Test-Path $StagedIcu) { From 5384de437bef7582eca0eafa52e57a2f6d3225ac Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 11:30:02 +0000 Subject: [PATCH 178/308] refactor: remove dead product metadata version specs --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 23 ++++++++++++- tools/release/check_release_metadata.py | 4 +++ tools/release/product_metadata.py | 34 ------------------- 3 files changed, 26 insertions(+), 35 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index affedda9..c97f5d01 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -66,7 +66,7 @@ until the current-state gates here are checked with fresh local evidence. - [x] Run targeted dead-code detection for Rust, TypeScript/JavaScript, shell, Python, and release helpers. -- [ ] Remove only confirmed dead code with reference evidence. +- [x] Remove only confirmed dead code with reference evidence. - [x] 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. @@ -78,6 +78,27 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Removed unused Python version-spec compatibility helpers after a + repo reference scan found no callers for `parser_for_version_file`, + `canonical_version_spec`, `product_version_specs`, or + `release_owned_version_specs` outside their own internal chain. The + release-metadata check now rejects reintroducing those helpers so current + product version values stay behind the Bun release graph `product-versions` + query. A subagent review was attempted for the next cleanup slice, but the + current session had reached the agent thread limit, so this pass used local + repo evidence instead. Fresh checks passed: parser-removal `rg` scan, + `python3 -m py_compile` for touched Python helpers, `python3 + tools/release/check_release_metadata.py`, `python3 + tools/release/check_consumer_shape.py`, `tools/release/release.py check`, + `bash tools/policy/check-tooling-stack.sh`, + `tools/dev/bun.sh tools/policy/check-python-entrypoints.mjs --json`, + `tools/release/local_registry_publish.py publish --surface cargo --strict`, + and `tools/release/local_registry_publish.py publish --surface npm --strict`. + The Python entrypoint inventory still reported 9 Python entrypoints, with + `product_metadata.py` reduced to 793 lines and 27,997 bytes. A fresh Cargo + local-registry sweep covered 836 `.crate` files with no crate above the 10 + MiB crates.io limit; the largest generated crate remained a split WASIX + PostGIS AOT part at 10,212,312 bytes. - 2026-06-27: Tightened the native `pg_dump`/`psql` tools split so root native release staging no longer copies those tools and relies on pruning later. Linux and macOS release asset packagers now exclude `/bin/pg_dump` and diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index baa0b562..a2c25089 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -274,6 +274,10 @@ def validate_graph_files(graph: dict) -> None: or "currentProductVersionSync(" not in release_graph_query or "parse_version_text(" in product_metadata_source or "parse_toml_path(" in product_metadata_source + or "parser_for_version_file(" in product_metadata_source + or "canonical_version_spec(" in product_metadata_source + or "product_version_specs(" in product_metadata_source + or "release_owned_version_specs(" in product_metadata_source or "import tomllib" in product_metadata_source ): fail("current product version values must be read through the Bun release graph product-versions query") diff --git a/tools/release/product_metadata.py b/tools/release/product_metadata.py index 4add6f4f..f136b214 100644 --- a/tools/release/product_metadata.py +++ b/tools/release/product_metadata.py @@ -720,33 +720,6 @@ def tag_prefix(product: str, graph: dict | None = None) -> str: return _graph_string(product_config(product, graph), "tag_prefix", product) -def parser_for_version_file(product: str, path: str) -> str: - name = Path(path).name - if name == "Cargo.toml": - return "cargo" - if name == "package.json": - return "json:version" - if name == "gradle.properties": - return "gradle:VERSION_NAME" - if name in {"VERSION", "LIBOLIPHAUNT_VERSION"}: - return "raw" - if name == "jsr.json": - return "json:version" - fail(f"{product}.version_files has unsupported version file type: {path}") - - -def canonical_version_spec(product: str, graph: dict | None = None) -> tuple[str, str]: - path = version_files(product)[0] - return path, parser_for_version_file(product, path) - - -def product_version_specs(graph: dict | None = None) -> dict[str, tuple[str, str]]: - return { - product: canonical_version_spec(product) - for product in graph_products() - } - - def _compatibility_version_entries(*, require_source_product: bool) -> dict[str, tuple[str | None, str, str]]: rows = _release_graph_query_rows( "compatibility-version-entries", @@ -794,13 +767,6 @@ def compatibility_version_links(graph: dict | None = None) -> dict[str, tuple[st } -def release_owned_version_specs(graph: dict | None = None) -> dict[str, tuple[str, str]]: - return { - **product_version_specs(), - **compatibility_version_specs(), - } - - @lru_cache(maxsize=1) def _product_version_rows() -> tuple[dict[str, Any], ...]: return _release_graph_query_rows("product-versions") From 63a311d565186361b2dea943400d70feaae05d56 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 11:44:56 +0000 Subject: [PATCH 179/308] refactor: retire compatibility version metadata adapters --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 26 ++++++++++ tools/release/check_release_metadata.py | 6 ++- tools/release/product_metadata.py | 47 ------------------- 3 files changed, 30 insertions(+), 49 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index c97f5d01..d6937250 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,32 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Retired the unused Python compatibility-version metadata adapter + after a repo reference scan found no callers for + `_compatibility_version_entries`, `compatibility_version_specs`, or + `compatibility_version_links` outside their own internal chain. Compatibility + version sync now stays directly on the Bun release graph: + `release_graph_query.mjs compatibility-version-entries` remains the query + surface, and `sync-release-pr.mjs` consumes `compatibilityVersionEntries` + without Python wrapping. The release-metadata check now rejects reintroducing + those Python wrappers while still requiring the Bun query and sync-release-pr + integration. A subagent review was attempted for the next cleanup slice, but + the current session had reached the agent thread limit, so this pass used + local repo evidence instead. Fresh checks passed: wrapper-removal `rg` scan, + `python3 -m py_compile` for touched Python helpers, + `tools/dev/bun.sh tools/release/release_graph_query.mjs + compatibility-version-entries --require-source-product`, `python3 + tools/release/check_release_metadata.py`, `python3 + tools/release/check_consumer_shape.py`, `tools/release/release.py check`, + `bash tools/policy/check-tooling-stack.sh`, + `tools/dev/bun.sh tools/policy/check-python-entrypoints.mjs --json`, + `tools/release/local_registry_publish.py publish --surface cargo --strict`, + and `tools/release/local_registry_publish.py publish --surface npm --strict`. + The Python entrypoint inventory still reported 9 Python entrypoints, with + `product_metadata.py` reduced to 746 lines and 25,699 bytes. A fresh Cargo + local-registry sweep covered 836 `.crate` files with no crate above the 10 + MiB crates.io limit; the largest generated crate remained a split WASIX + PostGIS AOT part at 10,212,312 bytes. - 2026-06-27: Removed unused Python version-spec compatibility helpers after a repo reference scan found no callers for `parser_for_version_file`, `canonical_version_spec`, `product_version_specs`, or diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index a2c25089..ca74b8e3 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -251,8 +251,10 @@ def validate_graph_files(graph: dict) -> None: build_extension_ci_artifacts = read_text("tools/release/build-extension-ci-artifacts.mjs") check_staged_artifacts = read_text("tools/release/check-staged-artifacts.mjs") if ( - '"compatibility-version-entries"' not in product_metadata_source - or "_release_metadata(product).get(\"compatibility_versions\"" in product_metadata_source + "_release_metadata(product).get(\"compatibility_versions\"" in product_metadata_source + or "_compatibility_version_entries(" in product_metadata_source + or "compatibility_version_specs(" in product_metadata_source + or "compatibility_version_links(" in product_metadata_source or "compatibility-version-entries [--require-source-product]" not in release_graph_query or "compatibilityVersionEntries(graphProducts()" not in sync_release_pr ): diff --git a/tools/release/product_metadata.py b/tools/release/product_metadata.py index f136b214..943a39a1 100644 --- a/tools/release/product_metadata.py +++ b/tools/release/product_metadata.py @@ -720,53 +720,6 @@ def tag_prefix(product: str, graph: dict | None = None) -> str: return _graph_string(product_config(product, graph), "tag_prefix", product) -def _compatibility_version_entries(*, require_source_product: bool) -> dict[str, tuple[str | None, str, str]]: - rows = _release_graph_query_rows( - "compatibility-version-entries", - ("--require-source-product",) if require_source_product else (), - ) - specs: dict[str, tuple[str | None, str, str]] = {} - for row in rows: - spec_id = row.get("id") - product = row.get("product") - source_product = row.get("sourceProduct") - path = row.get("path") - parser = row.get("parser") - if not isinstance(spec_id, str) or not spec_id: - fail("compatibility-version-entries rows must declare a non-empty id") - if not isinstance(product, str) or not product: - fail(f"compatibility-version-entries {spec_id}.product must be a non-empty string") - if require_source_product and (not isinstance(source_product, str) or not source_product): - fail(f"compatibility-version-entries {spec_id}.sourceProduct must be a non-empty string") - if source_product is not None and not isinstance(source_product, str): - fail(f"compatibility-version-entries {spec_id}.sourceProduct must be a string or null") - if not isinstance(path, str) or not path: - fail(f"compatibility-version-entries {spec_id}.path must be a non-empty string") - if not isinstance(parser, str) or not parser: - fail(f"compatibility-version-entries {spec_id}.parser must be a non-empty string") - if not (ROOT / path).is_file(): - fail(f"compatibility-version-entries {spec_id} path does not exist: {path}") - specs[spec_id] = (source_product, path, parser) - return specs - - -def compatibility_version_specs(graph: dict | None = None) -> dict[str, tuple[str, str]]: - return { - spec_id: (path, parser) - for spec_id, (_, path, parser) in _compatibility_version_entries(require_source_product=False).items() - } - - -def compatibility_version_links(graph: dict | None = None) -> dict[str, tuple[str, str, str]]: - return { - spec_id: (source_product, path, parser) - for spec_id, (source_product, path, parser) in _compatibility_version_entries( - require_source_product=True - ).items() - if source_product is not None - } - - @lru_cache(maxsize=1) def _product_version_rows() -> tuple[dict[str, Any], ...]: return _release_graph_query_rows("product-versions") From 33c818ddc0406c2980826cfbba99e53b6d9399d3 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 12:04:15 +0000 Subject: [PATCH 180/308] refactor: share typescript optional runtime selection --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 31 +++++++++++ tools/release/check_release_metadata.py | 10 ++++ tools/release/product_metadata.py | 54 +++++++++---------- tools/release/release-artifact-targets.mjs | 37 +++++++++++++ tools/release/release_graph_query.mjs | 16 ++++++ tools/release/sync-release-pr.mjs | 38 ++----------- 6 files changed, 123 insertions(+), 63 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index d6937250..87628a1b 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,37 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Centralized TypeScript optional runtime package selection in + `release-artifact-targets.mjs` so release sync, Python metadata adapters, and + validation share one artifact-target-backed source for broker, native runtime, + native tools, and node-direct optional packages. `release_graph_query.mjs + typescript-optional-runtime-package-versions` now returns 16 package/version + rows, including the separate `@oliphaunt/tools-*` packages; `sync-release-pr.mjs` + consumes the shared selector; and `product_metadata.py` only adapts the query + rows instead of recomputing the selector in Python. `check_release_metadata.py` + now rejects reintroducing a Python selector or a local sync-release-pr selector + for this package set. A subagent review was attempted for the next cleanup + slice, but the current session had reached the agent thread limit, so this + pass used local repo evidence instead. Fresh checks passed: selector-removal + `rg` scan, `tools/dev/bun.sh tools/release/release_graph_query.mjs + typescript-optional-runtime-package-versions`, Python smoke for + `typescript_optional_runtime_package_versions`, `python3 -m py_compile` for + touched Python helpers, `tools/dev/bun.sh + tools/release/sync-release-pr.mjs --check`, `python3 + tools/release/check_release_metadata.py`, `python3 + tools/release/check_consumer_shape.py`, `tools/release/release.py check`, + `bash tools/policy/check-tooling-stack.sh`, `bash + tools/policy/check-policy-tools.sh`, `tools/release/local_registry_publish.py + publish --surface cargo --strict`, and + `tools/release/local_registry_publish.py publish --surface npm --strict`. + The Python entrypoint inventory still reported 9 Python entrypoints, with + `product_metadata.py` at 744 lines and 25,873 bytes. A fresh Cargo + local-registry sweep covered 836 `.crate` files with no crate above the 10 + MiB crates.io limit; the largest generated crates were split WASIX PostGIS AOT + part crates at 10,212,312 bytes, and the hard over-limit query returned no + crates. The strict npm publish included `@oliphaunt/liboliphaunt-linux-x64-gnu`, + `@oliphaunt/tools-linux-x64-gnu`, `@oliphaunt/icu`, `@oliphaunt/ts`, broker, + node-direct, and native extension packages from the local Verdaccio registry. - 2026-06-27: Retired the unused Python compatibility-version metadata adapter after a repo reference scan found no callers for `_compatibility_version_entries`, `compatibility_version_specs`, or diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index ca74b8e3..e2025fd1 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -283,6 +283,16 @@ def validate_graph_files(graph: dict) -> None: or "import tomllib" in product_metadata_source ): fail("current product version values must be read through the Bun release graph product-versions query") + if ( + "typescript_optional_runtime_package_products(" in product_metadata_source + or "typescript-broker" in product_metadata_source + or "function typescriptOptionalRuntimePackageProducts(" in sync_release_pr + or "export function typescriptOptionalRuntimePackageProducts(" not in release_artifact_targets + or "typescriptOptionalRuntimePackageProducts(PREFIX)" not in sync_release_pr + or "typescript-optional-runtime-package-versions" not in release_graph_query + or "typescriptOptionalRuntimePackageProducts(TOOL)" not in release_graph_query + ): + fail("TypeScript optional runtime package selection must come from the shared Bun artifact target helper") def validate_exact_extension_registry_shape(graph: dict) -> None: diff --git a/tools/release/product_metadata.py b/tools/release/product_metadata.py index 943a39a1..0aa3b9df 100644 --- a/tools/release/product_metadata.py +++ b/tools/release/product_metadata.py @@ -462,37 +462,35 @@ def ci_sdk_package_artifact_names(product: str | None = None) -> list[str]: 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 = [ - ("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: - fail(f"{product} has no published {kind} TypeScript optional package targets") - for target in targets: - if target.npm_package is None: - fail(f"{target.id} must declare npm_package for TypeScript optional dependencies") - if target.npm_package in package_products: - fail(f"duplicate TypeScript optional package target {target.npm_package}") - package_products[target.npm_package] = target.product - return dict(sorted(package_products.items())) +@lru_cache(maxsize=1) +def _typescript_optional_runtime_package_version_rows() -> tuple[dict[str, Any], ...]: + return _release_graph_query_rows("typescript-optional-runtime-package-versions") def typescript_optional_runtime_package_versions() -> dict[str, str]: - return { - package_name: read_current_version(product) - for package_name, product in typescript_optional_runtime_package_products().items() - } + versions: dict[str, str] = {} + for row in _typescript_optional_runtime_package_version_rows(): + package_name = row.get("packageName") + product = row.get("product") + version = row.get("version") + artifact_target = row.get("artifactTarget") + if not isinstance(package_name, str) or not package_name: + fail("typescript-optional-runtime-package-versions rows must declare a non-empty packageName") + if not isinstance(product, str) or not product: + fail(f"typescript-optional-runtime-package-versions {package_name}.product must be a non-empty string") + if not isinstance(version, str) or not version: + fail(f"typescript-optional-runtime-package-versions {package_name}.version must be a non-empty string") + if not isinstance(artifact_target, str) or not artifact_target: + fail( + f"typescript-optional-runtime-package-versions {package_name}.artifactTarget " + "must be a non-empty string" + ) + if package_name in versions: + fail(f"duplicate TypeScript optional runtime package target {package_name}") + versions[package_name] = version + if not versions: + fail("release graph returned no TypeScript optional runtime package versions") + return versions def graph_products(graph: dict | None = None) -> dict[str, dict[str, Any]]: diff --git a/tools/release/release-artifact-targets.mjs b/tools/release/release-artifact-targets.mjs index 871fffad..f027c617 100644 --- a/tools/release/release-artifact-targets.mjs +++ b/tools/release/release-artifact-targets.mjs @@ -606,6 +606,43 @@ export function allArtifactTargets( }); } +export function typescriptOptionalRuntimePackageProducts(prefix = "release-artifact-targets.mjs") { + const selected = allArtifactTargets({ publishedOnly: true }, prefix).filter((target) => { + if (target.product === "oliphaunt-broker" && target.kind === "broker-helper") { + return target.surfaces.includes("typescript-broker"); + } + if (target.product === "liboliphaunt-native" && ["native-runtime", "native-tools"].includes(target.kind)) { + return target.surfaces.includes("typescript-native-direct"); + } + if (target.product === "oliphaunt-node-direct" && target.kind === "node-direct-addon") { + return target.surfaces.includes("npm-optional"); + } + return false; + }); + if (selected.length === 0) { + fail(prefix, "no TypeScript optional runtime package targets found"); + } + const rows = []; + const seen = new Set(); + for (const target of selected) { + if (typeof target.npmPackage !== "string" || !target.npmPackage) { + fail(prefix, `${target.id} must declare npmPackage for TypeScript optional dependencies`); + } + if (seen.has(target.npmPackage)) { + fail(prefix, `duplicate TypeScript optional package target ${target.npmPackage}`); + } + seen.add(target.npmPackage); + rows.push({ + packageName: target.npmPackage, + product: target.product, + target: target.target, + kind: target.kind, + artifactTarget: target.id, + }); + } + return rows.sort((left, right) => compareText(left.packageName, right.packageName)); +} + export function artifactTargets(product, kind, prefix) { return allArtifactTargets({ product, kind, publishedOnly: true }, prefix); } diff --git a/tools/release/release_graph_query.mjs b/tools/release/release_graph_query.mjs index df5eff35..8040fd9b 100644 --- a/tools/release/release_graph_query.mjs +++ b/tools/release/release_graph_query.mjs @@ -7,6 +7,7 @@ import { extensionSourceIdentity, exactExtensionProducts, rawArtifactTargetRows, + typescriptOptionalRuntimePackageProducts, } from "./release-artifact-targets.mjs"; import { buildPlan, @@ -294,6 +295,18 @@ function runProductVersions(argv) { ); } +function runTypescriptOptionalRuntimePackageVersions(argv) { + for (const value of argv) { + fail(`unknown argument ${value}`); + } + printJson( + typescriptOptionalRuntimePackageProducts(TOOL).map((row) => ({ + ...row, + version: currentProductVersionSync(row.product, TOOL), + })), + ); +} + function runExtensionMetadata(argv) { let product; for (let index = 0; index < argv.length; index += 1) { @@ -334,6 +347,7 @@ Commands: extension-targets [--product PRODUCT] [--family native|wasix] [--published-only] extension-metadata [--product PRODUCT] product-versions [--product PRODUCT] + typescript-optional-runtime-package-versions compatibility-version-entries [--require-source-product] wasix-cargo-artifact-contract `; @@ -361,6 +375,8 @@ function main(argv) { runExtensionMetadata(rest); } else if (command === "product-versions") { runProductVersions(rest); + } else if (command === "typescript-optional-runtime-package-versions") { + runTypescriptOptionalRuntimePackageVersions(rest); } else if (command === "compatibility-version-entries") { runCompatibilityVersionEntries(rest); } else if (command === "wasix-cargo-artifact-contract") { diff --git a/tools/release/sync-release-pr.mjs b/tools/release/sync-release-pr.mjs index e34826c9..89179a90 100644 --- a/tools/release/sync-release-pr.mjs +++ b/tools/release/sync-release-pr.mjs @@ -12,11 +12,11 @@ import path from "node:path"; import { ROOT, - allArtifactTargets, compareText, currentProductVersion, exactExtensionProducts, extensionArtifactTargets, + typescriptOptionalRuntimePackageProducts, } from "./release-artifact-targets.mjs"; import { compatibilityVersionEntries, loadGraph } from "./release-graph.mjs"; @@ -305,46 +305,14 @@ async function syncCompatibilityVersions(changes, { write }) { async function expectedTypescriptOptionalRuntimeVersions() { const versions = {}; - for (const [packageName, product] of typescriptOptionalRuntimePackageProducts()) { + for (const { packageName, product } of typescriptOptionalRuntimePackageProducts(PREFIX)) { versions[packageName] = `workspace:${await currentProductVersion(product, PREFIX)}`; } return versions; } -function typescriptOptionalRuntimePackageProducts() { - const selected = allArtifactTargets({ publishedOnly: true }, PREFIX) - .filter((target) => { - if (target.product === "oliphaunt-broker" && target.kind === "broker-helper") { - return target.surfaces.includes("typescript-broker"); - } - if (target.product === "liboliphaunt-native" && ["native-runtime", "native-tools"].includes(target.kind)) { - return target.surfaces.includes("typescript-native-direct"); - } - if (target.product === "oliphaunt-node-direct" && target.kind === "node-direct-addon") { - return target.surfaces.includes("npm-optional"); - } - return false; - }); - if (selected.length === 0) { - fail("no TypeScript optional runtime package targets found"); - } - const pairs = []; - const seen = new Set(); - for (const target of selected) { - if (typeof target.npmPackage !== "string" || !target.npmPackage) { - fail(`${target.id} must declare npmPackage for TypeScript optional dependencies`); - } - if (seen.has(target.npmPackage)) { - fail(`duplicate TypeScript optional package target ${target.npmPackage}`); - } - seen.add(target.npmPackage); - pairs.push([target.npmPackage, target.product]); - } - return pairs.sort(([left], [right]) => compareText(left, right)); -} - function typescriptOptionalRuntimePackages() { - return typescriptOptionalRuntimePackageProducts().map(([packageName]) => packageName); + return typescriptOptionalRuntimePackageProducts(PREFIX).map(({ packageName }) => packageName); } async function syncTypescriptOptionalRuntimeDependencies(changes, { write }) { From 7011c161dcb64cfcc68cf58256660f999ceed214 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 12:23:08 +0000 Subject: [PATCH 181/308] refactor: share sdk package selection --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 32 ++++++++++++++++ tools/release/check_release_metadata.py | 10 +++++ tools/release/product_metadata.py | 37 +++++++++++++------ tools/release/release-artifact-targets.mjs | 16 ++++++++ tools/release/release_graph_query.mjs | 32 ++++++++++++++++ 5 files changed, 116 insertions(+), 11 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 87628a1b..b0b27c4a 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,38 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Moved SDK package product and CI artifact-name selection out of + the Python compatibility layer and into the Bun release graph. `release-artifact-targets.mjs` + now exposes `sdkPackageProducts`, `release_graph_query.mjs sdk-package-products + [--product PRODUCT]` returns the six SDK package rows, and `product_metadata.py` + adapts those rows for legacy Python callers instead of scanning + `config.kind == "sdk"` or special-casing the WASIX Rust artifact name locally. + `check_release_metadata.py` now rejects reintroducing Python SDK product + selection or Python-side SDK artifact-name special cases. A subagent review was + attempted for the next cleanup slice, but the current session had reached the + agent thread limit, so this pass used local repo evidence instead. Fresh + checks passed: `tools/dev/bun.sh tools/release/release_graph_query.mjs + sdk-package-products`, `tools/dev/bun.sh tools/release/release_graph_query.mjs + sdk-package-products --product oliphaunt-wasix-rust`, Python smoke for + `sdk_package_products` and `ci_sdk_package_artifact_names`, selector-removal + `rg` scan, `python3 -m py_compile` for touched Python helpers, `python3 + tools/release/check_release_metadata.py`, `python3 + tools/release/check_artifact_targets.py`, `python3 + tools/release/check_consumer_shape.py`, `tools/release/release.py check`, + `bash tools/policy/check-tooling-stack.sh`, `bash + tools/policy/check-policy-tools.sh`, `tools/release/release.py ci-products + --family sdk-package`, `tools/release/release.py ci-artifacts --product + oliphaunt-wasix-rust --family sdk-package`, + `tools/release/local_registry_publish.py publish --surface cargo --strict`, + and `tools/release/local_registry_publish.py publish --surface npm --strict`. + The Python entrypoint inventory still reported 9 Python entrypoints, with + `product_metadata.py` at 759 lines and 26,646 bytes. A fresh Cargo + local-registry sweep covered 836 `.crate` files with no crate above the 10 + MiB crates.io limit; the largest generated crates were split WASIX PostGIS AOT + part crates at 10,212,312 bytes, and the hard over-limit query returned no + crates. The strict npm publish included `@oliphaunt/liboliphaunt-linux-x64-gnu`, + `@oliphaunt/tools-linux-x64-gnu`, `@oliphaunt/icu`, `@oliphaunt/ts`, broker, + node-direct, and native extension packages from the local Verdaccio registry. - 2026-06-27: Centralized TypeScript optional runtime package selection in `release-artifact-targets.mjs` so release sync, Python metadata adapters, and validation share one artifact-target-backed source for broker, native runtime, diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index e2025fd1..c3064787 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -293,6 +293,16 @@ def validate_graph_files(graph: dict) -> None: or "typescriptOptionalRuntimePackageProducts(TOOL)" not in release_graph_query ): fail("TypeScript optional runtime package selection must come from the shared Bun artifact target helper") + if ( + '"sdk-package-products"' not in product_metadata_source + or "config.get(\"kind\") == \"sdk\"" in product_metadata_source + or "config.get(\"kind\") != \"sdk\"" in product_metadata_source + or "oliphaunt-wasix-rust" in product_metadata_source + or "export function sdkPackageProducts(" not in release_artifact_targets + or "sdk-package-products [--product PRODUCT]" not in release_graph_query + or "sdkPackageProducts(TOOL)" not in release_graph_query + ): + fail("SDK package product and CI artifact-name selection must come from the shared Bun release graph query") def validate_exact_extension_registry_shape(graph: dict) -> None: diff --git a/tools/release/product_metadata.py b/tools/release/product_metadata.py index 0aa3b9df..c5bae83f 100644 --- a/tools/release/product_metadata.py +++ b/tools/release/product_metadata.py @@ -439,21 +439,36 @@ def ci_wasix_runtime_artifact_names() -> list[str]: return sorted(names) +@lru_cache(maxsize=1) +def _sdk_package_product_rows() -> tuple[dict[str, Any], ...]: + return _release_graph_query_rows("sdk-package-products") + + +def _sdk_package_product_row(product: str) -> dict[str, Any]: + matches = [row for row in _sdk_package_product_rows() if row.get("product") == product] + if len(matches) != 1: + fail(f"release graph sdk-package-products query must return one row for SDK product {product}, got {len(matches)}") + return dict(matches[0]) + + +def _sdk_row_string(row: dict[str, Any], key: str, product: str) -> str: + value = row.get(key) + if not isinstance(value, str) or not value: + fail(f"release graph sdk-package-products {product}.{key} must be a non-empty string") + return value + + def ci_sdk_package_artifact_name(product: str) -> str: - config = product_config(product) - if config.get("kind") != "sdk": - 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" + return _sdk_row_string(_sdk_package_product_row(product), "artifactName", product) def sdk_package_products() -> tuple[str, ...]: - return tuple( - product - for product, config in graph_products().items() - if config.get("kind") == "sdk" - ) + products = tuple(_sdk_row_string(row, "product", "") for row in _sdk_package_product_rows()) + if len(products) != len(set(products)): + fail("release graph sdk-package-products query returned duplicate SDK products") + if not products: + fail("release graph returned no SDK package products") + return products def ci_sdk_package_artifact_names(product: str | None = None) -> list[str]: diff --git a/tools/release/release-artifact-targets.mjs b/tools/release/release-artifact-targets.mjs index f027c617..485fcd00 100644 --- a/tools/release/release-artifact-targets.mjs +++ b/tools/release/release-artifact-targets.mjs @@ -764,6 +764,22 @@ export function exactExtensionProducts(prefix = "release-artifact-targets.mjs") .sort(compareText); } +export function sdkPackageProducts(prefix = "release-artifact-targets.mjs") { + const rows = Object.entries(graph(prefix).products) + .filter(([, config]) => config.kind === "sdk") + .map(([product]) => ({ + product, + artifactName: product === "oliphaunt-wasix-rust" + ? `${product}-package-artifacts` + : `${product}-sdk-package-artifacts`, + })) + .sort((left, right) => compareText(left.product, right.product)); + if (rows.length === 0) { + fail(prefix, "release graph contains no SDK package products"); + } + return rows; +} + export function extensionSqlName(product, prefix = "release-artifact-targets.mjs") { const value = productConfig(product, prefix).extension_sql_name; if (typeof value !== "string" || !value) { diff --git a/tools/release/release_graph_query.mjs b/tools/release/release_graph_query.mjs index 8040fd9b..6f4e2a66 100644 --- a/tools/release/release_graph_query.mjs +++ b/tools/release/release_graph_query.mjs @@ -7,6 +7,7 @@ import { extensionSourceIdentity, exactExtensionProducts, rawArtifactTargetRows, + sdkPackageProducts, typescriptOptionalRuntimePackageProducts, } from "./release-artifact-targets.mjs"; import { @@ -307,6 +308,34 @@ function runTypescriptOptionalRuntimePackageVersions(argv) { ); } +function runSdkPackageProducts(argv) { + let product; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === "--product") { + if (index + 1 >= argv.length) { + fail("--product requires a value"); + } + product = argv[index + 1]; + index += 1; + } else if (value.startsWith("--product=")) { + product = value.slice("--product=".length); + } else { + fail(`unknown argument ${value}`); + } + } + const rows = sdkPackageProducts(TOOL); + if (product === undefined) { + printJson(rows); + return; + } + const matches = rows.filter((row) => row.product === product); + if (matches.length !== 1) { + fail(`${product} is not an SDK release product`); + } + printJson(matches); +} + function runExtensionMetadata(argv) { let product; for (let index = 0; index < argv.length; index += 1) { @@ -348,6 +377,7 @@ Commands: extension-metadata [--product PRODUCT] product-versions [--product PRODUCT] typescript-optional-runtime-package-versions + sdk-package-products [--product PRODUCT] compatibility-version-entries [--require-source-product] wasix-cargo-artifact-contract `; @@ -377,6 +407,8 @@ function main(argv) { runProductVersions(rest); } else if (command === "typescript-optional-runtime-package-versions") { runTypescriptOptionalRuntimePackageVersions(rest); + } else if (command === "sdk-package-products") { + runSdkPackageProducts(rest); } else if (command === "compatibility-version-entries") { runCompatibilityVersionEntries(rest); } else if (command === "wasix-cargo-artifact-contract") { From 134cfaecf522f9daa14fc95e7792147fe9f0907a Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 12:53:50 +0000 Subject: [PATCH 182/308] refactor: clarify split tool artifact packaging --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 29 ++++++++++ .../wasix/crates/assets/README.md | 4 ++ .../liboliphaunt/wasix/crates/tools/README.md | 5 ++ ...ck-wasix-release-dependency-invariants.mjs | 20 ++++--- tools/release/check_consumer_shape.py | 4 +- tools/release/check_release_metadata.py | 15 ++++- tools/release/product_metadata.py | 51 +++++++++-------- tools/release/release-artifact-targets.mjs | 37 ++++++++++++ tools/release/release_graph_query.mjs | 57 +++++++++++++++++++ 9 files changed, 186 insertions(+), 36 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index b0b27c4a..ee69bb2c 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,35 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Clarified the current root/tools split for registry-published + artifacts and revalidated it from generated packages. The WASIX + `liboliphaunt-wasix-portable`, `oliphaunt-wasix-tools`, root AOT, and + tools-AOT manifests in the checkout are source templates, so they intentionally + keep `publish = false` until release packaging injects payloads and strips the + guard in the generated registry crates. The release dependency invariant and + consumer-shape checks now name those as `SOURCE_TEMPLATE_*` manifests instead + of implying the generated artifacts are private-only. Fresh checks passed: + `python3 -m py_compile` for touched Python helpers, `python3 + tools/release/check_release_metadata.py`, `python3 + tools/release/check_consumer_shape.py`, `tools/dev/bun.sh + tools/policy/check-wasix-release-dependency-invariants.mjs`, `python3 + tools/release/check_artifact_targets.py`, `tools/release/release.py check`, + `bash tools/policy/check-tooling-stack.sh`, `bash + tools/policy/check-policy-tools.sh`, `tools/dev/bun.sh + tools/policy/check-python-entrypoints.mjs --json`, + `tools/release/local_registry_publish.py publish --surface cargo --strict`, + and `tools/release/local_registry_publish.py publish --surface npm --strict`. + A fresh Cargo local-registry sweep covered 836 `.crate` files with + `over_limit=0`; the largest crates were split WASIX PostGIS AOT parts at + 10,212,312 bytes, below the 10,485,760-byte crates.io limit. Generated native + Cargo and npm package inspection found root runtime payloads carrying only + `initdb`, `pg_ctl`, and `postgres`, while `oliphaunt-tools`/ + `@oliphaunt/tools-linux-x64-gnu` carried only `pg_dump` and `psql`. WASIX root + inspection found `bin/initdb.wasix.wasm`, `manifest.json`, + `oliphaunt.wasix.tar.zst`, and prepopulated template files in the portable + root payload; the nested runtime archive contained only `oliphaunt/bin/initdb` + and `oliphaunt/bin/postgres`; and `oliphaunt-wasix-tools` contained only + `bin/pg_dump.wasix.wasm` and `bin/psql.wasix.wasm`, with no WASIX `pg_ctl`. - 2026-06-27: Moved SDK package product and CI artifact-name selection out of the Python compatibility layer and into the Bun release graph. `release-artifact-targets.mjs` now exposes `sdkPackageProducts`, `release_graph_query.mjs sdk-package-products diff --git a/src/runtimes/liboliphaunt/wasix/crates/assets/README.md b/src/runtimes/liboliphaunt/wasix/crates/assets/README.md index a54678ef..5bf0a26c 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/assets/README.md +++ b/src/runtimes/liboliphaunt/wasix/crates/assets/README.md @@ -7,3 +7,7 @@ Applications depend on `oliphaunt-wasix`, not on this crate directly. `liboliphaunt-wasix` runtime version. Release packaging publishes this crate directly from staged WASIX release assets so Cargo resolves the packaged WASIX runtime without a runtime download step. + +The published root runtime crate carries `postgres` and `initdb` only. Standalone +client tools are split into `oliphaunt-wasix-tools`, which carries `pg_dump` and +`psql`; WASIX has no `pg_ctl` payload. diff --git a/src/runtimes/liboliphaunt/wasix/crates/tools/README.md b/src/runtimes/liboliphaunt/wasix/crates/tools/README.md index 63531676..24151779 100644 --- a/src/runtimes/liboliphaunt/wasix/crates/tools/README.md +++ b/src/runtimes/liboliphaunt/wasix/crates/tools/README.md @@ -3,3 +3,8 @@ Cargo artifact crate for Oliphaunt WASIX PostgreSQL command-line tools. The `oliphaunt-wasix` crate selects it through the `tools` feature when an application needs the WASIX `pg_dump` or `psql` modules. + +This checkout copy is a source template. Release packaging injects +`pg_dump.wasix.wasm` and `psql.wasix.wasm`, removes the local `publish = false` +guard, and publishes the generated `oliphaunt-wasix-tools` crate to the Cargo +registry. WASIX intentionally has no `pg_ctl` tools crate payload. diff --git a/tools/policy/check-wasix-release-dependency-invariants.mjs b/tools/policy/check-wasix-release-dependency-invariants.mjs index 230f6b93..6393c8c2 100644 --- a/tools/policy/check-wasix-release-dependency-invariants.mjs +++ b/tools/policy/check-wasix-release-dependency-invariants.mjs @@ -5,12 +5,12 @@ 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 = +const SOURCE_TEMPLATE_ASSETS_MANIFEST = 'src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml'; -const INTERNAL_TOOLS_MANIFEST = +const SOURCE_TEMPLATE_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 = +const SOURCE_TEMPLATE_AOT_MANIFESTS_DIR = 'src/runtimes/liboliphaunt/wasix/crates/aot'; +const SOURCE_TEMPLATE_TOOLS_AOT_MANIFESTS_DIR = 'src/runtimes/liboliphaunt/wasix/crates/tools-aot'; function fail(errors) { @@ -83,17 +83,17 @@ for (const [tableName, deps] of dependencyTables(productManifest)) { } } -const internalManifestPaths = [INTERNAL_ASSETS_MANIFEST, INTERNAL_TOOLS_MANIFEST]; -for (const manifestsDir of [INTERNAL_AOT_MANIFESTS_DIR, INTERNAL_TOOLS_AOT_MANIFESTS_DIR]) { +const sourceTemplateManifestPaths = [SOURCE_TEMPLATE_ASSETS_MANIFEST, SOURCE_TEMPLATE_TOOLS_MANIFEST]; +for (const manifestsDir of [SOURCE_TEMPLATE_AOT_MANIFESTS_DIR, SOURCE_TEMPLATE_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')); + sourceTemplateManifestPaths.push(join(manifestsDir, entry, 'Cargo.toml')); } } -for (const manifestPath of internalManifestPaths) { +for (const manifestPath of sourceTemplateManifestPaths) { const manifest = await readToml(manifestPath); const packageConfig = manifest.package ?? {}; const name = packageConfig.name; @@ -108,7 +108,9 @@ for (const manifestPath of internalManifestPaths) { ); } if (packageConfig.publish !== false) { - errors.push(`${manifestPath}: source artifact crate template ${name} must declare publish = false`); + errors.push( + `${manifestPath}: source artifact crate template ${name} must declare publish = false until release packaging injects payloads and strips the guard`, + ); } if (!productDeps.has(name)) { errors.push(`oliphaunt-wasix must depend on WASIX artifact crate ${name}`); diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 4d1c6160..f1161791 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -1969,8 +1969,8 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: 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 + "SOURCE_TEMPLATE_TOOLS_MANIFEST" in wasix_dependency_invariant_source + and "SOURCE_TEMPLATE_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.", diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index c3064787..a352cb7f 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -303,6 +303,17 @@ def validate_graph_files(graph: dict) -> None: or "sdkPackageProducts(TOOL)" not in release_graph_query ): fail("SDK package product and CI artifact-name selection must come from the shared Bun release graph query") + if ( + '"ci-artifact-names"' not in product_metadata_source + or "f\"{product}-release-assets-{target.target}\"" in product_metadata_source + or "f\"{product}-npm-package-{target.target}\"" in product_metadata_source + or "export function ciReleaseAssetArtifactRows(" not in release_artifact_targets + or "export function ciNpmPackageArtifactRows(" not in release_artifact_targets + or "ci-artifact-names --family release-assets|npm-package --product PRODUCT --kind KIND" not in release_graph_query + or "ciReleaseAssetArtifactRows(product, kind, TOOL)" not in release_graph_query + or "ciNpmPackageArtifactRows(product, kind, TOOL)" not in release_graph_query + ): + fail("CI release asset and npm package artifact names must come from the shared Bun artifact target helper") def validate_exact_extension_registry_shape(graph: dict) -> None: @@ -1570,8 +1581,8 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None 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 + "SOURCE_TEMPLATE_TOOLS_MANIFEST" not in wasix_dependency_invariant_source + or "SOURCE_TEMPLATE_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") diff --git a/tools/release/product_metadata.py b/tools/release/product_metadata.py index c5bae83f..eb909b21 100644 --- a/tools/release/product_metadata.py +++ b/tools/release/product_metadata.py @@ -373,34 +373,39 @@ def expected_assets( 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, - ) - ] +@lru_cache(maxsize=None) +def _ci_artifact_name_rows(family: str, product: str, kind: str) -> tuple[dict[str, Any], ...]: + return _release_graph_query_rows( + "ci-artifact-names", + ("--family", family, "--product", product, "--kind", kind), + ) + + +def _ci_artifact_names(family: str, product: str, kind: str) -> list[str]: + names: list[str] = [] + for row in _ci_artifact_name_rows(family, product, kind): + artifact_name = row.get("artifactName") + artifact_target = row.get("artifactTarget") + if row.get("family") != family or row.get("product") != product or row.get("kind") != kind: + fail(f"release graph ci-artifact-names returned an unexpected row for {family}/{product}/{kind}") + if not isinstance(artifact_name, str) or not artifact_name: + fail(f"release graph ci-artifact-names {family}/{product}/{kind} artifactName must be a non-empty string") + if not isinstance(artifact_target, str) or not artifact_target: + fail(f"release graph ci-artifact-names {family}/{product}/{kind} artifactTarget must be a non-empty string") + names.append(artifact_name) + if len(names) != len(set(names)): + fail(f"release graph ci-artifact-names returned duplicate artifacts for {family}/{product}/{kind}") if not names: - fail(f"{product} has no published {kind} CI release asset targets") + fail(f"release graph returned no CI artifact names for {family}/{product}/{kind}") return sorted(names) +def ci_release_asset_artifact_names(product: str, kind: str) -> list[str]: + return _ci_artifact_names("release-assets", product, kind) + + 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: - fail(f"{product} has no published {kind} CI npm package targets") - return sorted(names) + return _ci_artifact_names("npm-package", product, kind) def ci_wasix_aot_runtime_artifact_names() -> list[str]: diff --git a/tools/release/release-artifact-targets.mjs b/tools/release/release-artifact-targets.mjs index 485fcd00..61cdcedb 100644 --- a/tools/release/release-artifact-targets.mjs +++ b/tools/release/release-artifact-targets.mjs @@ -647,6 +647,43 @@ export function artifactTargets(product, kind, prefix) { return allArtifactTargets({ product, kind, publishedOnly: true }, prefix); } +function ciArtifactRows({ product, kind, surface, family, name }, prefix) { + const targets = allArtifactTargets({ product, kind, surface, publishedOnly: true }, prefix); + if (targets.length === 0) { + fail(prefix, `${product} has no published ${kind} CI ${family} artifact targets`); + } + return targets + .map((target) => ({ + family, + product, + target: target.target, + kind: target.kind, + artifactTarget: target.id, + artifactName: name(target), + })) + .sort((left, right) => compareText(left.artifactName, right.artifactName)); +} + +export function ciReleaseAssetArtifactRows(product, kind, prefix = "release-artifact-targets.mjs") { + return ciArtifactRows({ + product, + kind, + surface: "github-release", + family: "release-assets", + name: (target) => `${product}-release-assets-${target.target}`, + }, prefix); +} + +export function ciNpmPackageArtifactRows(product, kind, prefix = "release-artifact-targets.mjs") { + return ciArtifactRows({ + product, + kind, + surface: "npm-optional", + family: "npm-package", + name: (target) => `${product}-npm-package-${target.target}`, + }, prefix); +} + export function releaseMetadata(product, prefix) { const release = graph(prefix).moon_projects?.[product]?.project?.metadata?.release; if (!release) { diff --git a/tools/release/release_graph_query.mjs b/tools/release/release_graph_query.mjs index 6f4e2a66..a5e466b6 100644 --- a/tools/release/release_graph_query.mjs +++ b/tools/release/release_graph_query.mjs @@ -1,6 +1,8 @@ #!/usr/bin/env bun import { allArtifactTargets, + ciNpmPackageArtifactRows, + ciReleaseAssetArtifactRows, currentProductVersionSync, extensionArtifactTargets, extensionMetadata, @@ -336,6 +338,58 @@ function runSdkPackageProducts(argv) { printJson(matches); } +function runCiArtifactNames(argv) { + let family; + let product; + let kind; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === "--family") { + if (index + 1 >= argv.length) { + fail("--family requires a value"); + } + family = argv[index + 1]; + index += 1; + } else if (value.startsWith("--family=")) { + family = value.slice("--family=".length); + } else if (value === "--product") { + if (index + 1 >= argv.length) { + fail("--product requires a value"); + } + product = argv[index + 1]; + index += 1; + } else if (value.startsWith("--product=")) { + product = value.slice("--product=".length); + } else if (value === "--kind") { + if (index + 1 >= argv.length) { + fail("--kind requires a value"); + } + kind = argv[index + 1]; + index += 1; + } else if (value.startsWith("--kind=")) { + kind = value.slice("--kind=".length); + } else { + fail(`unknown argument ${value}`); + } + } + if (family === undefined) { + fail("--family is required"); + } + if (product === undefined) { + fail("--product is required"); + } + if (kind === undefined) { + fail("--kind is required"); + } + if (family === "release-assets") { + printJson(ciReleaseAssetArtifactRows(product, kind, TOOL)); + } else if (family === "npm-package") { + printJson(ciNpmPackageArtifactRows(product, kind, TOOL)); + } else { + fail("--family must be release-assets or npm-package"); + } +} + function runExtensionMetadata(argv) { let product; for (let index = 0; index < argv.length; index += 1) { @@ -378,6 +432,7 @@ Commands: product-versions [--product PRODUCT] typescript-optional-runtime-package-versions sdk-package-products [--product PRODUCT] + ci-artifact-names --family release-assets|npm-package --product PRODUCT --kind KIND compatibility-version-entries [--require-source-product] wasix-cargo-artifact-contract `; @@ -409,6 +464,8 @@ function main(argv) { runTypescriptOptionalRuntimePackageVersions(rest); } else if (command === "sdk-package-products") { runSdkPackageProducts(rest); + } else if (command === "ci-artifact-names") { + runCiArtifactNames(rest); } else if (command === "compatibility-version-entries") { runCompatibilityVersionEntries(rest); } else if (command === "wasix-cargo-artifact-contract") { From 2c4911d460756c1cf69da56049af6b36cd4887e6 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 13:18:16 +0000 Subject: [PATCH 183/308] refactor: centralize local publish artifact preset --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 27 +++++ tools/release/check_release_metadata.py | 28 +++-- tools/release/local_registry_publish.py | 18 +-- tools/release/product_metadata.py | 100 +++++++++------- tools/release/release-artifact-targets.mjs | 110 ++++++++++++++++++ tools/release/release_graph_query.mjs | 16 +++ 6 files changed, 233 insertions(+), 66 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index ee69bb2c..a8c87bcf 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,33 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Moved the local-registry CI artifact download preset into the Bun + release graph. `release-artifact-targets.mjs` now exposes + `localPublishArtifactRows`, `release_graph_query.mjs local-publish-artifacts + [--aggregate-only]` returns the shared artifact rows, and + `product_metadata.py`/`local_registry_publish.py` only validate and adapt + those rows for legacy Python callers. The preset now reports 6 aggregate + artifacts and 35 total local-publish artifacts from one graph-backed source. + A dry-run against the configured GitHub Actions run passed with all 35 + artifacts present, including split native runtime, WASIX runtime/AOT, + extension package, node-direct, and SDK package artifacts. Fresh checks + passed: `tools/dev/bun.sh tools/release/release_graph_query.mjs + local-publish-artifacts`, `tools/dev/bun.sh + tools/release/release_graph_query.mjs local-publish-artifacts + --aggregate-only`, Python smoke checks for `ci_local_publish_artifact_names` + and `local_publish_artifacts`, `python3 -m py_compile` for touched Python + helpers, `python3 tools/release/check_release_metadata.py`, `python3 + tools/release/check_consumer_shape.py`, `python3 + tools/release/check_artifact_targets.py`, `tools/release/release.py check`, + `tools/release/local_registry_publish.py download --preset local-publish + --dry-run`, `tools/release/local_registry_publish.py publish --surface cargo + --strict`, and `tools/release/local_registry_publish.py publish --surface npm + --strict`. The Python entrypoint inventory still reports 9 Python entrypoints; + `local_registry_publish.py` dropped to 3,041 lines and 109,882 bytes while + `product_metadata.py` remains a compatibility adapter at 780 lines and 28,569 + bytes. A fresh Cargo local-registry sweep covered 836 `.crate` files with + `over_limit=0`; the largest crates remained split WASIX PostGIS AOT parts at + 10,212,312 bytes, below the 10,485,760-byte crates.io limit. - 2026-06-27: Clarified the current root/tools split for registry-published artifacts and revalidated it from generated packages. The WASIX `liboliphaunt-wasix-portable`, `oliphaunt-wasix-tools`, root AOT, and diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index a352cb7f..6caa7bbf 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -314,6 +314,17 @@ def validate_graph_files(graph: dict) -> None: or "ciNpmPackageArtifactRows(product, kind, TOOL)" not in release_graph_query ): fail("CI release asset and npm package artifact names must come from the shared Bun artifact target helper") + if ( + '"local-publish-artifacts"' not in product_metadata_source + or "export function localPublishArtifactRows(" not in release_artifact_targets + or "local-publish-artifacts [--aggregate-only]" not in release_graph_query + or "localPublishArtifactRows({ aggregateOnly }, TOOL)" not in release_graph_query + or "liboliphaunt-wasix-runtime-aot-{target.target}" in product_metadata_source + or "liboliphaunt-wasix-runtime-{target.target}" in product_metadata_source + or "liboliphaunt-wasix-extension-artifacts-{target_id}" in product_metadata_source + or "oliphaunt-extension-package-artifacts" in product_metadata_source + ): + fail("local-registry publish artifact preset must come from the shared Bun release graph query") def validate_exact_extension_registry_shape(graph: dict) -> None: @@ -480,15 +491,16 @@ def validate_local_registry_publisher() -> None: 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 + or "ci_local_publish_artifact_names(aggregate_only=True)" not in publisher + or "ci_local_publish_artifact_names()" not in publisher + or "ci_aggregate_release_asset_artifact_name(\"liboliphaunt-native\")" in publisher + or "ci_wasix_runtime_artifact_names()" in publisher + or "ci_wasix_aot_runtime_artifact_names()" in publisher + or "ci_wasix_extension_artifact_names()" in publisher + or "ci_extension_package_artifact_names()" in publisher + or "ci_release_asset_artifact_names(\"liboliphaunt-native\", \"native-runtime\")" 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") + fail("local registry publish preset must come from the shared Bun local-publish-artifacts query") with tempfile.TemporaryDirectory(prefix="oliphaunt-extension-manifest-dedupe-") as tmp: root = Path(tmp) first = root / "first" / "oliphaunt-extension-demo" diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index b6dd9986..d48db498 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -60,25 +60,11 @@ ) def local_publish_aggregate_artifacts() -> list[str]: - return [ - product_metadata.ci_aggregate_release_asset_artifact_name("liboliphaunt-native"), - product_metadata.ci_aggregate_release_asset_artifact_name("liboliphaunt-wasix"), - *product_metadata.ci_wasix_runtime_artifact_names(), - *product_metadata.ci_wasix_extension_artifact_names(), - *product_metadata.ci_extension_package_artifact_names(), - ] + return product_metadata.ci_local_publish_artifact_names(aggregate_only=True) def local_publish_artifacts() -> list[str]: - artifacts = [ - *local_publish_aggregate_artifacts(), - *product_metadata.ci_release_asset_artifact_names("liboliphaunt-native", "native-runtime"), - *product_metadata.ci_wasix_aot_runtime_artifact_names(), - *product_metadata.ci_release_asset_artifact_names("oliphaunt-broker", "broker-helper"), - *product_metadata.ci_release_asset_artifact_names("oliphaunt-node-direct", "node-direct-addon"), - *product_metadata.ci_npm_package_artifact_names("oliphaunt-node-direct", "node-direct-addon"), - *product_metadata.ci_sdk_package_artifact_names(), - ] + artifacts = product_metadata.ci_local_publish_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)) diff --git a/tools/release/product_metadata.py b/tools/release/product_metadata.py index eb909b21..f5941773 100644 --- a/tools/release/product_metadata.py +++ b/tools/release/product_metadata.py @@ -408,40 +408,70 @@ def ci_npm_package_artifact_names(product: str, kind: str) -> list[str]: return _ci_artifact_names("npm-package", product, kind) -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, - ) - ] +@lru_cache(maxsize=None) +def _local_publish_artifact_rows(aggregate_only: bool = False) -> tuple[dict[str, Any], ...]: + args = ("--aggregate-only",) if aggregate_only else () + return _release_graph_query_rows("local-publish-artifacts", args) + + +def _local_publish_row_names(rows: Iterable[dict[str, Any]], *, context: str) -> list[str]: + names: list[str] = [] + for row in rows: + artifact_name = row.get("artifactName") + aggregate = row.get("aggregate") + family = row.get("family") + if not isinstance(artifact_name, str) or not artifact_name: + fail(f"release graph local-publish-artifacts {context} artifactName must be a non-empty string") + if not isinstance(aggregate, bool): + fail(f"release graph local-publish-artifacts {artifact_name}.aggregate must be true or false") + if not isinstance(family, str) or not family: + fail(f"release graph local-publish-artifacts {artifact_name}.family must be a non-empty string") + names.append(artifact_name) + if len(names) != len(set(names)): + fail(f"release graph local-publish-artifacts returned duplicate names for {context}") if not names: - fail("liboliphaunt-wasix has no published WASIX AOT runtime targets") + fail(f"release graph returned no local-publish artifacts for {context}") return sorted(names) +def ci_local_publish_artifact_names(*, aggregate_only: bool = False) -> list[str]: + return _local_publish_row_names( + _local_publish_artifact_rows(aggregate_only), + context="aggregate-only" if aggregate_only else "full preset", + ) + + +def _local_publish_artifact_names_by_family(family: str, *, aggregate_only: bool = False) -> list[str]: + return _local_publish_row_names( + ( + row + for row in _local_publish_artifact_rows(aggregate_only) + if row.get("family") == family + ), + context=family, + ) + + +def ci_wasix_aot_runtime_artifact_names() -> list[str]: + return _local_publish_artifact_names_by_family("wasix-aot-runtime") + + def ci_aggregate_release_asset_artifact_name(product: str) -> str: - config = product_config(product) - release_artifacts = config.get("release_artifacts") - if not isinstance(release_artifacts, list) or not release_artifacts: - fail(f"{product} does not publish aggregate release assets") - return f"{product}-release-assets" + names = _local_publish_row_names( + ( + row + for row in _local_publish_artifact_rows(aggregate_only=True) + if row.get("family") == "aggregate-release-assets" and row.get("product") == product + ), + context=f"aggregate release assets for {product}", + ) + if len(names) != 1: + fail(f"release graph returned {len(names)} aggregate release asset rows for {product}") + return names[0] 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: - fail("liboliphaunt-wasix has no published WASIX runtime targets") - return sorted(names) + return _local_publish_artifact_names_by_family("wasix-runtime", aggregate_only=True) @lru_cache(maxsize=1) @@ -592,25 +622,11 @@ def published_extension_target_ids(*, family: str) -> list[str]: def ci_wasix_extension_artifact_names() -> list[str]: - names = [ - f"liboliphaunt-wasix-extension-artifacts-{target_id}" - for target_id in published_extension_target_ids(family="wasix") - ] - if not names: - fail("exact-extension metadata has no published WASIX artifact targets") - return names + return _local_publish_artifact_names_by_family("wasix-extension-artifacts", aggregate_only=True) def ci_extension_package_artifact_names() -> list[str]: - names = ["oliphaunt-extension-package-artifacts"] - mobile_targets = [ - target - for target in extension_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 + return _local_publish_artifact_names_by_family("extension-package-artifacts", aggregate_only=True) def string_list(config: dict, key: str, product: str) -> list[str]: diff --git a/tools/release/release-artifact-targets.mjs b/tools/release/release-artifact-targets.mjs index 61cdcedb..7f234885 100644 --- a/tools/release/release-artifact-targets.mjs +++ b/tools/release/release-artifact-targets.mjs @@ -684,6 +684,116 @@ export function ciNpmPackageArtifactRows(product, kind, prefix = "release-artifa }, prefix); } +function aggregateReleaseAssetArtifactRow(product, prefix) { + const config = productConfig(product, prefix); + const releaseArtifacts = config.release_artifacts; + if (!Array.isArray(releaseArtifacts) || releaseArtifacts.length === 0) { + fail(prefix, `${product} does not publish aggregate release assets`); + } + return { + aggregate: true, + family: "aggregate-release-assets", + product, + artifactName: `${product}-release-assets`, + }; +} + +function localPublishAggregateArtifactRows(prefix) { + const rows = [ + aggregateReleaseAssetArtifactRow("liboliphaunt-native", prefix), + aggregateReleaseAssetArtifactRow("liboliphaunt-wasix", prefix), + ]; + rows.push( + ...allArtifactTargets({ + product: "liboliphaunt-wasix", + kind: "wasix-runtime", + publishedOnly: true, + }, prefix).map((target) => ({ + aggregate: true, + family: "wasix-runtime", + product: target.product, + kind: target.kind, + target: target.target, + artifactTarget: target.id, + artifactName: `liboliphaunt-wasix-runtime-${target.target}`, + })), + ); + rows.push( + ...[...new Set( + extensionArtifactTargets({ family: "wasix", publishedOnly: true }, prefix).map((target) => target.target), + )].sort(compareText).map((target) => ({ + aggregate: true, + family: "wasix-extension-artifacts", + target, + artifactName: `liboliphaunt-wasix-extension-artifacts-${target}`, + })), + ); + rows.push({ + aggregate: true, + family: "extension-package-artifacts", + artifactName: "oliphaunt-extension-package-artifacts", + }); + if (extensionArtifactTargets({ family: "native", publishedOnly: true }, prefix).some( + (target) => target.kind === "native-static-registry", + )) { + rows.push({ + aggregate: true, + family: "extension-package-artifacts", + artifactName: "oliphaunt-mobile-extension-package-artifacts", + }); + } + return rows; +} + +export function localPublishArtifactRows({ aggregateOnly = false } = {}, prefix = "release-artifact-targets.mjs") { + const rows = localPublishAggregateArtifactRows(prefix); + if (!aggregateOnly) { + rows.push( + ...ciReleaseAssetArtifactRows("liboliphaunt-native", "native-runtime", prefix).map((row) => ({ + ...row, + aggregate: false, + })), + ...allArtifactTargets({ + product: "liboliphaunt-wasix", + kind: "wasix-aot-runtime", + publishedOnly: true, + }, prefix).map((target) => ({ + aggregate: false, + family: "wasix-aot-runtime", + product: target.product, + kind: target.kind, + target: target.target, + artifactTarget: target.id, + artifactName: `liboliphaunt-wasix-runtime-aot-${target.target}`, + })), + ...ciReleaseAssetArtifactRows("oliphaunt-broker", "broker-helper", prefix).map((row) => ({ + ...row, + aggregate: false, + })), + ...ciReleaseAssetArtifactRows("oliphaunt-node-direct", "node-direct-addon", prefix).map((row) => ({ + ...row, + aggregate: false, + })), + ...ciNpmPackageArtifactRows("oliphaunt-node-direct", "node-direct-addon", prefix).map((row) => ({ + ...row, + aggregate: false, + })), + ...sdkPackageProducts(prefix).map((row) => ({ + aggregate: false, + family: "sdk-package", + product: row.product, + artifactName: row.artifactName, + })), + ); + } + const names = rows.map((row) => row.artifactName); + const duplicates = [...new Set(names.filter((name, index) => names.indexOf(name) !== index))].sort(compareText); + if (duplicates.length > 0) { + fail(prefix, `duplicate local publish artifact names: ${duplicates.join(", ")}`); + } + return rows.sort((left, right) => compareText(left.artifactName, right.artifactName)); +} + export function releaseMetadata(product, prefix) { const release = graph(prefix).moon_projects?.[product]?.project?.metadata?.release; if (!release) { diff --git a/tools/release/release_graph_query.mjs b/tools/release/release_graph_query.mjs index a5e466b6..84cb5e36 100644 --- a/tools/release/release_graph_query.mjs +++ b/tools/release/release_graph_query.mjs @@ -8,6 +8,7 @@ import { extensionMetadata, extensionSourceIdentity, exactExtensionProducts, + localPublishArtifactRows, rawArtifactTargetRows, sdkPackageProducts, typescriptOptionalRuntimePackageProducts, @@ -390,6 +391,18 @@ function runCiArtifactNames(argv) { } } +function runLocalPublishArtifacts(argv) { + let aggregateOnly = false; + for (const value of argv) { + if (value === "--aggregate-only") { + aggregateOnly = true; + } else { + fail(`unknown argument ${value}`); + } + } + printJson(localPublishArtifactRows({ aggregateOnly }, TOOL)); +} + function runExtensionMetadata(argv) { let product; for (let index = 0; index < argv.length; index += 1) { @@ -433,6 +446,7 @@ Commands: typescript-optional-runtime-package-versions sdk-package-products [--product PRODUCT] ci-artifact-names --family release-assets|npm-package --product PRODUCT --kind KIND + local-publish-artifacts [--aggregate-only] compatibility-version-entries [--require-source-product] wasix-cargo-artifact-contract `; @@ -466,6 +480,8 @@ function main(argv) { runSdkPackageProducts(rest); } else if (command === "ci-artifact-names") { runCiArtifactNames(rest); + } else if (command === "local-publish-artifacts") { + runLocalPublishArtifacts(rest); } else if (command === "compatibility-version-entries") { runCompatibilityVersionEntries(rest); } else if (command === "wasix-cargo-artifact-contract") { From 4f1232adcc92fdd589646eda747437703e98c225 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 13:28:59 +0000 Subject: [PATCH 184/308] refactor: centralize expected release assets --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 25 +++++++ tools/release/check_release_metadata.py | 9 +++ tools/release/product_metadata.py | 58 +++++++++++++---- tools/release/release-artifact-targets.mjs | 50 +++++++++++++- tools/release/release_graph_query.mjs | 65 +++++++++++++++++++ 5 files changed, 191 insertions(+), 16 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index a8c87bcf..c6462a4c 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,31 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Moved expected GitHub release asset-name selection out of the + Python compatibility layer and into the Bun release graph. `release-artifact-targets.mjs` + now exposes `expectedAssetRows`, `release_graph_query.mjs expected-assets + --product PRODUCT --version VERSION` returns structured expected asset rows, + and `product_metadata.expected_assets` now validates and adapts those rows for + legacy Python callers such as `release.py`, `check_consumer_shape.py`, and + `check-release-policy.py`. `check_release_metadata.py` now rejects + reintroducing the old Python-side `target.asset_name(version)` selector. A + subagent review was attempted for this slice, but the current session is still + at the agent thread limit, so this pass used local repository evidence. + Fresh checks passed: `tools/dev/bun.sh tools/release/release_graph_query.mjs + expected-assets --product liboliphaunt-wasix --version 0.1.0`, + `tools/dev/bun.sh tools/release/release_graph_query.mjs expected-assets + --product oliphaunt-broker --version 0.1.0 --kind broker-helper`, Python + smoke checks for `product_metadata.expected_assets`, `python3 -m py_compile` + for touched Python helpers, `python3 tools/release/check_release_metadata.py`, + `python3 tools/release/check_consumer_shape.py`, `python3 + tools/release/check_artifact_targets.py`, `tools/release/release.py check`, + `bash tools/policy/check-tooling-stack.sh`, `bash + tools/policy/check-policy-tools.sh`, `bash tools/policy/check-docs.sh`, + `tools/release/local_registry_publish.py download --preset local-publish + --dry-run`, and JSON/diff checks for the new query. The Python entrypoint + inventory still reports 9 Python entrypoints; + `product_metadata.py` is now 812 lines and 30,090 bytes, while + `check_release_metadata.py` is 1,758 lines and 89,961 bytes. - 2026-06-27: Moved the local-registry CI artifact download preset into the Bun release graph. `release-artifact-targets.mjs` now exposes `localPublishArtifactRows`, `release_graph_query.mjs local-publish-artifacts diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 6caa7bbf..f85f9e19 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -314,6 +314,15 @@ def validate_graph_files(graph: dict) -> None: or "ciNpmPackageArtifactRows(product, kind, TOOL)" not in release_graph_query ): fail("CI release asset and npm package artifact names must come from the shared Bun artifact target helper") + if ( + '"expected-assets"' not in product_metadata_source + or "export function expectedAssetRows(" not in release_artifact_targets + or "expected-assets --product PRODUCT --version VERSION" not in release_graph_query + or "expectedAssetRows({" not in release_graph_query + or "target.asset_name(version)" in product_metadata_source + or "allowed_kinds = set(kinds)" in product_metadata_source + ): + fail("expected release asset names must come from the shared Bun release graph query") if ( '"local-publish-artifacts"' not in product_metadata_source or "export function localPublishArtifactRows(" not in release_artifact_targets diff --git a/tools/release/product_metadata.py b/tools/release/product_metadata.py index f5941773..aa8bd1aa 100644 --- a/tools/release/product_metadata.py +++ b/tools/release/product_metadata.py @@ -350,6 +350,46 @@ def wasix_extension_aot_package_name(product: str, target: str) -> str: return f"{product}-wasix-aot-{target}" +@lru_cache(maxsize=None) +def _expected_asset_rows( + product: str, + version: str, + surface: str, + published_only: bool, + kinds: tuple[str, ...] | None, +) -> tuple[dict[str, Any], ...]: + args: list[str] = ["--product", product, "--version", version, "--surface", surface] + if not published_only: + args.append("--include-unpublished") + if kinds is not None: + for kind in kinds: + args.extend(["--kind", kind]) + return _release_graph_query_rows("expected-assets", tuple(args)) + + +def _expected_asset_names(rows: Iterable[dict[str, Any]], *, context: str) -> list[str]: + names: list[str] = [] + for row in rows: + asset_name = row.get("assetName") + artifact_target = row.get("artifactTarget") + product = row.get("product") + kind = row.get("kind") + if not isinstance(asset_name, str) or not asset_name: + fail(f"release graph expected-assets {context} assetName must be a non-empty string") + if not isinstance(artifact_target, str) or not artifact_target: + fail(f"release graph expected-assets {asset_name}.artifactTarget must be a non-empty string") + if not isinstance(product, str) or not product: + fail(f"release graph expected-assets {asset_name}.product must be a non-empty string") + if not isinstance(kind, str) or not kind: + fail(f"release graph expected-assets {asset_name}.kind must be a non-empty string") + names.append(asset_name) + if len(names) != len(set(names)): + fail(f"release graph expected-assets returned duplicate names for {context}") + if not names: + fail(f"release graph returned no expected assets for {context}") + return sorted(names) + + def expected_assets( product: str, version: str, @@ -358,19 +398,11 @@ def expected_assets( published_only: bool = True, kinds: Iterable[str] | None = None, ) -> list[str]: - allowed_kinds = set(kinds) if kinds is not None else None - assets = [ - target.asset_name(version) - for target in artifact_targets( - product=product, - surface=surface, - published_only=published_only, - ) - if allowed_kinds is None or target.kind in allowed_kinds - ] - if not assets: - fail(f"{product} has no artifact targets for surface {surface}") - return sorted(assets) + kind_tuple = None if kinds is None else tuple(sorted(set(kinds))) + return _expected_asset_names( + _expected_asset_rows(product, version, surface, published_only, kind_tuple), + context=f"{product}/{surface}", + ) @lru_cache(maxsize=None) diff --git a/tools/release/release-artifact-targets.mjs b/tools/release/release-artifact-targets.mjs index 7f234885..b5784933 100644 --- a/tools/release/release-artifact-targets.mjs +++ b/tools/release/release-artifact-targets.mjs @@ -684,6 +684,51 @@ export function ciNpmPackageArtifactRows(product, kind, prefix = "release-artifa }, prefix); } +export function expectedAssetRows( + { + product, + version, + surface = "github-release", + publishedOnly = true, + kinds = undefined, + } = {}, + prefix = "release-artifact-targets.mjs", +) { + if (typeof product !== "string" || product.length === 0) { + fail(prefix, "expected asset rows require a product"); + } + if (typeof version !== "string" || version.length === 0) { + fail(prefix, "expected asset rows require a version"); + } + const kindSet = kinds === undefined ? undefined : new Set(kinds); + if ( + kindSet !== undefined + && (kindSet.size === 0 || [...kindSet].some((kind) => typeof kind !== "string" || kind.length === 0)) + ) { + fail(prefix, "expected asset row kinds must be a non-empty string list"); + } + const rows = allArtifactTargets({ product, surface, publishedOnly }, prefix) + .filter((target) => kindSet === undefined || kindSet.has(target.kind)) + .map((target) => ({ + product: target.product, + kind: target.kind, + target: target.target, + surface, + artifactTarget: target.id, + assetName: target.asset.replaceAll("{version}", version), + })) + .sort((left, right) => compareText(left.assetName, right.assetName)); + if (rows.length === 0) { + fail(prefix, `${product} has no artifact targets for surface ${surface}`); + } + const names = rows.map((row) => row.assetName); + const duplicates = [...new Set(names.filter((name, index) => names.indexOf(name) !== index))].sort(compareText); + if (duplicates.length > 0) { + fail(prefix, `${product} has duplicate expected asset names: ${duplicates.join(", ")}`); + } + return rows; +} + function aggregateReleaseAssetArtifactRow(product, prefix) { const config = productConfig(product, prefix); const releaseArtifacts = config.release_artifacts; @@ -889,9 +934,8 @@ export async function currentProductVersion(product, prefix = "release-artifact- } export function expectedAssets(product, kind, version, prefix) { - const assets = artifactTargets(product, kind, prefix).map((target) => - target.asset.replaceAll("{version}", version), - ); + const assets = expectedAssetRows({ product, version, kinds: [kind] }, prefix) + .map((row) => row.assetName); assets.push(`${product}-${version}-release-assets.sha256`); return assets.sort(compareText); } diff --git a/tools/release/release_graph_query.mjs b/tools/release/release_graph_query.mjs index 84cb5e36..4256ac67 100644 --- a/tools/release/release_graph_query.mjs +++ b/tools/release/release_graph_query.mjs @@ -8,6 +8,7 @@ import { extensionMetadata, extensionSourceIdentity, exactExtensionProducts, + expectedAssetRows, localPublishArtifactRows, rawArtifactTargetRows, sdkPackageProducts, @@ -403,6 +404,67 @@ function runLocalPublishArtifacts(argv) { printJson(localPublishArtifactRows({ aggregateOnly }, TOOL)); } +function runExpectedAssets(argv) { + let product; + let version; + let surface = "github-release"; + let publishedOnly = true; + const kinds = []; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === "--product") { + if (index + 1 >= argv.length) { + fail("--product requires a value"); + } + product = argv[index + 1]; + index += 1; + } else if (value.startsWith("--product=")) { + product = value.slice("--product=".length); + } else if (value === "--version") { + if (index + 1 >= argv.length) { + fail("--version requires a value"); + } + version = argv[index + 1]; + index += 1; + } else if (value.startsWith("--version=")) { + version = value.slice("--version=".length); + } else if (value === "--surface") { + if (index + 1 >= argv.length) { + fail("--surface requires a value"); + } + surface = argv[index + 1]; + index += 1; + } else if (value.startsWith("--surface=")) { + surface = value.slice("--surface=".length); + } else if (value === "--kind") { + if (index + 1 >= argv.length) { + fail("--kind requires a value"); + } + kinds.push(argv[index + 1]); + index += 1; + } else if (value.startsWith("--kind=")) { + kinds.push(value.slice("--kind=".length)); + } else if (value === "--include-unpublished") { + publishedOnly = false; + } else { + fail(`unknown argument ${value}`); + } + } + if (product === undefined) { + fail("--product is required"); + } + if (version === undefined) { + fail("--version is required"); + } + printJson(expectedAssetRows({ + product, + version, + surface, + publishedOnly, + kinds: kinds.length === 0 ? undefined : kinds, + }, TOOL)); +} + function runExtensionMetadata(argv) { let product; for (let index = 0; index < argv.length; index += 1) { @@ -447,6 +509,7 @@ Commands: sdk-package-products [--product PRODUCT] ci-artifact-names --family release-assets|npm-package --product PRODUCT --kind KIND local-publish-artifacts [--aggregate-only] + expected-assets --product PRODUCT --version VERSION [--surface SURFACE] [--kind KIND...] [--include-unpublished] compatibility-version-entries [--require-source-product] wasix-cargo-artifact-contract `; @@ -482,6 +545,8 @@ function main(argv) { runCiArtifactNames(rest); } else if (command === "local-publish-artifacts") { runLocalPublishArtifacts(rest); + } else if (command === "expected-assets") { + runExpectedAssets(rest); } else if (command === "compatibility-version-entries") { runCompatibilityVersionEntries(rest); } else if (command === "wasix-cargo-artifact-contract") { From 36f5798f7dacb1513f79e98d16ec9df51019ad5b Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 13:37:05 +0000 Subject: [PATCH 185/308] refactor: centralize registry package selection --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 24 +++++++++ tools/release/check_release_metadata.py | 9 ++++ tools/release/product_metadata.py | 25 ++++++--- tools/release/release-artifact-targets.mjs | 51 +++++++++++++++++++ tools/release/release_graph_query.mjs | 35 +++++++++++++ 5 files changed, 138 insertions(+), 6 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index c6462a4c..09748af9 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,30 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Moved registry package-name selection out of the Python + compatibility layer and into the Bun release graph. `release-artifact-targets.mjs` + now exposes `registryPackageRows`, `release_graph_query.mjs registry-packages + --product PRODUCT [--kind KIND]` returns parsed registry package rows, and + `product_metadata.registry_package_names` now validates and adapts those rows + for legacy release callers such as `release.py` Cargo/Maven publish helpers. + The parser preserves Maven coordinates with embedded colons by splitting only + the leading `kind:` prefix. `check_release_metadata.py` now rejects + reintroducing Python-side `registry_packages` parsing. A subagent review was + attempted again for this slice, but the current session is still at the agent + thread limit, so this pass used local repository evidence. Fresh checks + passed: `tools/dev/bun.sh tools/release/release_graph_query.mjs + registry-packages --product liboliphaunt-native --kind crates`, + `tools/dev/bun.sh tools/release/release_graph_query.mjs registry-packages + --product oliphaunt-kotlin --kind maven`, Python smoke checks for + `product_metadata.registry_package_names`, `python3 -m py_compile` for + touched Python helpers, `python3 tools/release/check_release_metadata.py`, + `python3 tools/release/check_consumer_shape.py`, `tools/release/release.py + check`, `bash tools/policy/check-tooling-stack.sh`, `bash + tools/policy/check-policy-tools.sh`, `bash tools/policy/check-docs.sh`, and + `tools/release/local_registry_publish.py download --preset local-publish + --dry-run`. The Python entrypoint inventory still reports 9 Python entrypoints; + `product_metadata.py` is now 825 lines and 30,733 bytes, while + `check_release_metadata.py` is 1,767 lines and 90,577 bytes. - 2026-06-27: Moved expected GitHub release asset-name selection out of the Python compatibility layer and into the Bun release graph. `release-artifact-targets.mjs` now exposes `expectedAssetRows`, `release_graph_query.mjs expected-assets diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index f85f9e19..ad553ade 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -323,6 +323,15 @@ def validate_graph_files(graph: dict) -> None: or "allowed_kinds = set(kinds)" in product_metadata_source ): fail("expected release asset names must come from the shared Bun release graph query") + if ( + '"registry-packages"' not in product_metadata_source + or "export function registryPackageRows(" not in release_artifact_targets + or "registry-packages --product PRODUCT [--kind KIND]" not in release_graph_query + or "registryPackageRows({ product, packageKind }, TOOL)" not in release_graph_query + or 'for raw in string_list(product_config(product), "registry_packages", product)' in product_metadata_source + or 'raw.partition(":")' in product_metadata_source + ): + fail("registry package name selection must come from the shared Bun release graph query") if ( '"local-publish-artifacts"' not in product_metadata_source or "export function localPublishArtifactRows(" not in release_artifact_targets diff --git a/tools/release/product_metadata.py b/tools/release/product_metadata.py index aa8bd1aa..4aff939e 100644 --- a/tools/release/product_metadata.py +++ b/tools/release/product_metadata.py @@ -668,14 +668,27 @@ def string_list(config: dict, key: str, product: str) -> list[str]: return value +@lru_cache(maxsize=None) +def _registry_package_rows(product: str, package_kind: str | None = None) -> tuple[dict[str, Any], ...]: + args = ["--product", product] + if package_kind is not None: + args.extend(["--kind", package_kind]) + return _release_graph_query_rows("registry-packages", tuple(args)) + + 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) + for row in _registry_package_rows(product, package_kind): + row_product = row.get("product") + kind = row.get("packageKind") + name = row.get("packageName") + if row_product != product: + fail(f"release graph registry-packages returned row for {row_product!r}, expected {product!r}") + if kind != package_kind: + fail(f"release graph registry-packages returned {product}.{kind!r}, expected {package_kind!r}") + if not isinstance(name, str) or not name: + fail(f"release graph registry-packages {product}.{package_kind} packageName must be a non-empty string") + names.append(name) duplicates = sorted({name for name in names if names.count(name) > 1}) if duplicates: fail( diff --git a/tools/release/release-artifact-targets.mjs b/tools/release/release-artifact-targets.mjs index b5784933..0dccbe77 100644 --- a/tools/release/release-artifact-targets.mjs +++ b/tools/release/release-artifact-targets.mjs @@ -729,6 +729,57 @@ export function expectedAssetRows( return rows; } +export function registryPackageRows( + { + product, + packageKind = undefined, + } = {}, + prefix = "release-artifact-targets.mjs", +) { + if (typeof product !== "string" || product.length === 0) { + fail(prefix, "registry package rows require a product"); + } + if ( + packageKind !== undefined + && (typeof packageKind !== "string" || packageKind.length === 0) + ) { + fail(prefix, "registry package kind must be a non-empty string"); + } + const config = productConfig(product, prefix); + const entries = config.registry_packages ?? []; + if (!Array.isArray(entries) || entries.some((entry) => typeof entry !== "string")) { + fail(prefix, `${product}.registry_packages must be a string list`); + } + const rows = []; + const seen = new Set(); + for (const raw of entries) { + const separator = raw.indexOf(":"); + if (separator <= 0 || separator === raw.length - 1) { + fail(prefix, `${product}.registry_packages entry ${JSON.stringify(raw)} must use kind:name`); + } + const kind = raw.slice(0, separator); + const packageName = raw.slice(separator + 1); + const key = `${kind}\0${packageName}`; + if (seen.has(key)) { + fail(prefix, `${product} declares duplicate ${kind} registry package ${packageName}`); + } + seen.add(key); + if (packageKind !== undefined && kind !== packageKind) { + continue; + } + rows.push({ + product, + packageKind: kind, + packageName, + raw, + }); + } + return rows.sort((left, right) => + compareText(left.packageKind, right.packageKind) + || compareText(left.packageName, right.packageName) + ); +} + function aggregateReleaseAssetArtifactRow(product, prefix) { const config = productConfig(product, prefix); const releaseArtifacts = config.release_artifacts; diff --git a/tools/release/release_graph_query.mjs b/tools/release/release_graph_query.mjs index 4256ac67..fc6d845f 100644 --- a/tools/release/release_graph_query.mjs +++ b/tools/release/release_graph_query.mjs @@ -11,6 +11,7 @@ import { expectedAssetRows, localPublishArtifactRows, rawArtifactTargetRows, + registryPackageRows, sdkPackageProducts, typescriptOptionalRuntimePackageProducts, } from "./release-artifact-targets.mjs"; @@ -465,6 +466,37 @@ function runExpectedAssets(argv) { }, TOOL)); } +function runRegistryPackages(argv) { + let product; + let packageKind; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === "--product") { + if (index + 1 >= argv.length) { + fail("--product requires a value"); + } + product = argv[index + 1]; + index += 1; + } else if (value.startsWith("--product=")) { + product = value.slice("--product=".length); + } else if (value === "--kind") { + if (index + 1 >= argv.length) { + fail("--kind requires a value"); + } + packageKind = argv[index + 1]; + index += 1; + } else if (value.startsWith("--kind=")) { + packageKind = value.slice("--kind=".length); + } else { + fail(`unknown argument ${value}`); + } + } + if (product === undefined) { + fail("--product is required"); + } + printJson(registryPackageRows({ product, packageKind }, TOOL)); +} + function runExtensionMetadata(argv) { let product; for (let index = 0; index < argv.length; index += 1) { @@ -510,6 +542,7 @@ Commands: ci-artifact-names --family release-assets|npm-package --product PRODUCT --kind KIND local-publish-artifacts [--aggregate-only] expected-assets --product PRODUCT --version VERSION [--surface SURFACE] [--kind KIND...] [--include-unpublished] + registry-packages --product PRODUCT [--kind KIND] compatibility-version-entries [--require-source-product] wasix-cargo-artifact-contract `; @@ -547,6 +580,8 @@ function main(argv) { runLocalPublishArtifacts(rest); } else if (command === "expected-assets") { runExpectedAssets(rest); + } else if (command === "registry-packages") { + runRegistryPackages(rest); } else if (command === "compatibility-version-entries") { runCompatibilityVersionEntries(rest); } else if (command === "wasix-cargo-artifact-contract") { From 14656c5e2543002832d61cba17c14a3a3005103f Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 13:44:31 +0000 Subject: [PATCH 186/308] refactor: centralize extension product discovery --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 18 ++++++++++++++++++ tools/release/check_release_metadata.py | 2 ++ tools/release/product_metadata.py | 16 +++++++++++----- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 09748af9..2590b037 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,24 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Moved extension product discovery in the Python compatibility + layer onto the existing Bun `extension-metadata` query. `product_metadata.extension_product_ids` + now validates and adapts the structured extension metadata rows instead of + filtering raw product configs for `kind == "exact-extension-artifact"`. + `check_release_metadata.py` now rejects reintroducing that Python-side product + kind filter and asserts the query remains backed by `exactExtensionProducts`. + Fresh checks passed: `tools/dev/bun.sh tools/release/release_graph_query.mjs + extension-metadata`, Python smoke checks for `product_metadata.extension_product_ids`, + `python3 -m py_compile` for touched Python helpers, `python3 + tools/release/check_release_metadata.py`, `python3 + tools/release/check_consumer_shape.py`, `python3 + src/extensions/tools/check-extension-model.py`, `tools/release/release.py + check`, `bash tools/policy/check-tooling-stack.sh`, `bash + tools/policy/check-policy-tools.sh`, `bash tools/policy/check-docs.sh`, and + `tools/release/local_registry_publish.py download --preset local-publish + --dry-run`. The Python entrypoint inventory still reports 9 Python entrypoints; + `product_metadata.py` is now 831 lines and 31,107 bytes, while + `check_release_metadata.py` is 1,769 lines and 90,735 bytes. - 2026-06-27: Moved registry package-name selection out of the Python compatibility layer and into the Bun release graph. `release-artifact-targets.mjs` now exposes `registryPackageRows`, `release_graph_query.mjs registry-packages diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index ad553ade..0781b806 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -264,6 +264,8 @@ def validate_graph_files(graph: dict) -> None: or "extension-metadata [--product PRODUCT]" not in release_graph_query or "export function extensionMetadata(" not in release_artifact_targets or "export function extensionSourceIdentity(" not in release_artifact_targets + or "exactExtensionProducts(TOOL)" not in release_graph_query + or 'config.get("kind") == "exact-extension-artifact"' in product_metadata_source or "function extensionMetadata(" in build_extension_ci_artifacts or "function extensionSourceIdentity(" in build_extension_ci_artifacts or "function extensionMetadata(" in check_staged_artifacts diff --git a/tools/release/product_metadata.py b/tools/release/product_metadata.py index 4aff939e..089ccc0f 100644 --- a/tools/release/product_metadata.py +++ b/tools/release/product_metadata.py @@ -602,11 +602,17 @@ def product_ids(graph: dict | None = None) -> list[str]: def extension_product_ids(graph: dict | None = None) -> list[str]: - return sorted( - product - for product, config in graph_products(graph).items() - if config.get("kind") == "exact-extension-artifact" - ) + products: list[str] = [] + for row in _extension_metadata_rows(): + product = row.get("product") + if not isinstance(product, str) or not product: + fail("release graph extension-metadata rows must declare a non-empty product") + products.append(product) + if len(products) != len(set(products)): + fail("release graph extension-metadata query returned duplicate extension products") + if not products: + fail("release graph returned no extension products") + return sorted(products) @lru_cache(maxsize=None) From 4674cfefe3e4cde0b82f4c28eab28ac5d5d58d0c Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 13:52:37 +0000 Subject: [PATCH 187/308] refactor: reuse extension product selector --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 23 +++++++++++++++++++ tools/policy/check-release-policy.py | 6 +---- tools/release/check_artifact_targets.py | 6 +---- tools/release/check_consumer_shape.py | 6 +---- tools/release/check_release_metadata.py | 6 +++++ 5 files changed, 32 insertions(+), 15 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 2590b037..40147de3 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,29 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Removed the remaining duplicated exact-extension product selector + comprehensions from Python release validators. `check_artifact_targets.py`, + `check_consumer_shape.py`, and `check-release-policy.py` now use + `product_metadata.extension_product_ids()`, which is backed by the Bun + `extension-metadata` query, while retaining the per-product checks that verify + each exact-extension config still declares `kind = "exact-extension-artifact"`. + `check_release_metadata.py` now guards those validator call sites so exact + extension product discovery stays centralized. A subagent review was attempted + for this slice, but the current session is still at the agent thread limit, so + this pass used local repository evidence. Fresh checks passed: Python + `py_compile` for touched Python helpers, `python3 + tools/release/check_artifact_targets.py`, `python3 + tools/release/check_release_metadata.py`, `python3 + tools/release/check_consumer_shape.py`, `tools/release/release.py check`, + `python3 src/extensions/tools/check-extension-model.py`, `bash + tools/policy/check-tooling-stack.sh`, `bash tools/policy/check-policy-tools.sh`, + `bash tools/policy/check-docs.sh`, and + `tools/release/local_registry_publish.py download --preset local-publish + --dry-run`. The Python entrypoint inventory still reports 9 Python entrypoints; + `check_artifact_targets.py` is now 1,441 lines and 72,452 bytes, + `check_consumer_shape.py` is 2,274 lines and 97,180 bytes, + `check-release-policy.py` is 1,541 lines and 65,328 bytes, and + `check_release_metadata.py` is 1,775 lines and 91,280 bytes. - 2026-06-27: Moved extension product discovery in the Python compatibility layer onto the existing Bun `extension-metadata` query. `product_metadata.extension_product_ids` now validates and adapts the structured extension metadata rows instead of diff --git a/tools/policy/check-release-policy.py b/tools/policy/check-release-policy.py index a569aa79..2e895be8 100644 --- a/tools/policy/check-release-policy.py +++ b/tools/policy/check-release-policy.py @@ -451,11 +451,7 @@ def check_release_metadata(graph: dict) -> None: fail("release metadata must define products") if set(products) != expected_products(): fail(f"release product set mismatch: expected {sorted(expected_products())}, got {sorted(products)}") - modeled_extension_products = { - product - for product in product_metadata.product_ids(graph) - if product_metadata.product_config(product, graph).get("kind") == "exact-extension-artifact" - } + modeled_extension_products = set(product_metadata.extension_product_ids(graph)) expected_extension_products = expected_extension_products_from_sdk_catalog() if modeled_extension_products != expected_extension_products: fail( diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py index f69c1596..89491e38 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -194,11 +194,7 @@ def wasm_extension_target_id(runtime_target: str) -> str: def validate_extension_artifact_targets() -> None: - extension_products = [ - product - for product in product_metadata.product_ids() - if product_metadata.product_config(product).get("kind") == "exact-extension-artifact" - ] + extension_products = product_metadata.extension_product_ids() if not extension_products: fail("exact-extension release products must be modeled as release products") diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index f1161791..8f91fa4f 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -2147,11 +2147,7 @@ def check_exact_extension(findings: list[Finding], product: str) -> None: def exact_extension_products() -> set[str]: - return { - product - for product in product_metadata.product_ids() - if product_metadata.product_config(product).get("kind") == "exact-extension-artifact" - } + return set(product_metadata.extension_product_ids()) def known_consumer_products() -> set[str]: diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 0781b806..0550e019 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -250,6 +250,9 @@ def validate_graph_files(graph: dict) -> None: sync_release_pr = read_text("tools/release/sync-release-pr.mjs") build_extension_ci_artifacts = read_text("tools/release/build-extension-ci-artifacts.mjs") check_staged_artifacts = read_text("tools/release/check-staged-artifacts.mjs") + check_artifact_targets = read_text("tools/release/check_artifact_targets.py") + check_consumer_shape = read_text("tools/release/check_consumer_shape.py") + release_policy = read_text("tools/policy/check-release-policy.py") if ( "_release_metadata(product).get(\"compatibility_versions\"" in product_metadata_source or "_compatibility_version_entries(" in product_metadata_source @@ -266,6 +269,9 @@ def validate_graph_files(graph: dict) -> None: or "export function extensionSourceIdentity(" not in release_artifact_targets or "exactExtensionProducts(TOOL)" not in release_graph_query or 'config.get("kind") == "exact-extension-artifact"' in product_metadata_source + or "extension_products = product_metadata.extension_product_ids()" not in check_artifact_targets + or "return set(product_metadata.extension_product_ids())" not in check_consumer_shape + or "modeled_extension_products = set(product_metadata.extension_product_ids(graph))" not in release_policy or "function extensionMetadata(" in build_extension_ci_artifacts or "function extensionSourceIdentity(" in build_extension_ci_artifacts or "function extensionMetadata(" in check_staged_artifacts From 1fed3b1b6a459b393fd03d9d37f81e28bce7d84b Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 14:07:54 +0000 Subject: [PATCH 188/308] refactor: centralize wasix extension package names --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 39 ++++++++++++++ tools/release/check_release_metadata.py | 13 +++++ ...kage_liboliphaunt_wasix_cargo_artifacts.py | 16 +----- tools/release/product_metadata.py | 35 ++++++++++--- tools/release/release_graph_query.mjs | 52 ++++++++++++++++++- 5 files changed, 134 insertions(+), 21 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 40147de3..119c3758 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,45 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Centralized WASIX extension Cargo package naming behind the Bun + WASIX artifact contract. `release_graph_query.mjs + wasix-extension-package-names --product PRODUCT [--target TARGET...]` now + adapts `wasixExtensionPackageName(product)` and + `wasixExtensionAotPackageName(product, target)` from + `wasix-cargo-artifact-contract.mjs`; `product_metadata.py` only validates and + adapts that shared query, and the WASIX Cargo artifact packager now consumes + `product_metadata.wasix_extension_package_name` and + `product_metadata.wasix_extension_aot_package_name` instead of carrying local + duplicate string builders. `check_release_metadata.py` now guards against + reintroducing Python-side WASIX extension naming. Fresh generated-crate probes + confirmed the split tool contract: native root runtime parts contain + `postgres`, `initdb`, and `pg_ctl` with no `pg_dump`/`psql`; native + `oliphaunt-tools-*` parts contain `pg_dump` and `psql`; the WASIX root archive + contains `oliphaunt/bin/postgres` and `oliphaunt/bin/initdb` with no + `pg_ctl`/`pg_dump`/`psql`; `oliphaunt-wasix-tools` contains + `pg_dump.wasix.wasm` and `psql.wasix.wasm` with no `pg_ctl`; and tools AOT + crates carry `pg_dump`/`psql` AOT artifacts separately. Strict local Cargo + publishing generated 675 crate files across `target/local-registries/cargo` + and `target/local-registries/cargo-generated` with 0 crates over the 10 MiB + limit; the largest crates are the PostGIS WASIX AOT part crates at about + 9.74 MiB. Fresh checks passed: `tools/dev/bun.sh + tools/release/release_graph_query.mjs wasix-extension-package-names --product + oliphaunt-extension-unaccent --target x86_64-unknown-linux-gnu`, Python smoke + checks for `product_metadata.wasix_extension_package_name` and + `product_metadata.wasix_extension_aot_package_name`, `python3 -m py_compile` + for touched Python helpers, `python3 tools/release/check_release_metadata.py`, + `python3 tools/release/check_consumer_shape.py`, `python3 + tools/release/check_artifact_targets.py`, `tools/release/local_registry_publish.py + publish --surface cargo --strict`, `tools/release/release.py check`, `bash + tools/policy/check-tooling-stack.sh`, `bash tools/policy/check-policy-tools.sh`, + `tools/dev/bun.sh tools/policy/check-python-entrypoints.mjs --json`, and + `tools/release/local_registry_publish.py download --preset local-publish + --dry-run`. A subagent review was not used for this slice; local generated + artifacts and repository guards provided the evidence. The Python entrypoint + inventory still reports 9 Python entrypoints; `product_metadata.py` is now 854 + lines and 32,583 bytes, `package_liboliphaunt_wasix_cargo_artifacts.py` is + 1,403 lines and 53,890 bytes, `check_release_metadata.py` is 1,788 lines and + 92,240 bytes, and `release_graph_query.mjs` is 648 lines and 18,961 bytes. - 2026-06-27: Removed the remaining duplicated exact-extension product selector comprehensions from Python release validators. `check_artifact_targets.py`, `check_consumer_shape.py`, and `check-release-policy.py` now use diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 0550e019..b33d4286 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -340,6 +340,15 @@ def validate_graph_files(graph: dict) -> None: or 'raw.partition(":")' in product_metadata_source ): fail("registry package name selection must come from the shared Bun release graph query") + if ( + '"wasix-extension-package-names"' not in product_metadata_source + or "wasix-extension-package-names --product PRODUCT [--target TARGET...]" not in release_graph_query + or "wasixExtensionPackageName(product)" not in release_graph_query + or "wasixExtensionAotPackageName(product, target)" not in release_graph_query + or 'return f"{product}-wasix"' in product_metadata_source + or 'return f"{product}-wasix-aot-{target}"' in product_metadata_source + ): + fail("WASIX extension package names must come from the shared Bun WASIX Cargo artifact contract query") if ( '"local-publish-artifacts"' not in product_metadata_source or "export function localPublishArtifactRows(" not in release_artifact_targets @@ -1614,6 +1623,10 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None or "product_metadata.wasix_tools_payload_files()" not in wasix_packager_source or "product_metadata.wasix_forbidden_runtime_archive_tool_files()" not in wasix_packager_source or "product_metadata.wasix_tools_aot_artifacts()" not in wasix_packager_source + or "product_metadata.wasix_extension_package_name(" not in wasix_packager_source + or "product_metadata.wasix_extension_aot_package_name(" not in wasix_packager_source + or "def wasix_extension_package_name(product" in wasix_packager_source + or "def wasix_extension_aot_package_name(product" 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 publishable tools crates while keeping only postgres/initdb in root runtime crates") diff --git a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py index 109b0f29..9de39c01 100644 --- a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py +++ b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py @@ -750,18 +750,6 @@ 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 wasix_extension_aot_part_package_name(package_name: str, index: int) -> str: return f"{package_name}-part-{index:03d}" @@ -833,7 +821,7 @@ def extension_aot_specs(extension_dir: Path, *, product: str, version: str, sql_ seen_targets.add(target) specs.append( ExtensionAotCargoSpec( - name=wasix_extension_aot_package_name(product, target), + name=product_metadata.wasix_extension_aot_package_name(product, target), version=version, sql_name=sql_name, target=target, @@ -858,7 +846,7 @@ def extension_cargo_specs(extension_roots: list[Path]) -> list[ExtensionCargoSpe continue specs.append( ExtensionCargoSpec( - name=wasix_extension_package_name(str(product)), + name=product_metadata.wasix_extension_package_name(str(product)), product=str(product), version=str(version), sql_name=str(sql_name), diff --git a/tools/release/product_metadata.py b/tools/release/product_metadata.py index 089ccc0f..629bc2e1 100644 --- a/tools/release/product_metadata.py +++ b/tools/release/product_metadata.py @@ -338,16 +338,39 @@ def wasix_expected_extension_aot_targets() -> tuple[str, ...]: return _wasix_contract_string_list("expectedExtensionAotTargets") +@lru_cache(maxsize=None) +def _wasix_extension_package_names(product: str, targets: tuple[str, ...] = ()) -> dict[str, Any]: + args: list[str] = ["--product", product] + for target in targets: + args.extend(["--target", target]) + value = _release_graph_query_json("wasix-extension-package-names", tuple(args)) + if not isinstance(value, dict): + fail("release graph wasix-extension-package-names query must return a JSON object") + if value.get("product") != product: + fail(f"release graph wasix-extension-package-names returned product {value.get('product')!r}, expected {product!r}") + package_name = value.get("packageName") + if not isinstance(package_name, str) or not package_name: + fail(f"release graph wasix-extension-package-names {product}.packageName must be a non-empty string") + aot_packages = value.get("aotPackages") + if not isinstance(aot_packages, list) or not all(isinstance(row, dict) for row in aot_packages): + fail(f"release graph wasix-extension-package-names {product}.aotPackages must be an object list") + return value + + def wasix_extension_package_name(product: str) -> str: - if not product: - fail("WASIX extension package product must be non-empty") - return f"{product}-wasix" + return str(_wasix_extension_package_names(product).get("packageName")) def wasix_extension_aot_package_name(product: str, target: str) -> str: - if not product or not target: - fail("WASIX extension AOT package product and target must be non-empty") - return f"{product}-wasix-aot-{target}" + rows = _wasix_extension_package_names(product, (target,)).get("aotPackages") + assert isinstance(rows, list) + matches = [row for row in rows if row.get("target") == target] + if len(matches) != 1: + fail(f"release graph returned {len(matches)} WASIX extension AOT package names for {product}/{target}") + package_name = matches[0].get("packageName") + if not isinstance(package_name, str) or not package_name: + fail(f"release graph wasix-extension-package-names {product}/{target}.packageName must be a non-empty string") + return package_name @lru_cache(maxsize=None) diff --git a/tools/release/release_graph_query.mjs b/tools/release/release_graph_query.mjs index fc6d845f..fce18ce1 100644 --- a/tools/release/release_graph_query.mjs +++ b/tools/release/release_graph_query.mjs @@ -24,7 +24,11 @@ import { releaseOrder, releaseProductProjectId, } from "./release-graph.mjs"; -import { wasixCargoArtifactContract } from "./wasix-cargo-artifact-contract.mjs"; +import { + wasixCargoArtifactContract, + wasixExtensionAotPackageName, + wasixExtensionPackageName, +} from "./wasix-cargo-artifact-contract.mjs"; const TOOL = "release_graph_query.mjs"; @@ -264,6 +268,49 @@ function runWasixCargoArtifactContract() { printJson(wasixCargoArtifactContract()); } +function runWasixExtensionPackageNames(argv) { + let product; + const targets = []; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === "--product") { + if (index + 1 >= argv.length) { + fail("--product requires a value"); + } + product = argv[index + 1]; + index += 1; + } else if (value.startsWith("--product=")) { + product = value.slice("--product=".length); + } else if (value === "--target") { + if (index + 1 >= argv.length) { + fail("--target requires a value"); + } + targets.push(argv[index + 1]); + index += 1; + } else if (value.startsWith("--target=")) { + targets.push(value.slice("--target=".length)); + } else { + fail(`unknown argument ${value}`); + } + } + if (product === undefined || product.length === 0) { + fail("--product is required"); + } + for (const target of targets) { + if (target.length === 0) { + fail("--target values must be non-empty"); + } + } + printJson({ + product, + packageName: wasixExtensionPackageName(product), + aotPackages: targets.map((target) => ({ + target, + packageName: wasixExtensionAotPackageName(product, target), + })), + }); +} + function runCompatibilityVersionEntries(argv) { let requireSourceProduct = false; for (const value of argv) { @@ -543,6 +590,7 @@ Commands: local-publish-artifacts [--aggregate-only] expected-assets --product PRODUCT --version VERSION [--surface SURFACE] [--kind KIND...] [--include-unpublished] registry-packages --product PRODUCT [--kind KIND] + wasix-extension-package-names --product PRODUCT [--target TARGET...] compatibility-version-entries [--require-source-product] wasix-cargo-artifact-contract `; @@ -584,6 +632,8 @@ function main(argv) { runRegistryPackages(rest); } else if (command === "compatibility-version-entries") { runCompatibilityVersionEntries(rest); + } else if (command === "wasix-extension-package-names") { + runWasixExtensionPackageNames(rest); } else if (command === "wasix-cargo-artifact-contract") { runWasixCargoArtifactContract(); } else if (command === "--help" || command === "-h") { From 453daf63687b21b2287c523408fb505f8981a996 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 14:19:31 +0000 Subject: [PATCH 189/308] refactor: centralize product config metadata --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 27 +++++++ tools/release/check_release_metadata.py | 10 +++ tools/release/product_metadata.py | 70 ++++++++++++++----- tools/release/release-graph.mjs | 19 +++++ tools/release/release_graph_query.mjs | 26 +++++++ 5 files changed, 135 insertions(+), 17 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 119c3758..f579b698 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,33 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Moved basic release product config reads behind the Bun release + graph query. `release-graph.mjs` now exposes `productConfigRows`, + `release_graph_query.mjs product-configs [--product PRODUCT]` returns + normalized product rows, and `product_metadata.graph_products`, + `product_metadata.product_config`, `product_metadata.product_ids`, + `version_files`, `derived_version_files`, `changelog_path`, and `tag_prefix` + now validate and adapt that query instead of inspecting `graph.products` + directly. The adapter preserves the legacy empty-list default for optional + `registry_packages`, which keeps products such as `oliphaunt-swift` + compatible while still validating present values. `check_release_metadata.py` + now guards against reintroducing Python-side product config parsing. Fresh + checks passed: `tools/dev/bun.sh tools/release/release_graph_query.mjs + product-configs --product liboliphaunt-wasix`, Python smoke checks for + `product_metadata.product_ids`, `package_path`, `tag_prefix`, + `product_config`, and `version_files`, `python3 -m py_compile` for touched + Python helpers, `python3 tools/release/check_release_metadata.py`, `python3 + tools/release/check_consumer_shape.py`, `python3 + tools/release/check_artifact_targets.py`, `tools/release/release.py check`, + `bash tools/policy/check-tooling-stack.sh`, `bash + tools/policy/check-policy-tools.sh`, `bash tools/policy/check-docs.sh`, + `tools/dev/bun.sh tools/policy/check-python-entrypoints.mjs --json`, and + `tools/release/local_registry_publish.py download --preset local-publish + --dry-run`. The Python entrypoint inventory still reports 9 Python + entrypoints; `product_metadata.py` is now 890 lines and 34,330 bytes, + `check_release_metadata.py` is 1,798 lines and 92,888 bytes, + `release_graph_query.mjs` is 674 lines and 19,731 bytes, and + `release-graph.mjs` is 822 lines and 30,022 bytes. - 2026-06-27: Centralized WASIX extension Cargo package naming behind the Bun WASIX artifact contract. `release_graph_query.mjs wasix-extension-package-names --product PRODUCT [--target TARGET...]` now diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index b33d4286..0ba0d007 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -246,6 +246,7 @@ def validate_graph_files(graph: dict) -> None: product_metadata.validate_all_extension_metadata(graph) product_metadata_source = read_text("tools/release/product_metadata.py") release_graph_query = read_text("tools/release/release_graph_query.mjs") + release_graph_source = read_text("tools/release/release-graph.mjs") release_artifact_targets = read_text("tools/release/release-artifact-targets.mjs") sync_release_pr = read_text("tools/release/sync-release-pr.mjs") build_extension_ci_artifacts = read_text("tools/release/build-extension-ci-artifacts.mjs") @@ -291,6 +292,15 @@ def validate_graph_files(graph: dict) -> None: or "import tomllib" in product_metadata_source ): fail("current product version values must be read through the Bun release graph product-versions query") + if ( + '"product-configs"' not in product_metadata_source + or "product-configs [--product PRODUCT]" not in release_graph_query + or "productConfigRows({ product }, TOOL)" not in release_graph_query + or "export function productConfigRows(" not in release_graph_source + or "source = load_graph() if graph is None else graph" in product_metadata_source + or 'products = source.get("products")' in product_metadata_source + ): + fail("product config metadata must be adapted through the Bun release graph product-configs query") if ( "typescript_optional_runtime_package_products(" in product_metadata_source or "typescript-broker" in product_metadata_source diff --git a/tools/release/product_metadata.py b/tools/release/product_metadata.py index 629bc2e1..52f1cb59 100644 --- a/tools/release/product_metadata.py +++ b/tools/release/product_metadata.py @@ -598,30 +598,66 @@ def typescript_optional_runtime_package_versions() -> dict[str, str]: return versions +@lru_cache(maxsize=None) +def _product_config_rows(product: str | None = None) -> tuple[dict[str, Any], ...]: + args = () if product is None else ("--product", product) + rows = _release_graph_query_rows("product-configs", args) + if product is not None and len(rows) != 1: + fail(f"release graph product-configs query must return one row for {product}, got {len(rows)}") + seen: set[str] = set() + parsed: list[dict[str, Any]] = [] + for row in rows: + product_id = row.get("product") + config_id = row.get("id") + if not isinstance(product_id, str) or not product_id: + fail("release graph product-configs rows must declare a non-empty product") + if product_id in seen: + fail(f"release graph product-configs query returned duplicate product {product_id}") + seen.add(product_id) + if config_id != product_id: + fail(f"release graph product-configs {product_id}.id must match the product id") + for key in ["kind", "owner", "path", "changelog_path", "tag_prefix"]: + value = row.get(key) + if not isinstance(value, str) or not value: + fail(f"release graph product-configs {product_id}.{key} must be a non-empty string") + for key in ["publish_targets", "release_artifacts", "version_files"]: + value = row.get(key) + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + fail(f"release graph product-configs {product_id}.{key} must be a string list") + if not value: + fail(f"release graph product-configs {product_id}.{key} must not be empty") + for key in ["registry_packages", "derived_version_files"]: + value = row.get(key) + if value is None: + row[key] = [] + continue + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + fail(f"release graph product-configs {product_id}.{key} must be a string list") + parsed.append(dict(row)) + if not parsed: + fail("release graph returned no product config rows") + return tuple(parsed) + + +def _product_config_row(product: str) -> dict[str, Any]: + row = dict(_product_config_rows(product)[0]) + row.pop("product", None) + return row + + def graph_products(graph: dict | None = None) -> dict[str, dict[str, Any]]: - source = load_graph() if graph is None else graph - products = source.get("products") if isinstance(source, dict) else None - if not isinstance(products, dict) or not products: - fail("release graph must contain a non-empty products object") - parsed: dict[str, dict[str, Any]] = {} - for product, config in products.items(): - if not isinstance(product, str) or not product: - fail("release graph product ids must be non-empty strings") - if not isinstance(config, dict): - fail(f"release graph product {product} config must be an object") - parsed[product] = dict(config) - return parsed + return { + str(row["product"]): _product_config_row(str(row["product"])) + for row in _product_config_rows() + } def product_config(product: str, graph: dict | None = None) -> dict[str, Any]: - config = graph_products(graph).get(product) - if config is None: - fail(f"unknown release product {product!r}") - return config + return _product_config_row(product) def product_ids(graph: dict | None = None) -> list[str]: - return list(graph_products(graph)) + return [str(row["product"]) for row in _product_config_rows()] def extension_product_ids(graph: dict | None = None) -> list[str]: diff --git a/tools/release/release-graph.mjs b/tools/release/release-graph.mjs index 9094e77e..4c19babb 100644 --- a/tools/release/release-graph.mjs +++ b/tools/release/release-graph.mjs @@ -388,6 +388,25 @@ export function loadGraph(prefix = "release-graph") { }; } +export function productConfigRows({ product = undefined } = {}, prefix = "release-graph") { + const products = loadGraph(prefix).products; + if (product !== undefined && !(product in products)) { + fail(prefix, `unknown release product ${product}`); + } + return Object.entries(products) + .filter(([productId]) => product === undefined || productId === product) + .sort(([left], [right]) => compareText(left, right)) + .map(([productId, config]) => { + if (config.id !== productId) { + fail(prefix, `${productId} release metadata id must match product id`); + } + return { + product: productId, + ...config, + }; + }); +} + function assertObject(value, context, prefix) { if (value === null || Array.isArray(value) || typeof value !== "object") { fail(prefix, `${context} must be a table`); diff --git a/tools/release/release_graph_query.mjs b/tools/release/release_graph_query.mjs index fce18ce1..95e5ae13 100644 --- a/tools/release/release_graph_query.mjs +++ b/tools/release/release_graph_query.mjs @@ -21,6 +21,7 @@ import { compareText, loadGraph, normalizeFiles, + productConfigRows, releaseOrder, releaseProductProjectId, } from "./release-graph.mjs"; @@ -134,6 +135,28 @@ function runProductProjects() { printJson(graphProductProjects(loadGraph(TOOL))); } +function runProductConfigs(argv) { + let product; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === "--product") { + if (index + 1 >= argv.length) { + fail("--product requires a value"); + } + product = argv[index + 1]; + index += 1; + } else if (value.startsWith("--product=")) { + product = value.slice("--product=".length); + } else { + fail(`unknown argument ${value}`); + } + } + if (product !== undefined && product.length === 0) { + fail("--product values must be non-empty"); + } + printJson(productConfigRows({ product }, TOOL)); +} + function runReleaseOrder(argv) { const graph = loadGraph(TOOL); const selected = assertStringList( @@ -576,6 +599,7 @@ function usage() { Commands: graph product-projects + product-configs [--product PRODUCT] release-order --products-json JSON plan [--changed-file PATH...] plans-for-paths --paths-json JSON @@ -602,6 +626,8 @@ function main(argv) { runGraph(); } else if (command === "product-projects") { runProductProjects(); + } else if (command === "product-configs") { + runProductConfigs(rest); } else if (command === "release-order") { runReleaseOrder(rest); } else if (command === "plan") { From 0d7a7e25c57ba49cb21040a279b5ad91a394ca0e Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 14:28:50 +0000 Subject: [PATCH 190/308] refactor: centralize moon release metadata --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 25 ++++++++++++ tools/release/check_release_metadata.py | 9 +++++ tools/release/product_metadata.py | 40 ++++++++++++++----- tools/release/release-graph.mjs | 24 +++++++++++ tools/release/release_graph_query.mjs | 26 ++++++++++++ 5 files changed, 114 insertions(+), 10 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index f579b698..5cdbb3ed 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,31 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Moved Moon release metadata reads behind the Bun release graph + query. `release-graph.mjs` now exposes `moonReleaseMetadataRows`, + `release_graph_query.mjs moon-release-metadata [--product PRODUCT]` returns + normalized Moon release metadata rows, and + `product_metadata.moon_release_metadata` now validates and adapts that query + instead of walking `load_graph().moon_projects` directly. This keeps + `check_artifact_targets.py` using the compatibility API while removing one + more raw graph shape dependency from Python. `check_release_metadata.py` now + guards against reintroducing Python-side `moon_projects` traversal. Fresh + checks passed: `tools/dev/bun.sh tools/release/release_graph_query.mjs + moon-release-metadata --product liboliphaunt-wasix`, Python smoke checks for + the four runtime products' `component`, `packagePath`, and `artifactTargets` + presets, `python3 -m py_compile` for touched Python helpers, `python3 + tools/release/check_release_metadata.py`, `python3 + tools/release/check_artifact_targets.py`, `python3 + tools/release/check_consumer_shape.py`, `tools/release/release.py check`, + `bash tools/policy/check-tooling-stack.sh`, `bash + tools/policy/check-policy-tools.sh`, `bash tools/policy/check-docs.sh`, + `tools/dev/bun.sh tools/policy/check-python-entrypoints.mjs --json`, and + `tools/release/local_registry_publish.py download --preset local-publish + --dry-run`. The Python entrypoint inventory still reports 9 Python + entrypoints; `product_metadata.py` is now 910 lines and 35,253 bytes, + `check_release_metadata.py` is 1,807 lines and 93,465 bytes, + `release_graph_query.mjs` is 700 lines and 20,535 bytes, and + `release-graph.mjs` is 846 lines and 31,063 bytes. - 2026-06-27: Moved basic release product config reads behind the Bun release graph query. `release-graph.mjs` now exposes `productConfigRows`, `release_graph_query.mjs product-configs [--product PRODUCT]` returns diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 0ba0d007..0fb321d2 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -301,6 +301,15 @@ def validate_graph_files(graph: dict) -> None: or 'products = source.get("products")' in product_metadata_source ): fail("product config metadata must be adapted through the Bun release graph product-configs query") + if ( + '"moon-release-metadata"' not in product_metadata_source + or "moon-release-metadata [--product PRODUCT]" not in release_graph_query + or "moonReleaseMetadataRows({ product }, TOOL)" not in release_graph_query + or "export function moonReleaseMetadataRows(" not in release_graph_source + or 'load_graph().get("moon_projects")' in product_metadata_source + or 'project.get("project")' in product_metadata_source + ): + fail("Moon release metadata must be adapted through the Bun release graph moon-release-metadata query") if ( "typescript_optional_runtime_package_products(" in product_metadata_source or "typescript-broker" in product_metadata_source diff --git a/tools/release/product_metadata.py b/tools/release/product_metadata.py index 52f1cb59..3221ce43 100644 --- a/tools/release/product_metadata.py +++ b/tools/release/product_metadata.py @@ -56,17 +56,37 @@ def package_path(product: str) -> str: return value +@lru_cache(maxsize=None) +def _moon_release_metadata_rows(product: str | None = None) -> tuple[dict[str, Any], ...]: + args = () if product is None else ("--product", product) + rows = _release_graph_query_rows("moon-release-metadata", args) + if product is not None and len(rows) != 1: + fail(f"release graph moon-release-metadata query must return one row for {product}, got {len(rows)}") + seen: set[str] = set() + parsed: list[dict[str, Any]] = [] + for row in rows: + product_id = row.get("product") + component = row.get("component") + package_path = row.get("packagePath") + if not isinstance(product_id, str) or not product_id: + fail("release graph moon-release-metadata rows must declare a non-empty product") + if product_id in seen: + fail(f"release graph moon-release-metadata returned duplicate product {product_id}") + seen.add(product_id) + if component != product_id: + fail(f"release graph moon-release-metadata {product_id}.component must match the product id") + if not isinstance(package_path, str) or not package_path: + fail(f"release graph moon-release-metadata {product_id}.packagePath must be a non-empty string") + parsed.append(dict(row)) + if not parsed: + fail("release graph returned no Moon release metadata rows") + return tuple(parsed) + + def moon_release_metadata(product: str) -> dict[str, Any]: - projects = load_graph().get("moon_projects") - project = projects.get(product) if isinstance(projects, dict) else None - if not isinstance(project, dict): - fail(f"unknown Moon release component {product!r}") - project_config = project.get("project") - metadata = project_config.get("metadata") if isinstance(project_config, dict) else None - release = metadata.get("release") if isinstance(metadata, dict) else None - if not isinstance(release, dict): - fail(f"Moon release component {product!r} has no release metadata") - return release + row = dict(_moon_release_metadata_rows(product)[0]) + row.pop("product", None) + return row def load_graph() -> dict[str, Any]: diff --git a/tools/release/release-graph.mjs b/tools/release/release-graph.mjs index 4c19babb..a56d7569 100644 --- a/tools/release/release-graph.mjs +++ b/tools/release/release-graph.mjs @@ -407,6 +407,30 @@ export function productConfigRows({ product = undefined } = {}, prefix = "releas }); } +export function moonReleaseMetadataRows({ product = undefined } = {}, prefix = "release-graph") { + const graph = loadGraph(prefix); + const productIds = product === undefined ? Object.keys(graph.products).sort(compareText) : [product]; + if (product !== undefined && !(product in graph.products)) { + fail(prefix, `unknown release product ${product}`); + } + return productIds.map((productId) => { + const release = graph.moon_projects?.[productId]?.project?.metadata?.release; + if (release === null || Array.isArray(release) || typeof release !== "object") { + fail(prefix, `Moon release metadata does not include ${productId}`); + } + if (release.component !== productId) { + fail(prefix, `Moon release metadata for ${productId} must use matching component`); + } + if (typeof release.packagePath !== "string" || release.packagePath.length === 0) { + fail(prefix, `Moon release metadata for ${productId} must declare packagePath`); + } + return { + product: productId, + ...release, + }; + }); +} + function assertObject(value, context, prefix) { if (value === null || Array.isArray(value) || typeof value !== "object") { fail(prefix, `${context} must be a table`); diff --git a/tools/release/release_graph_query.mjs b/tools/release/release_graph_query.mjs index 95e5ae13..e2348165 100644 --- a/tools/release/release_graph_query.mjs +++ b/tools/release/release_graph_query.mjs @@ -20,6 +20,7 @@ import { compatibilityVersionEntries, compareText, loadGraph, + moonReleaseMetadataRows, normalizeFiles, productConfigRows, releaseOrder, @@ -157,6 +158,28 @@ function runProductConfigs(argv) { printJson(productConfigRows({ product }, TOOL)); } +function runMoonReleaseMetadata(argv) { + let product; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === "--product") { + if (index + 1 >= argv.length) { + fail("--product requires a value"); + } + product = argv[index + 1]; + index += 1; + } else if (value.startsWith("--product=")) { + product = value.slice("--product=".length); + } else { + fail(`unknown argument ${value}`); + } + } + if (product !== undefined && product.length === 0) { + fail("--product values must be non-empty"); + } + printJson(moonReleaseMetadataRows({ product }, TOOL)); +} + function runReleaseOrder(argv) { const graph = loadGraph(TOOL); const selected = assertStringList( @@ -600,6 +623,7 @@ Commands: graph product-projects product-configs [--product PRODUCT] + moon-release-metadata [--product PRODUCT] release-order --products-json JSON plan [--changed-file PATH...] plans-for-paths --paths-json JSON @@ -628,6 +652,8 @@ function main(argv) { runProductProjects(); } else if (command === "product-configs") { runProductConfigs(rest); + } else if (command === "moon-release-metadata") { + runMoonReleaseMetadata(rest); } else if (command === "release-order") { runReleaseOrder(rest); } else if (command === "plan") { From 6a1006d9cf5854a98eeefc42b89787a3f5048aff Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 14:40:06 +0000 Subject: [PATCH 191/308] refactor: centralize moon project policy rows --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 28 ++++++++ tools/policy/check-release-policy.py | 72 ++++++++----------- tools/release/check_release_metadata.py | 10 +++ tools/release/release-graph.mjs | 23 ++++++ tools/release/release_graph_query.mjs | 26 +++++++ 5 files changed, 118 insertions(+), 41 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 5cdbb3ed..4e7fdd5c 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,34 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Moved release policy's Moon project ownership checks onto + normalized Bun graph rows. `release-graph.mjs` now carries Moon project + `layer` and exposes `moonProjectRows`, `release_graph_query.mjs + moon-projects [--project PROJECT]` returns normalized project rows with tags, + dependency scopes, release metadata, and layer, and + `check-release-policy.py` now consumes that query plus + `product_metadata.graph_products` instead of parsing `graph.products` or + invoking `moon query projects` directly. The check still verifies release + product tags, Moon release metadata, exact-extension `library` layer, and + production dependencies on `extension-runtime-contract`, `liboliphaunt-native`, + and `liboliphaunt-wasix`. `check_release_metadata.py` now rejects + reintroducing Python-side Moon project traversal in the policy check. Fresh + checks passed: `tools/dev/bun.sh tools/release/release_graph_query.mjs + moon-projects --project oliphaunt-extension-unaccent`, `python3 -m + py_compile` for touched Python helpers, `python3 + tools/policy/check-release-policy.py`, `python3 + tools/release/check_release_metadata.py`, `python3 + tools/release/check_artifact_targets.py`, `python3 + tools/release/check_consumer_shape.py`, `tools/release/release.py check`, + `bash tools/policy/check-tooling-stack.sh`, `bash + tools/policy/check-policy-tools.sh`, `bash tools/policy/check-docs.sh`, + `tools/dev/bun.sh tools/policy/check-python-entrypoints.mjs --json`, and + `tools/release/local_registry_publish.py download --preset local-publish + --dry-run`. The Python entrypoint inventory still reports 9 Python + entrypoints; `check-release-policy.py` is now 1,531 lines and 65,003 bytes, + `check_release_metadata.py` is 1,817 lines and 94,042 bytes, + `release_graph_query.mjs` is 726 lines and 21,293 bytes, and + `release-graph.mjs` is 869 lines and 31,967 bytes. - 2026-06-27: Moved Moon release metadata reads behind the Bun release graph query. `release-graph.mjs` now exposes `moonReleaseMetadataRows`, `release_graph_query.mjs moon-release-metadata [--product PRODUCT]` returns diff --git a/tools/policy/check-release-policy.py b/tools/policy/check-release-policy.py index 2e895be8..91212f73 100644 --- a/tools/policy/check-release-policy.py +++ b/tools/policy/check-release-policy.py @@ -3,7 +3,6 @@ import json import re -import os import pathlib import subprocess import sys @@ -257,6 +256,30 @@ def release_product_projects() -> dict[str, str]: return value +def moon_project_rows() -> dict[str, dict]: + value = bun_json(["tools/release/release_graph_query.mjs", "moon-projects"]) + if not isinstance(value, list) or not all(isinstance(item, dict) for item in value): + fail("release graph moon-projects query did not return an object list") + rows: dict[str, dict] = {} + for row in value: + project_id = row.get("id") + if not isinstance(project_id, str) or not project_id: + fail("release graph moon-projects rows must declare non-empty ids") + if project_id in rows: + fail(f"release graph moon-projects query returned duplicate project {project_id}") + tags = row.get("tags") + dependency_scopes = row.get("dependencyScopes") + if not isinstance(tags, list) or not all(isinstance(item, str) for item in tags): + fail(f"release graph moon-projects {project_id}.tags must be a string list") + if not isinstance(dependency_scopes, dict) or not all( + isinstance(key, str) and isinstance(value, str) + for key, value in dependency_scopes.items() + ): + fail(f"release graph moon-projects {project_id}.dependencyScopes must be a string map") + rows[project_id] = row + return rows + + def release_plans_for_single_paths(paths: list[str]) -> dict[str, dict]: value = bun_json( [ @@ -313,45 +336,14 @@ def expected_products() -> set[str]: return BASE_PRODUCTS | expected_extension_products_from_sdk_catalog() -def moon_projects() -> dict[str, dict]: - moon_bin = os.environ.get("MOON_BIN") - if moon_bin is None: - proto_moon = pathlib.Path.home() / ".proto/bin/moon" - moon_bin = str(proto_moon) if proto_moon.exists() else "moon" - output = subprocess.check_output( - [moon_bin, "query", "projects"], - cwd=ROOT, - text=True, - ) - projects = json.loads(output).get("projects") - if not isinstance(projects, list): - fail("moon query projects did not return a projects array") - return {project["id"]: project for project in projects} - - def project_release_metadata(project: dict) -> dict | None: - config = project.get("config") if isinstance(project.get("config"), dict) else {} - project_config = config.get("project") if isinstance(config.get("project"), dict) else {} - metadata = project_config.get("metadata") if isinstance(project_config.get("metadata"), dict) else {} - release = metadata.get("release") if isinstance(metadata, dict) else None - if isinstance(release, dict): - return release - release = project_config.get("release") + release = project.get("release") return release if isinstance(release, dict) else None def project_dependency_scopes(project: dict) -> dict[str, str]: - config = project.get("config") if isinstance(project.get("config"), dict) else {} - raw_deps = project.get("dependencies") or config.get("dependsOn") or [] - scopes: dict[str, str] = {} - if not isinstance(raw_deps, list): - return scopes - for dependency in raw_deps: - if isinstance(dependency, str): - scopes[dependency] = "production" - elif isinstance(dependency, dict) and isinstance(dependency.get("id"), str): - scopes[dependency["id"]] = str(dependency.get("scope") or "production") - return scopes + scopes = project.get("dependencyScopes") + return dict(scopes) if isinstance(scopes, dict) else {} def assert_no_file(path: str) -> None: @@ -446,9 +438,7 @@ def assert_text_order(text: str, snippets: list[str], message: str) -> None: def check_release_metadata(graph: dict) -> None: - products = graph.get("products") - if not isinstance(products, dict): - fail("release metadata must define products") + products = product_metadata.graph_products(graph) if set(products) != expected_products(): fail(f"release product set mismatch: expected {sorted(expected_products())}, got {sorted(products)}") modeled_extension_products = set(product_metadata.extension_product_ids(graph)) @@ -459,7 +449,7 @@ def check_release_metadata(graph: dict) -> None: f"expected {sorted(expected_extension_products)}, got {sorted(modeled_extension_products)}" ) - projects = moon_projects() + projects = moon_project_rows() product_projects = release_product_projects() for product, config in products.items(): release_path = ROOT / config["path"] / "release.toml" @@ -477,7 +467,7 @@ def check_release_metadata(graph: dict) -> None: project = projects.get(project_id) if project is None: fail(f"{product} has no owning Moon project") - tags = set(project.get("config", {}).get("tags", [])) + tags = set(project.get("tags", [])) if "release-product" not in tags: fail(f"{project_id} must be tagged release-product") release = project_release_metadata(project) @@ -489,7 +479,7 @@ def check_release_metadata(graph: dict) -> None: fail(f"{project_id} packagePath expected {config.get('path')}, got {release.get('packagePath')}") if config.get("kind") == "exact-extension-artifact": product_metadata.extension_metadata(product, graph) - layer = project.get("config", {}).get("layer") + layer = project.get("layer") if layer != "library": fail(f"{project_id} must be a library layer project; exact extension artifacts are publishable runtime-compatible products") scopes = project_dependency_scopes(project) diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 0fb321d2..ed2d8a8c 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -310,6 +310,16 @@ def validate_graph_files(graph: dict) -> None: or 'project.get("project")' in product_metadata_source ): fail("Moon release metadata must be adapted through the Bun release graph moon-release-metadata query") + if ( + "moon-projects [--project PROJECT]" not in release_graph_query + or "export function moonProjectRows(" not in release_graph_source + or 'bun_json(["tools/release/release_graph_query.mjs", "moon-projects"])' not in release_policy + or "def moon_projects(" in release_policy + or "moon query projects" in release_policy + or 'graph.get("products")' in release_policy + or 'project.get("config")' in release_policy + ): + fail("release policy must consume normalized Bun Moon project rows and product-config metadata") if ( "typescript_optional_runtime_package_products(" in product_metadata_source or "typescript-broker" in product_metadata_source diff --git a/tools/release/release-graph.mjs b/tools/release/release-graph.mjs index a56d7569..c387d165 100644 --- a/tools/release/release-graph.mjs +++ b/tools/release/release-graph.mjs @@ -171,6 +171,7 @@ export function moonProjectsById(prefix = "release-graph") { parsed.set(project.id, { id: project.id, source: project.source || config.source || "", + layer: typeof config.layer === "string" ? config.layer : undefined, dependsOn: Object.keys(dependencyScopes).sort(compareText), dependencyScopes, tags: Array.isArray(config.tags) ? [...config.tags].sort(compareText) : [], @@ -431,6 +432,28 @@ export function moonReleaseMetadataRows({ product = undefined } = {}, prefix = " }); } +export function moonProjectRows({ project = undefined } = {}, prefix = "release-graph") { + const projects = loadGraph(prefix).moon_projects; + if (project !== undefined && !(project in projects)) { + fail(prefix, `unknown Moon project ${project}`); + } + return Object.entries(projects) + .filter(([projectId]) => project === undefined || projectId === project) + .sort(([left], [right]) => compareText(left, right)) + .map(([projectId, row]) => { + const release = row.project?.metadata?.release; + return { + id: projectId, + source: row.source, + layer: row.layer, + tags: row.tags, + dependsOn: row.dependsOn, + dependencyScopes: row.dependencyScopes, + release: release && typeof release === "object" && !Array.isArray(release) ? release : null, + }; + }); +} + function assertObject(value, context, prefix) { if (value === null || Array.isArray(value) || typeof value !== "object") { fail(prefix, `${context} must be a table`); diff --git a/tools/release/release_graph_query.mjs b/tools/release/release_graph_query.mjs index e2348165..35b25322 100644 --- a/tools/release/release_graph_query.mjs +++ b/tools/release/release_graph_query.mjs @@ -20,6 +20,7 @@ import { compatibilityVersionEntries, compareText, loadGraph, + moonProjectRows, moonReleaseMetadataRows, normalizeFiles, productConfigRows, @@ -180,6 +181,28 @@ function runMoonReleaseMetadata(argv) { printJson(moonReleaseMetadataRows({ product }, TOOL)); } +function runMoonProjects(argv) { + let project; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === "--project") { + if (index + 1 >= argv.length) { + fail("--project requires a value"); + } + project = argv[index + 1]; + index += 1; + } else if (value.startsWith("--project=")) { + project = value.slice("--project=".length); + } else { + fail(`unknown argument ${value}`); + } + } + if (project !== undefined && project.length === 0) { + fail("--project values must be non-empty"); + } + printJson(moonProjectRows({ project }, TOOL)); +} + function runReleaseOrder(argv) { const graph = loadGraph(TOOL); const selected = assertStringList( @@ -624,6 +647,7 @@ Commands: product-projects product-configs [--product PRODUCT] moon-release-metadata [--product PRODUCT] + moon-projects [--project PROJECT] release-order --products-json JSON plan [--changed-file PATH...] plans-for-paths --paths-json JSON @@ -654,6 +678,8 @@ function main(argv) { runProductConfigs(rest); } else if (command === "moon-release-metadata") { runMoonReleaseMetadata(rest); + } else if (command === "moon-projects") { + runMoonProjects(rest); } else if (command === "release-order") { runReleaseOrder(rest); } else if (command === "plan") { From d865febd2c60df53248f159e04e775be1085e20d Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 15:04:31 +0000 Subject: [PATCH 192/308] refactor: query legacy artifact target rows --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 29 +++++++++++++++++++ tools/release/check_artifact_targets.py | 8 ++--- tools/release/check_release_metadata.py | 7 +++++ tools/release/product_metadata.py | 4 +++ tools/release/release_graph_query.mjs | 17 +++++++++++ 5 files changed, 59 insertions(+), 6 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 4e7fdd5c..ce6e3432 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,35 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Removed direct full release-graph reads from + `check_artifact_targets.py`. `release_graph_query.mjs` now exposes + `legacy-central-artifact-targets`, which validates and returns the deprecated + top-level `artifact_targets` rows from the Bun graph, and + `product_metadata.legacy_central_artifact_target_rows()` adapts that query for + the Python compatibility layer. `check_artifact_targets.py` now uses + `product_metadata.raw_artifact_target_tables()` without a graph argument, + preserves the legacy "no central artifact_targets" guard through the new + adapter, and no longer calls `product_metadata.load_graph()`. The metadata + guard now rejects reintroducing direct `product_metadata.load_graph()` calls in + artifact-target checks. A subagent review was attempted for this slice, but + the current session was at the agent thread limit, so this pass used local + repository evidence. Fresh checks passed: `tools/dev/bun.sh + tools/release/release_graph_query.mjs legacy-central-artifact-targets`, + Python adapter smoke for + `product_metadata.legacy_central_artifact_target_rows`, `python3 -m + py_compile` for touched Python helpers, `python3 + tools/release/check_artifact_targets.py`, `python3 + tools/release/check_release_metadata.py`, `python3 + tools/release/check_consumer_shape.py`, `tools/release/release.py check`, + `bash tools/policy/check-tooling-stack.sh`, `bash + tools/policy/check-policy-tools.sh`, `bash tools/policy/check-docs.sh`, + `tools/dev/bun.sh tools/policy/check-python-entrypoints.mjs --json`, + `tools/release/local_registry_publish.py download --preset local-publish + --dry-run`, and `git diff --check`. The Python entrypoint inventory still + reports 9 Python entrypoints; `check_artifact_targets.py` is now 1,437 lines + and 72,232 bytes, `check_release_metadata.py` is 1,824 lines and 94,495 + bytes, `product_metadata.py` is 914 lines and 35,400 bytes, and + `release_graph_query.mjs` is 743 lines and 21,931 bytes. - 2026-06-27: Moved release policy's Moon project ownership checks onto normalized Bun graph rows. `release-graph.mjs` now carries Moon project `layer` and exposes `moonProjectRows`, `release_graph_query.mjs diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py index 89491e38..cc0e51c7 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -86,7 +86,7 @@ def validate_target_shape() -> None: fail("artifact target metadata must define targets") raw_targets = { raw.get("id"): raw - for raw in product_metadata.raw_artifact_target_tables(product_metadata.load_graph()) + for raw in product_metadata.raw_artifact_target_tables() if isinstance(raw, dict) and isinstance(raw.get("id"), str) } @@ -140,13 +140,10 @@ def validate_target_shape() -> None: def validate_moon_runtime_targets() -> None: - graph_targets = product_metadata.load_graph().get("artifact_targets", []) - if not isinstance(graph_targets, list): - fail("release metadata artifact_targets must be an array of tables") + graph_targets = product_metadata.legacy_central_artifact_target_rows() central_targets = [ raw.get("id") for raw in graph_targets - if isinstance(raw, dict) ] if central_targets: fail( @@ -1423,7 +1420,6 @@ def validate_expected_product_assets() -> None: def main() -> int: - product_metadata.load_graph() validate_target_shape() validate_moon_runtime_targets() validate_extension_artifact_targets() diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index ed2d8a8c..443a7d66 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -320,6 +320,13 @@ def validate_graph_files(graph: dict) -> None: or 'project.get("config")' in release_policy ): fail("release policy must consume normalized Bun Moon project rows and product-config metadata") + if ( + '"legacy-central-artifact-targets"' not in product_metadata_source + or "legacy-central-artifact-targets" not in release_graph_query + or "product_metadata.legacy_central_artifact_target_rows()" not in check_artifact_targets + or "product_metadata.load_graph()" in check_artifact_targets + ): + fail("artifact target checks must use graph-query adapters instead of direct product_metadata.load_graph() calls") if ( "typescript_optional_runtime_package_products(" in product_metadata_source or "typescript-broker" in product_metadata_source diff --git a/tools/release/product_metadata.py b/tools/release/product_metadata.py index 3221ce43..c1742a8c 100644 --- a/tools/release/product_metadata.py +++ b/tools/release/product_metadata.py @@ -234,6 +234,10 @@ def raw_artifact_target_tables(graph: dict | None = None) -> list[dict[str, Any] ] +def legacy_central_artifact_target_rows() -> tuple[dict[str, Any], ...]: + return _release_graph_query_rows("legacy-central-artifact-targets") + + def artifact_targets( graph: dict | None = None, *, diff --git a/tools/release/release_graph_query.mjs b/tools/release/release_graph_query.mjs index 35b25322..5d440cc3 100644 --- a/tools/release/release_graph_query.mjs +++ b/tools/release/release_graph_query.mjs @@ -299,6 +299,20 @@ function runRawArtifactTargets(argv) { ); } +function runLegacyCentralArtifactTargets(argv) { + for (const value of argv) { + fail(`unknown argument ${value}`); + } + const targets = loadGraph(TOOL).artifact_targets ?? []; + if (!Array.isArray(targets)) { + fail("legacy central artifact_targets must be an array when present"); + } + if (!targets.every((target) => target !== null && typeof target === "object" && !Array.isArray(target))) { + fail("legacy central artifact_targets entries must be objects"); + } + printJson(targets); +} + function runExtensionTargets(argv) { let product; let family; @@ -653,6 +667,7 @@ Commands: plans-for-paths --paths-json JSON artifact-targets [--product PRODUCT] [--kind KIND] [--surface SURFACE] [--published-only] raw-artifact-targets [--product PRODUCT] [--kind KIND] [--surface SURFACE] [--published-only] + legacy-central-artifact-targets extension-targets [--product PRODUCT] [--family native|wasix] [--published-only] extension-metadata [--product PRODUCT] product-versions [--product PRODUCT] @@ -690,6 +705,8 @@ function main(argv) { runArtifactTargets(rest); } else if (command === "raw-artifact-targets") { runRawArtifactTargets(rest); + } else if (command === "legacy-central-artifact-targets") { + runLegacyCentralArtifactTargets(rest); } else if (command === "extension-targets") { runExtensionTargets(rest); } else if (command === "extension-metadata") { From f88ee60dd9a1302bb1c7984115aee009dbd10084 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 15:17:29 +0000 Subject: [PATCH 193/308] refactor: drop metadata graph handoff --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 21 ++++++++++ tools/release/check_release_metadata.py | 40 +++++++++---------- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index ce6e3432..c41efcc4 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,27 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Removed the direct full release-graph handoff from + `check_release_metadata.py`. The validator no longer defines a local + `load_graph()` wrapper, no longer passes a graph object into product config, + extension metadata, exact-extension registry shape, publish-target coverage, + or version collection checks, and now relies on the existing Bun-query-backed + `product_metadata` adapters directly. The release metadata guard also checks + that neither `check_release_metadata.py` nor `check_artifact_targets.py` + reintroduce direct full graph calls for the artifact-target path. A subagent + review was attempted again for this slice, but the current session remained + at the agent thread limit, so this pass used local repository evidence. Fresh + checks passed: `python3 -m py_compile + tools/release/check_release_metadata.py`, `python3 + tools/release/check_release_metadata.py`, `python3 + tools/release/check_artifact_targets.py`, `python3 + tools/release/check_consumer_shape.py`, `tools/release/release.py check`, + `bash tools/policy/check-tooling-stack.sh`, `bash + tools/policy/check-policy-tools.sh`, `bash tools/policy/check-docs.sh`, + `tools/dev/bun.sh tools/policy/check-python-entrypoints.mjs --json`, and + `tools/release/local_registry_publish.py download --preset local-publish + --dry-run`. The Python entrypoint inventory still reports 9 Python + entrypoints; `check_release_metadata.py` is now 1,822 lines and 94,537 bytes. - 2026-06-27: Removed direct full release-graph reads from `check_artifact_targets.py`. `release_graph_query.mjs` now exposes `legacy-central-artifact-targets`, which validates and returns the deprecated diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 443a7d66..14acd2da 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -198,10 +198,6 @@ def validate_platform_npm_packages( validate_publish_executable_files(package, executable_files, target.npm_package) -def load_graph() -> dict: - return product_metadata.load_graph() - - def stable_version(version: str, product: str) -> None: if not re.fullmatch(r"[0-9]+[.][0-9]+[.][0-9]+", version): fail(f"{product} must use a stable x.y.z release version, got {version!r}") @@ -234,16 +230,16 @@ def gradle_property(path: str, name: str) -> str: fail(f"{path} must declare {name}") -def validate_graph_files(graph: dict) -> None: - products = product_metadata.graph_products(graph) +def validate_graph_files() -> None: + products = product_metadata.graph_products() for product in products: for path in [ - *product_metadata.version_files(product, graph), - *product_metadata.derived_version_files(product, graph), + *product_metadata.version_files(product), + *product_metadata.derived_version_files(product), ]: if not (ROOT / path).is_file(): fail(f"{product} release metadata path does not exist: {path}") - product_metadata.validate_all_extension_metadata(graph) + product_metadata.validate_all_extension_metadata() product_metadata_source = read_text("tools/release/product_metadata.py") release_graph_query = read_text("tools/release/release_graph_query.mjs") release_graph_source = read_text("tools/release/release-graph.mjs") @@ -254,6 +250,7 @@ def validate_graph_files(graph: dict) -> None: check_artifact_targets = read_text("tools/release/check_artifact_targets.py") check_consumer_shape = read_text("tools/release/check_consumer_shape.py") release_policy = read_text("tools/policy/check-release-policy.py") + check_release_metadata_source = read_text("tools/release/check_release_metadata.py") if ( "_release_metadata(product).get(\"compatibility_versions\"" in product_metadata_source or "_compatibility_version_entries(" in product_metadata_source @@ -324,9 +321,11 @@ def validate_graph_files(graph: dict) -> None: '"legacy-central-artifact-targets"' not in product_metadata_source or "legacy-central-artifact-targets" not in release_graph_query or "product_metadata.legacy_central_artifact_target_rows()" not in check_artifact_targets - or "product_metadata.load_graph()" in check_artifact_targets + or ("product_metadata." + "load_graph()") in check_artifact_targets + or ("def " + "load_graph()") in check_release_metadata_source + or ("product_metadata." + "load_graph()") in check_release_metadata_source ): - fail("artifact target checks must use graph-query adapters instead of direct product_metadata.load_graph() calls") + fail("artifact target checks must use graph-query adapters instead of direct full graph calls") if ( "typescript_optional_runtime_package_products(" in product_metadata_source or "typescript-broker" in product_metadata_source @@ -398,9 +397,9 @@ def validate_graph_files(graph: dict) -> None: fail("local-registry publish artifact preset must come from the shared Bun release graph query") -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) +def validate_exact_extension_registry_shape() -> None: + for product in product_metadata.extension_product_ids(): + config = product_metadata.product_config(product) 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)) @@ -442,7 +441,7 @@ def validate_exact_extension_registry_shape(graph: dict) -> None: fail(f"{product} WASIX extension AOT Cargo package name is wrong: {package}") -def validate_publish_target_coverage(graph: dict) -> None: +def validate_publish_target_coverage() -> 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: @@ -452,7 +451,7 @@ def validate_publish_target_coverage(graph: dict) -> None: 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(): + for product, config in product_metadata.graph_products().items(): declared = set(product_metadata.string_list(config, "publish_targets", product)) supported = release.supported_publish_targets(product) if declared != supported: @@ -1784,16 +1783,15 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None def main() -> int: - graph = load_graph() - validate_graph_files(graph) - validate_exact_extension_registry_shape(graph) - validate_publish_target_coverage(graph) + validate_graph_files() + validate_exact_extension_registry_shape() + validate_publish_target_coverage() validate_release_setup_docs() validate_local_registry_publisher() versions = { product: product_metadata.read_current_version(product) - for product in product_metadata.product_ids(graph) + for product in product_metadata.product_ids() } for product, version in versions.items(): stable_version(version, product) From 3924c740120ccc8d74ccb3d5f7ff093e00ce742f Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 15:28:52 +0000 Subject: [PATCH 194/308] chore: drop stale python release packager requirements --- .../internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 16 ++++++++++++++++ tools/policy/check-tooling-stack.sh | 9 +++++++++ .../release/package-liboliphaunt-linux-assets.sh | 1 - .../package-liboliphaunt-mobile-assets.sh | 1 - 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index c41efcc4..87557f4c 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,22 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Removed stale Python command requirements from + `package-liboliphaunt-linux-assets.sh` and + `package-liboliphaunt-mobile-assets.sh`; these release asset packagers now + declare only the commands they still use after product versioning, native + stripping, optimization, and archive creation moved to Bun helpers. The + tooling-stack policy now rejects reintroducing that stale Python requirement + in those Bun-backed packagers. Fresh checks passed: `bash -n + tools/release/package-liboliphaunt-linux-assets.sh + tools/release/package-liboliphaunt-mobile-assets.sh + tools/policy/check-tooling-stack.sh`, broad `git grep` for the stale + requirement string, `bash tools/policy/check-tooling-stack.sh`, + `tools/dev/bun.sh tools/policy/check-python-entrypoints.mjs --json`, + `python3 tools/release/check_consumer_shape.py`, `python3 + tools/release/check_release_metadata.py`, `bash + tools/policy/check-policy-tools.sh`, and `git diff --check`. The Python + entrypoint inventory still reports 9 Python entrypoints. - 2026-06-27: Removed the direct full release-graph handoff from `check_release_metadata.py`. The validator no longer defines a local `load_graph()` wrapper, no longer passes a graph object into product config, diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 9b07ce4a..88dfb267 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -328,6 +328,15 @@ if git grep -n 'product_metadata\.py version' -- \ fail "release asset version-only reads must use the Bun helper" fi rm -f /tmp/oliphaunt-product-version-python-grep.$$ +for bun_only_release_asset_packager in \ + tools/release/package-liboliphaunt-linux-assets.sh \ + tools/release/package-liboliphaunt-mobile-assets.sh +do + python_required_pattern='require python''3' + if grep -Fq "$python_required_pattern" "$bun_only_release_asset_packager"; then + fail "$bun_only_release_asset_packager must not require Python after release packaging moved to Bun helpers" + fi +done for broker_cargo_caller in \ tools/release/release.py \ tools/release/local_registry_publish.py \ diff --git a/tools/release/package-liboliphaunt-linux-assets.sh b/tools/release/package-liboliphaunt-linux-assets.sh index 1c53a1f2..b2756bc4 100755 --- a/tools/release/package-liboliphaunt-linux-assets.sh +++ b/tools/release/package-liboliphaunt-linux-assets.sh @@ -38,7 +38,6 @@ esac require cargo require bun -require python3 version="$(tools/dev/bun.sh tools/release/product-version.mjs version liboliphaunt-native)" out_dir="${OLIPHAUNT_LIBOLIPHAUNT_RELEASE_ASSETS:-$root/target/liboliphaunt/release-assets}" diff --git a/tools/release/package-liboliphaunt-mobile-assets.sh b/tools/release/package-liboliphaunt-mobile-assets.sh index 1e75220e..82481e53 100755 --- a/tools/release/package-liboliphaunt-mobile-assets.sh +++ b/tools/release/package-liboliphaunt-mobile-assets.sh @@ -20,7 +20,6 @@ source "$root/tools/release/liboliphaunt-extension-guard.sh" require cargo require bun -require python3 require rsync target_id="${1:-}" From 0c494389f512f2cf1b47b24c39160d9ebbb68ee6 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 15:39:56 +0000 Subject: [PATCH 195/308] chore: use bun release planner in ci --- .github/actions/collect-ci-summary/action.yml | 2 +- .github/scripts/check-release-intent.sh | 2 +- .github/workflows/release.yml | 2 +- .../final-product-source-architecture.md | 3 ++- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 24 +++++++++++++++++++ docs/maintainers/release-setup.md | 9 +++---- docs/maintainers/release.md | 4 ++-- .../policy/assertions/assert-ci-workflows.mjs | 8 +++++++ tools/policy/check-release-policy.py | 3 +++ 9 files changed, 47 insertions(+), 10 deletions(-) diff --git a/.github/actions/collect-ci-summary/action.yml b/.github/actions/collect-ci-summary/action.yml index b363529f..4586fd34 100644 --- a/.github/actions/collect-ci-summary/action.yml +++ b/.github/actions/collect-ci-summary/action.yml @@ -12,5 +12,5 @@ runs: echo echo "- Moon projects: \`moon query projects\`" echo "- Moon tasks: \`moon query tasks\`" - echo "- Release plan: \`tools/release/release.py plan --from-product-tags --head-ref \`" + echo "- Release plan: \`tools/dev/bun.sh tools/release/release_plan.mjs --from-product-tags --head-ref \`" } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/scripts/check-release-intent.sh b/.github/scripts/check-release-intent.sh index 8556ed7d..b94204a5 100755 --- a/.github/scripts/check-release-intent.sh +++ b/.github/scripts/check-release-intent.sh @@ -149,7 +149,7 @@ EOF exit 1 fi -release_plan="$(tools/release/release.py plan --base-ref "${base_ref}" --head-ref "${head_ref}" --format json)" +release_plan="$(tools/dev/bun.sh tools/release/release_plan.mjs --base-ref "${base_ref}" --head-ref "${head_ref}" --format json)" release_products="$( bun -e 'const data = JSON.parse(await Bun.stdin.text()); console.log((data.releaseProducts ?? []).join("\n"));' <<< "${release_plan}" )" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 09b0aaa5..0e5b1403 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -258,7 +258,7 @@ jobs: - name: Plan product releases id: release_plan run: | - tools/release/release.py plan --from-product-tags --include-current-tags --head-ref "$RELEASE_HEAD_SHA" --format github-output >> "$GITHUB_OUTPUT" + tools/dev/bun.sh tools/release/release_plan.mjs --from-product-tags --include-current-tags --head-ref "$RELEASE_HEAD_SHA" --format github-output >> "$GITHUB_OUTPUT" - name: No package release planned if: ${{ steps.release_plan.outputs.has_release_changes != 'true' }} diff --git a/docs/architecture/final-product-source-architecture.md b/docs/architecture/final-product-source-architecture.md index 0a068988..43c3ebcc 100644 --- a/docs/architecture/final-product-source-architecture.md +++ b/docs/architecture/final-product-source-architecture.md @@ -165,7 +165,8 @@ scopes: 1. release-please identifies product components, versions, changelogs, and tag prefixes. 2. Product-local `release.toml` adds publish and artifact metadata. -3. `tools/release/release.py plan` maps changed paths to owning Moon projects. +3. `tools/dev/bun.sh tools/release/release_plan.mjs` maps changed paths to + owning Moon projects. 4. The release closure follows only Moon `production` and `peer` dependencies. 5. CI affectedness still follows all Moon dependencies, including `build`. diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 87557f4c..4c206d7f 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,30 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Switched active release-planning callers from the Python + `release.py plan` compatibility wrapper to the Bun + `tools/release/release_plan.mjs` entrypoint. The release workflow, + release-intent checker, CI summary action, maintainer release docs, and + architecture release-model docs now point at the Bun planner, while + `release.py plan` remains as a compatibility shim that delegates to the same + script. `assert-ci-workflows.mjs` now rejects the Python planner wrapper in + active workflow surfaces and requires the Bun planner command. Fresh checks + passed: `bash -n .github/scripts/check-release-intent.sh`, `python3 -m + py_compile tools/policy/check-release-policy.py`, `tools/dev/bun.sh + tools/release/release_plan.mjs --format json`, `tools/release/release.py plan + --format json`, direct JSON parity diff between those two planners, + `tools/dev/bun.sh tools/policy/assertions/assert-ci-workflows.mjs`, `python3 + tools/policy/check-release-policy.py`, `python3 + tools/release/check_release_metadata.py`, `bash tools/policy/check-docs.sh`, + `bash tools/policy/check-policy-tools.sh`, `bash + tools/policy/check-workflows.sh`, `tools/release/release.py check`, `bash + tools/policy/check-tooling-stack.sh`, `tools/dev/bun.sh + tools/policy/check-python-entrypoints.mjs --json`, active-surface grep for + the Python planner wrapper, and `git diff --check`. The Python entrypoint + inventory still reports 9 Python entrypoints; `check-release-policy.py` is + now 1,534 lines and 65,303 bytes. A subagent review was attempted for this + slice, but the current session remained at the agent thread limit, so this + pass used local repository evidence. - 2026-06-27: Removed stale Python command requirements from `package-liboliphaunt-linux-assets.sh` and `package-liboliphaunt-mobile-assets.sh`; these release asset packagers now diff --git a/docs/maintainers/release-setup.md b/docs/maintainers/release-setup.md index 6e5a6116..0a989ced 100644 --- a/docs/maintainers/release-setup.md +++ b/docs/maintainers/release-setup.md @@ -82,7 +82,7 @@ Useful verification: ```bash gh repo view f0rr0/oliphaunt gh workflow list --repo f0rr0/oliphaunt -tools/release/release.py plan --from-product-tags --include-current-tags --head-ref HEAD +tools/dev/bun.sh tools/release/release_plan.mjs --from-product-tags --include-current-tags --head-ref HEAD tools/release/release.py check ``` @@ -385,7 +385,7 @@ registry state: ```bash moon run dev-tools:doctor tools/release/release.py check -tools/release/release.py plan --from-product-tags --include-current-tags --head-ref HEAD +tools/dev/bun.sh tools/release/release_plan.mjs --from-product-tags --include-current-tags --head-ref HEAD tools/release/release.py check-registries --products-json '' --head-ref HEAD tools/release/release.py publish-dry-run --products-json '' --head-ref HEAD tools/release/release.py consumer-shape --require-ready --format markdown @@ -393,8 +393,9 @@ tools/release/release.py consumer-shape --require-ready --format markdown For the first public release, select every product that introduces a public dependency edge in one release plan. Treat the output of -`tools/release/release.py plan --from-product-tags --include-current-tags ---head-ref HEAD` as the source of truth; the core dependency lane is: +`tools/dev/bun.sh tools/release/release_plan.mjs --from-product-tags +--include-current-tags --head-ref HEAD` as the source of truth; the core +dependency lane is: ```json [ diff --git a/docs/maintainers/release.md b/docs/maintainers/release.md index f8252587..f7095aae 100644 --- a/docs/maintainers/release.md +++ b/docs/maintainers/release.md @@ -30,7 +30,7 @@ and product-scoped tags. Product-local `release.toml` files declare owner, kind, publish targets, registry packages, release artifacts, and compatibility-version files. Moon owns dependency scopes and path ownership. -`tools/release/release.py plan` computes release impact as: +`tools/dev/bun.sh tools/release/release_plan.mjs` computes release impact as: 1. map changed files to owning Moon projects; 2. follow Moon dependencies with `production` or `peer` scope; @@ -57,7 +57,7 @@ versions/tags. Use these commands while preparing or checking releases: ```sh -tools/release/release.py plan +tools/dev/bun.sh tools/release/release_plan.mjs tools/release/release.py check tools/release/release.py check-registries tools/release/release.py publish-dry-run diff --git a/tools/policy/assertions/assert-ci-workflows.mjs b/tools/policy/assertions/assert-ci-workflows.mjs index 7b946dab..834de253 100755 --- a/tools/policy/assertions/assert-ci-workflows.mjs +++ b/tools/policy/assertions/assert-ci-workflows.mjs @@ -135,6 +135,8 @@ function plannedBuildJobs(ciText) { const ciPath = '.github/workflows/ci.yml'; const mobilePath = '.github/workflows/mobile-e2e.yml'; const releasePath = '.github/workflows/release.yml'; +const releaseIntentPath = '.github/scripts/check-release-intent.sh'; +const ciSummaryActionPath = '.github/actions/collect-ci-summary/action.yml'; const wasixDownloadPath = '.github/scripts/download-wasix-runtime-build-artifacts.sh'; const ci = read(ciPath); @@ -318,6 +320,12 @@ assertCheckoutRef(mobileBlocks, 'ios', mobileArtifactRef); rejectText(releasePath, 'require-workflow-success.sh Builds'); rejectText(releasePath, 'artifact-builders'); rejectText(releasePath, 'BUILDS_RUN_ID'); +rejectText(releasePath, 'tools/release/release.py plan'); +requireText(releasePath, 'tools/dev/bun.sh tools/release/release_plan.mjs --from-product-tags --include-current-tags --head-ref "$RELEASE_HEAD_SHA" --format github-output'); +rejectText(releaseIntentPath, 'tools/release/release.py plan'); +requireText(releaseIntentPath, 'tools/dev/bun.sh tools/release/release_plan.mjs --base-ref "${base_ref}" --head-ref "${head_ref}" --format json'); +rejectText(ciSummaryActionPath, 'tools/release/release.py plan'); +requireText(ciSummaryActionPath, 'tools/dev/bun.sh tools/release/release_plan.mjs --from-product-tags --head-ref '); requireText(releasePath, 'Require release-commit CI build gate'); requireText(releasePath, 'id: ci_build_gate'); requireText(releasePath, 'require-workflow-success.sh CI "$RELEASE_HEAD_SHA" 7200 --job Builds'); diff --git a/tools/policy/check-release-policy.py b/tools/policy/check-release-policy.py index 91212f73..088f942f 100644 --- a/tools/policy/check-release-policy.py +++ b/tools/policy/check-release-policy.py @@ -876,9 +876,12 @@ def check_release_workflow_policy() -> None: "Create release-please target branch", "target-branch: ${{ steps.release_head.outputs.target_branch }}", "Remove release-please target branch", + 'tools/dev/bun.sh tools/release/release_plan.mjs --from-product-tags --include-current-tags --head-ref "$RELEASE_HEAD_SHA" --format github-output', ): if snippet not in release_workflow: fail(f"Release workflow must resolve and publish from an explicit release commit: missing {snippet!r}") + if "tools/release/release.py plan" in release_workflow: + fail("Release workflow must call the Bun release plan entrypoint directly") assert_text_order( publish_block, From 7e6d7b44eee06b03ac12856a4ab494cbd26fe67c Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 15:53:03 +0000 Subject: [PATCH 196/308] chore: query release ci artifacts with bun --- .github/workflows/release.yml | 8 +- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 30 +++++ .../examples-ci-release-validation.md | 5 +- .../policy/assertions/assert-ci-workflows.mjs | 6 + tools/policy/check-release-policy.py | 14 +- tools/release/check_artifact_targets.py | 8 +- tools/release/check_release_metadata.py | 3 +- tools/release/release_graph_query.mjs | 127 +++++++++++++++++- 8 files changed, 179 insertions(+), 22 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0e5b1403..a2831361 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -364,7 +364,7 @@ jobs: 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) + done < <(tools/dev/bun.sh tools/release/release_graph_query.mjs ci-artifact-names --product "$product" --family sdk-package --format lines) .github/scripts/download-build-artifacts.sh \ CI \ "$RELEASE_HEAD_SHA" \ @@ -375,7 +375,7 @@ jobs: } while IFS= read -r product; do download_sdk_artifact "$product" - done < <(tools/release/release.py ci-products --family sdk-package --products-json "$PRODUCTS_JSON") + done < <(tools/dev/bun.sh tools/release/release_graph_query.mjs ci-products --family sdk-package --products-json "$PRODUCTS_JSON" --format lines) - name: Download liboliphaunt release assets if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && steps.release_plan.outputs.product_liboliphaunt_native == 'true' }} @@ -424,7 +424,7 @@ jobs: 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) + done < <(tools/dev/bun.sh tools/release/release_graph_query.mjs ci-artifact-names --product "$product" --kind "$kind" --family release-assets --format lines) .github/scripts/download-build-artifacts.sh \ CI \ "$RELEASE_HEAD_SHA" \ @@ -456,7 +456,7 @@ jobs: 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) + done < <(tools/dev/bun.sh tools/release/release_graph_query.mjs ci-artifact-names --product oliphaunt-node-direct --kind node-direct-addon --family npm-package --format lines) .github/scripts/download-build-artifacts.sh \ CI \ "$RELEASE_HEAD_SHA" \ diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 4c206d7f..a09cde5b 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,36 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Switched release workflow CI artifact handoffs from the Python + `release.py ci-products` and `release.py ci-artifacts` compatibility + commands to direct Bun release graph queries. `release_graph_query.mjs` now + exposes `ci-products --family sdk-package --format lines` for selected SDK + release products and supports `ci-artifact-names --family sdk-package + --format lines` alongside the existing release-asset and npm-package artifact + families. The release workflow now downloads SDK, native helper, and Node + direct optional npm artifacts through those Bun queries; workflow assertions + and release policy reject reintroducing the Python CI artifact handoff in the + active release workflow. Fresh checks passed: Bun/Python parity diffs for + selected SDK products, SDK package artifacts, broker release assets, and Node + direct npm package artifacts; `tools/dev/bun.sh + tools/release/release_graph_query.mjs ci-products --family sdk-package + --format json`; `python3 -m py_compile tools/release/check_artifact_targets.py + tools/release/check_release_metadata.py tools/policy/check-release-policy.py`; + active-surface grep proving no `release.py ci-*` calls remain outside + historical notes; `tools/dev/bun.sh + tools/policy/assertions/assert-ci-workflows.mjs`; `python3 + tools/release/check_artifact_targets.py`; `python3 + tools/release/check_release_metadata.py`; `python3 + tools/policy/check-release-policy.py`; `bash tools/policy/check-workflows.sh`; + `bash tools/policy/check-policy-tools.sh`; `tools/release/release.py check`; + `tools/dev/bun.sh tools/policy/check-python-entrypoints.mjs --json`; `bash + tools/policy/check-tooling-stack.sh`; and `bash tools/policy/check-docs.sh`. + The Python entrypoint inventory still reports 9 Python entrypoints; + `check-release-policy.py` is now 1,540 lines and 65,797 bytes, + `check_artifact_targets.py` is 1,437 lines and 72,427 bytes, and + `check_release_metadata.py` is 1,823 lines and 94,610 bytes. A subagent + review was attempted for this slice, but the current session remained at the + agent thread limit, so this pass used local repository evidence. - 2026-06-27: Switched active release-planning callers from the Python `release.py plan` compatibility wrapper to the Bun `tools/release/release_plan.mjs` entrypoint. The release workflow, diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index 68637d17..52ce9f7d 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -268,8 +268,9 @@ the release/tooling surface after the runtime tool crate split. 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. + `tools/release/release_graph_query.mjs ci-artifact-names --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/tools/policy/assertions/assert-ci-workflows.mjs b/tools/policy/assertions/assert-ci-workflows.mjs index 834de253..23a2627f 100755 --- a/tools/policy/assertions/assert-ci-workflows.mjs +++ b/tools/policy/assertions/assert-ci-workflows.mjs @@ -321,7 +321,13 @@ rejectText(releasePath, 'require-workflow-success.sh Builds'); rejectText(releasePath, 'artifact-builders'); rejectText(releasePath, 'BUILDS_RUN_ID'); rejectText(releasePath, 'tools/release/release.py plan'); +rejectText(releasePath, 'tools/release/release.py ci-' + 'products'); +rejectText(releasePath, 'tools/release/release.py ci-' + 'artifacts'); requireText(releasePath, 'tools/dev/bun.sh tools/release/release_plan.mjs --from-product-tags --include-current-tags --head-ref "$RELEASE_HEAD_SHA" --format github-output'); +requireText(releasePath, 'tools/dev/bun.sh tools/release/release_graph_query.mjs ci-products --family sdk-package --products-json "$PRODUCTS_JSON" --format lines'); +requireText(releasePath, 'tools/dev/bun.sh tools/release/release_graph_query.mjs ci-artifact-names --product "$product" --family sdk-package --format lines'); +requireText(releasePath, 'tools/dev/bun.sh tools/release/release_graph_query.mjs ci-artifact-names --product "$product" --kind "$kind" --family release-assets --format lines'); +requireText(releasePath, 'tools/dev/bun.sh tools/release/release_graph_query.mjs ci-artifact-names --product oliphaunt-node-direct --kind node-direct-addon --family npm-package --format lines'); rejectText(releaseIntentPath, 'tools/release/release.py plan'); requireText(releaseIntentPath, 'tools/dev/bun.sh tools/release/release_plan.mjs --base-ref "${base_ref}" --head-ref "${head_ref}" --format json'); rejectText(ciSummaryActionPath, 'tools/release/release.py plan'); diff --git a/tools/policy/check-release-policy.py b/tools/policy/check-release-policy.py index 088f942f..47e04336 100644 --- a/tools/policy/check-release-policy.py +++ b/tools/policy/check-release-policy.py @@ -882,6 +882,12 @@ def check_release_workflow_policy() -> None: fail(f"Release workflow must resolve and publish from an explicit release commit: missing {snippet!r}") if "tools/release/release.py plan" in release_workflow: fail("Release workflow must call the Bun release plan entrypoint directly") + for legacy_release_query in ( + "tools/release/release.py ci-" + "products", + "tools/release/release.py ci-" + "artifacts", + ): + if legacy_release_query in release_workflow: + fail("Release workflow must call Bun release graph queries for CI artifact handoffs") assert_text_order( publish_block, @@ -918,10 +924,10 @@ def check_release_workflow_policy() -> None: "--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", + "tools/dev/bun.sh tools/release/release_graph_query.mjs ci-products --family sdk-package --products-json \"$PRODUCTS_JSON\" --format lines", + "tools/dev/bun.sh tools/release/release_graph_query.mjs ci-artifact-names --product \"$product\" --family sdk-package --format lines", + "tools/dev/bun.sh tools/release/release_graph_query.mjs ci-artifact-names --product \"$product\" --kind \"$kind\" --family release-assets --format lines", + "tools/dev/bun.sh tools/release/release_graph_query.mjs ci-artifact-names --product oliphaunt-node-direct --kind node-direct-addon --family npm-package --format lines", "pnpm install --frozen-lockfile", "target/oliphaunt-broker/release-assets", "target/oliphaunt-node-direct/release-assets", diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py index cc0e51c7..77300b76 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -428,12 +428,12 @@ def validate_ci_release_artifacts() -> None: 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', + 'tools/dev/bun.sh tools/release/release_graph_query.mjs ci-artifact-names --product "$product" --family sdk-package --format lines', "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"', + 'tools/dev/bun.sh tools/release/release_graph_query.mjs ci-products --family sdk-package --products-json "$PRODUCTS_JSON" --format lines', "release workflow must derive selected SDK package products from release metadata", ) for legacy_env in ( @@ -843,7 +843,7 @@ def validate_ci_release_artifacts() -> None: ) require_text( ".github/workflows/release.yml", - "tools/release/release.py ci-artifacts --product \"$product\" --kind \"$kind\" --family release-assets", + "tools/dev/bun.sh tools/release/release_graph_query.mjs ci-artifact-names --product \"$product\" --kind \"$kind\" --family release-assets --format lines", "release workflow must derive native helper release artifact names from target metadata", ) require_text( @@ -858,7 +858,7 @@ def validate_ci_release_artifacts() -> None: ) require_text( ".github/workflows/release.yml", - "tools/release/release.py ci-artifacts --product oliphaunt-node-direct --kind node-direct-addon --family npm-package", + "tools/dev/bun.sh tools/release/release_graph_query.mjs ci-artifact-names --product oliphaunt-node-direct --kind node-direct-addon --family npm-package --format lines", "release workflow must derive Node direct npm package artifact names from target metadata", ) require_text( diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 14acd2da..e1c02bf6 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -343,6 +343,7 @@ def validate_graph_files() -> None: or "oliphaunt-wasix-rust" in product_metadata_source or "export function sdkPackageProducts(" not in release_artifact_targets or "sdk-package-products [--product PRODUCT]" not in release_graph_query + or "ci-products --family sdk-package" not in release_graph_query or "sdkPackageProducts(TOOL)" not in release_graph_query ): fail("SDK package product and CI artifact-name selection must come from the shared Bun release graph query") @@ -352,7 +353,7 @@ def validate_graph_files() -> None: or "f\"{product}-npm-package-{target.target}\"" in product_metadata_source or "export function ciReleaseAssetArtifactRows(" not in release_artifact_targets or "export function ciNpmPackageArtifactRows(" not in release_artifact_targets - or "ci-artifact-names --family release-assets|npm-package --product PRODUCT --kind KIND" not in release_graph_query + or "ci-artifact-names --family release-assets|npm-package|sdk-package --product PRODUCT" not in release_graph_query or "ciReleaseAssetArtifactRows(product, kind, TOOL)" not in release_graph_query or "ciNpmPackageArtifactRows(product, kind, TOOL)" not in release_graph_query ): diff --git a/tools/release/release_graph_query.mjs b/tools/release/release_graph_query.mjs index 5d440cc3..e6241b1b 100644 --- a/tools/release/release_graph_query.mjs +++ b/tools/release/release_graph_query.mjs @@ -58,6 +58,19 @@ function printJson(value) { console.log(JSON.stringify(sortedValue(value), null, 2)); } +function printLines(values) { + for (const value of values) { + console.log(value); + } +} + +function validateFormat(format, command) { + if (!["json", "lines"].includes(format)) { + fail(`${command} --format must be json or lines`); + } + return format; +} + function parseJsonFlag(argv, name, { required = false } = {}) { const raw = stringFlag(argv, name, { required }); if (raw === undefined) { @@ -475,6 +488,7 @@ function runCiArtifactNames(argv) { let family; let product; let kind; + let format = "json"; for (let index = 0; index < argv.length; index += 1) { const value = argv[index]; if (value === "--family") { @@ -501,6 +515,14 @@ function runCiArtifactNames(argv) { index += 1; } else if (value.startsWith("--kind=")) { kind = value.slice("--kind=".length); + } else if (value === "--format") { + if (index + 1 >= argv.length) { + fail("--format requires a value"); + } + format = validateFormat(argv[index + 1], "ci-artifact-names"); + index += 1; + } else if (value.startsWith("--format=")) { + format = validateFormat(value.slice("--format=".length), "ci-artifact-names"); } else { fail(`unknown argument ${value}`); } @@ -511,15 +533,103 @@ function runCiArtifactNames(argv) { if (product === undefined) { fail("--product is required"); } - if (kind === undefined) { - fail("--kind is required"); - } + let rows; if (family === "release-assets") { - printJson(ciReleaseAssetArtifactRows(product, kind, TOOL)); + if (kind === undefined) { + fail("--kind is required for release-assets artifacts"); + } + rows = ciReleaseAssetArtifactRows(product, kind, TOOL); } else if (family === "npm-package") { - printJson(ciNpmPackageArtifactRows(product, kind, TOOL)); + if (kind === undefined) { + fail("--kind is required for npm-package artifacts"); + } + rows = ciNpmPackageArtifactRows(product, kind, TOOL); + } else if (family === "sdk-package") { + if (kind !== undefined) { + fail("--kind is not accepted for sdk-package artifacts"); + } + rows = sdkPackageProducts(TOOL) + .filter((row) => row.product === product) + .map((row) => ({ + family: "sdk-package", + product: row.product, + artifactName: row.artifactName, + })); + if (rows.length !== 1) { + fail(`${product} is not an SDK release product`); + } + } else { + fail("--family must be release-assets, npm-package, or sdk-package"); + } + if (format === "lines") { + printLines(rows.map((row) => row.artifactName)); + } else { + printJson(rows); + } +} + +function runCiProducts(argv) { + let family; + let productsJson; + let format = "json"; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === "--family") { + if (index + 1 >= argv.length) { + fail("--family requires a value"); + } + family = argv[index + 1]; + index += 1; + } else if (value.startsWith("--family=")) { + family = value.slice("--family=".length); + } else if (value === "--products-json") { + if (index + 1 >= argv.length) { + fail("--products-json requires a value"); + } + productsJson = argv[index + 1]; + index += 1; + } else if (value.startsWith("--products-json=")) { + productsJson = value.slice("--products-json=".length); + } else if (value === "--format") { + if (index + 1 >= argv.length) { + fail("--format requires a value"); + } + format = validateFormat(argv[index + 1], "ci-products"); + index += 1; + } else if (value.startsWith("--format=")) { + format = validateFormat(value.slice("--format=".length), "ci-products"); + } else { + fail(`unknown argument ${value}`); + } + } + if (family !== "sdk-package") { + fail("--family must be sdk-package"); + } + const sdkRows = sdkPackageProducts(TOOL); + const rowsByProduct = new Map(sdkRows.map((row) => [row.product, row])); + let products = sdkRows.map((row) => row.product); + if (productsJson !== undefined) { + let selected; + try { + selected = JSON.parse(productsJson); + } catch (error) { + fail(`--products-json must be valid JSON: ${error.message}`); + } + assertStringList(selected, "--products-json"); + const graph = loadGraph(TOOL); + 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(", ")}`); + } + products = releaseOrder(graph.products, graph.moon_projects, selected, TOOL) + .filter((product) => rowsByProduct.has(product)); + } + const rows = products.map((product) => rowsByProduct.get(product)); + if (format === "lines") { + printLines(rows.map((row) => row.product)); } else { - fail("--family must be release-assets or npm-package"); + printJson(rows); } } @@ -673,7 +783,8 @@ Commands: product-versions [--product PRODUCT] typescript-optional-runtime-package-versions sdk-package-products [--product PRODUCT] - ci-artifact-names --family release-assets|npm-package --product PRODUCT --kind KIND + ci-products --family sdk-package [--products-json JSON] [--format json|lines] + ci-artifact-names --family release-assets|npm-package|sdk-package --product PRODUCT [--kind KIND] [--format json|lines] local-publish-artifacts [--aggregate-only] expected-assets --product PRODUCT --version VERSION [--surface SURFACE] [--kind KIND...] [--include-unpublished] registry-packages --product PRODUCT [--kind KIND] @@ -717,6 +828,8 @@ function main(argv) { runTypescriptOptionalRuntimePackageVersions(rest); } else if (command === "sdk-package-products") { runSdkPackageProducts(rest); + } else if (command === "ci-products") { + runCiProducts(rest); } else if (command === "ci-artifact-names") { runCiArtifactNames(rest); } else if (command === "local-publish-artifacts") { From 45b9ff128221567ce48b20765c5399ead135badc Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 16:02:12 +0000 Subject: [PATCH 197/308] chore: use bun planner for release pr coverage --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 27 +++++++++++++++++-- tools/release/check_release_metadata.py | 7 +++++ tools/release/check_release_pr_coverage.mjs | 4 +-- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index a09cde5b..c2142376 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,29 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Switched `check_release_pr_coverage.mjs` from the Python + `release.py plan` compatibility wrapper to the Bun + `tools/release/release_plan.mjs` entrypoint. The release PR coverage checker + remains a Bun checker end to end: it now reads release-please manifest diffs + and Moon-selected release products from the same canonical Bun planner used + by the release workflow and release-intent check, while `release.py plan` + remains only a compatibility shim. `check_release_metadata.py` now rejects + reintroducing the Python planner wrapper in the release PR coverage checker. + Fresh checks passed: `tools/dev/bun.sh + tools/release/check_release_pr_coverage.mjs`, direct parity diff between + `tools/dev/bun.sh tools/release/release_plan.mjs --base-ref origin/main + --head-ref HEAD --format json` and `tools/release/release.py plan --base-ref + origin/main --head-ref HEAD --format json`, active-file grep proving + `check_release_pr_coverage.mjs` no longer calls `release.py`, `python3 -m + py_compile tools/release/check_release_metadata.py`, `python3 + tools/release/check_release_metadata.py`, `tools/release/release.py check`, + `bash tools/policy/check-policy-tools.sh`, `bash + tools/policy/check-tooling-stack.sh`, and `tools/dev/bun.sh + tools/policy/check-python-entrypoints.mjs --json`. The Python entrypoint + inventory still reports 9 Python entrypoints; `check_release_metadata.py` is + now 1,830 lines and 95,010 bytes. A subagent review was attempted for this + slice, but the current session remained at the agent thread limit, so this + pass used local repository evidence. - 2026-06-27: Switched release workflow CI artifact handoffs from the Python `release.py ci-products` and `release.py ci-artifacts` compatibility commands to direct Bun release graph queries. `release_graph_query.mjs` now @@ -1929,8 +1952,8 @@ until the current-state gates here are checked with fresh local evidence. 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. + diffs tied to `tools/release/release_plan.mjs --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` diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index e1c02bf6..6d9b76ce 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -245,6 +245,7 @@ def validate_graph_files() -> None: release_graph_source = read_text("tools/release/release-graph.mjs") release_artifact_targets = read_text("tools/release/release-artifact-targets.mjs") sync_release_pr = read_text("tools/release/sync-release-pr.mjs") + release_pr_coverage = read_text("tools/release/check_release_pr_coverage.mjs") build_extension_ci_artifacts = read_text("tools/release/build-extension-ci-artifacts.mjs") check_staged_artifacts = read_text("tools/release/check-staged-artifacts.mjs") check_artifact_targets = read_text("tools/release/check_artifact_targets.py") @@ -326,6 +327,12 @@ def validate_graph_files() -> None: or ("product_metadata." + "load_graph()") in check_release_metadata_source ): fail("artifact target checks must use graph-query adapters instead of direct full graph calls") + if ( + "tools/release/release_plan.mjs" not in release_pr_coverage + or "tools/release/release.py', [\n 'plan'" in release_pr_coverage + or 'tools/release/release.py", [\n "plan"' in release_pr_coverage + ): + fail("release PR coverage must call the Bun release planner directly") if ( "typescript_optional_runtime_package_products(" in product_metadata_source or "typescript-broker" in product_metadata_source diff --git a/tools/release/check_release_pr_coverage.mjs b/tools/release/check_release_pr_coverage.mjs index 2a4699fc..a0fe13b4 100644 --- a/tools/release/check_release_pr_coverage.mjs +++ b/tools/release/check_release_pr_coverage.mjs @@ -114,8 +114,8 @@ function releasePleaseProductPaths() { } function releasePlan(ref) { - const result = run('tools/release/release.py', [ - 'plan', + const result = run('tools/dev/bun.sh', [ + 'tools/release/release_plan.mjs', '--base-ref', ref, '--head-ref', From b995c2c501b39df7e76667f1a9011aa5e3e74ce0 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 16:24:37 +0000 Subject: [PATCH 198/308] docs: record release validation batch --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index c2142376..e7d72309 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -73,11 +73,34 @@ until the current-state gates here are checked with fresh local evidence. - [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 +- [x] Re-run Linux CI-like and release/local-registry lanes after each tooling migration batch. ### Current Fresh Evidence +- 2026-06-27: Re-ran the Linux-local release/local-registry validation batch + after the latest tooling migrations. Fresh checks passed: + `tools/release/local_registry_publish.py publish --surface cargo --strict`, + `tools/release/local_registry_publish.py publish --surface npm --strict`, + `tools/release/local_registry_publish.py publish --surface maven --strict`, + `tools/release/local_registry_publish.py publish --surface swift --strict`, + `tools/release/release.py check`, and + `act workflow_dispatch -W .github/workflows/ci.yml -j release-intent + --dryrun -P ubuntu-latest=ghcr.io/catthehacker/ubuntu:act-latest`. Cargo + strict publish generated/staged 500 local `.crate` files with none over the + 10 MiB crates.io limit; the largest observed local crate was + 10,212,312 bytes. Maven strict publish staged 14 files from + `oliphaunt-kotlin-sdk-package-artifacts/maven` into + `target/local-registries/maven`. Swift strict staging found copyable SwiftPM + artifacts and staged `Oliphaunt-source.zip` plus `OliphauntICU.swift`, while + recording that the Linux host does not have `swift` installed. `release.py + check` passed release policy, release-please config, artifact targets, + release PR derived-file sync, release metadata, and ready consumer-shape + checks across all products. The `act` release-intent dry run selected and + completed the PR-shaped Linux job; current upstream `nektos/act` issue + evidence still shows `actions/upload-artifact@v7` `mime_type` incompatibility, + so artifact-dependent downstream CI jobs remain not fully provable with local + `act` on this host. - 2026-06-27: Switched `check_release_pr_coverage.mjs` from the Python `release.py plan` compatibility wrapper to the Bun `tools/release/release_plan.mjs` entrypoint. The release PR coverage checker From ad65e7c457659358838f412bb0692fc7c7c8bce0 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 16:34:18 +0000 Subject: [PATCH 199/308] docs: record sdk parity validation --- .../examples-ci-release-validation.md | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index 52ce9f7d..399550fe 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -40,7 +40,7 @@ the release/tooling surface after the runtime tool crate split. - WASIX tools-AOT crates - extension runtime/AOT crates - [x] Verify release dry-runs publish the same package families to local registries. -- [ ] Keep release checks DRY: generation, validation, and publication should share one +- [x] Keep release checks DRY: generation, validation, and publication should share one package-family model per ecosystem. - [x] Make extension Maven registry surfaces explicit in generated extension metadata instead of silently appending them during release. @@ -54,13 +54,13 @@ the release/tooling surface after the runtime tool crate split. ## P1: SDK Consistency -- [ ] Compare native runtime/tool/extension/ICU resolution across Rust, JS, React +- [x] 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 +- [x] 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. +- [x] 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. - [x] Align Deno native runtime/tools/extension resolution with Node/Bun, or document @@ -82,6 +82,24 @@ the release/tooling surface after the runtime tool crate split. ## Current Evidence +- On 2026-06-27, the open release DRY and SDK consistency tracker items were + rechecked against current source. Fresh checks passed: + `bash tools/policy/check-sdk-parity.sh`, + `python3 tools/release/check_artifact_targets.py`, + `python3 tools/release/check_release_metadata.py`, + `tools/dev/bun.sh tools/policy/assertions/assert-ci-workflows.mjs`, and + `tools/dev/bun.sh examples/tools/check-examples.mjs`. The SDK parity gate + covers native and WASIX artifact resolution, split native/WASIX tool + semantics, mobile runtime-resource validation, React Native delegation, + TypeScript Node/Bun/Deno runtime cache publication, and shared protocol, + transaction, backup/restore, lifecycle, capability, package-size, and + extension semantics. The release checks derive expected artifacts, workflow + handoffs, local-publish presets, registry package names, and WASIX + runtime/tools/AOT package families from the same release graph and WASIX + artifact contract instead of copied package-family lists. The examples check + keeps root and nested examples on local registries and verifies the native + `pg_dump` plus WASIX `preflight_tools`, `pg_dump`, and noninteractive `psql` + control flows remain represented. - On 2026-06-27, strict npm local-registry publication was rerun against the current split runtime/tools package surface with `tools/release/local_registry_publish.py publish --surface npm --strict`. From 9bf0255687af6b6b73f1719e64415616a44add14 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 16:38:27 +0000 Subject: [PATCH 200/308] fix: dedupe react native ios resource bundles --- docs/maintainers/examples-ci-release-validation.md | 6 ++++++ src/sdks/react-native/ios/OliphauntAdapter.swift | 2 +- tools/policy/check-sdk-parity.sh | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index 399550fe..1bc82f6f 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -323,6 +323,12 @@ the release/tooling surface after the runtime tool crate split. - 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. +- React Native iOS runtime-resource resolution no longer repeats the + `OliphauntResources` bundle candidate in its native-library fallback. The SDK + parity check now requires the published bundle candidate list and rejects the + duplicated fallback list; `bash tools/policy/check-sdk-parity.sh`, `bash + src/sdks/react-native/tools/check-sdk.sh package-shape`, and `git diff + --check` passed locally. - 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/src/sdks/react-native/ios/OliphauntAdapter.swift b/src/sdks/react-native/ios/OliphauntAdapter.swift index b840bd00..0f174510 100644 --- a/src/sdks/react-native/ios/OliphauntAdapter.swift +++ b/src/sdks/react-native/ios/OliphauntAdapter.swift @@ -551,7 +551,7 @@ public final class OliphauntAdapterDatabase: NSObject, @unchecked Sendable { return url } } - for bundleName in ["OliphauntReactNativeResources", "OliphauntResources", "OliphauntResources"] { + for bundleName in ["OliphauntReactNativeResources", "OliphauntResources"] { guard let bundleURL = Bundle.main.url(forResource: bundleName, withExtension: "bundle"), let bundle = Bundle(url: bundleURL), let url = bundledLibraryURL(in: bundle) diff --git a/tools/policy/check-sdk-parity.sh b/tools/policy/check-sdk-parity.sh index 0b681732..2bef9acf 100755 --- a/tools/policy/check-sdk-parity.sh +++ b/tools/policy/check-sdk-parity.sh @@ -1221,6 +1221,10 @@ require_text src/sdks/react-native/ios/OliphauntAdapter.swift "libraryPath must "React Native iOS adapter must reject blank native library overrides before Swift SDK open/restore" require_text src/sdks/react-native/ios/OliphauntAdapter.swift "runtimeDirectory must not be empty" \ "React Native iOS adapter must reject blank runtime-directory overrides before Swift SDK open" +require_text src/sdks/react-native/ios/OliphauntAdapter.swift '["OliphauntReactNativeResources", "OliphauntResources"]' \ + "React Native iOS resource bundle resolution must check each published bundle candidate once" +reject_text src/sdks/react-native/ios/OliphauntAdapter.swift '["OliphauntReactNativeResources", "OliphauntResources", "OliphauntResources"]' \ + "React Native iOS resource bundle resolution must not duplicate fallback bundle candidates" require_text src/sdks/react-native/ios/OliphauntAdapter.swift "return try nonBlankValue(try string(dictionary, key), key, emptyMessage: emptyMessage)" \ "React Native iOS adapter path helper must reject NUL-containing roots and native override paths" reject_text src/sdks/react-native/ios/OliphauntAdapter.swift 'username: string(config, "username")' \ From e9a7067310e5657b14cdd87cab1833264e2238dc Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 16:45:21 +0000 Subject: [PATCH 201/308] test: assert ci artifact families --- .../examples-ci-release-validation.md | 13 +- .../policy/assertions/assert-ci-workflows.mjs | 140 +++++++++++++++++- 2 files changed, 151 insertions(+), 2 deletions(-) diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index 1bc82f6f..a9318ff9 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -30,7 +30,8 @@ the release/tooling surface after the runtime tool crate split. ## P1: CI and Release Shape -- [ ] Verify CI lanes build and upload the artifact families now expected by examples: +- [x] Verify CI/release lanes build, upload, or stage the artifact families now + expected by examples: - native runtime Cargo crates - native tools Cargo crates - broker Cargo crates @@ -100,6 +101,16 @@ the release/tooling surface after the runtime tool crate split. keeps root and nested examples on local registries and verifies the native `pg_dump` plus WASIX `preflight_tools`, `pg_dump`, and noninteractive `psql` control flows remain represented. +- On 2026-06-27, CI/release artifact-family coverage was audited against the + release graph and workflow topology. `tools/policy/assertions/assert-ci-workflows.mjs` + now verifies that native `pg_dump`/`psql` tool assets share the desktop + `liboliphaunt-native-release-assets-${{ matrix.target }}` upload and aggregate + into `liboliphaunt-native-release-assets`; WASIX runtime, tools, ICU, + runtime-AOT, and tools-AOT Cargo packages are exactly the public WASIX Cargo + artifact contract staged by `wasix-rust-package`; and the release workflow + consumes graph-derived SDK, helper, native, WASIX, and extension artifact names. + `tools/dev/bun.sh tools/policy/assertions/assert-ci-workflows.mjs` passed + locally. - On 2026-06-27, strict npm local-registry publication was rerun against the current split runtime/tools package surface with `tools/release/local_registry_publish.py publish --surface npm --strict`. diff --git a/tools/policy/assertions/assert-ci-workflows.mjs b/tools/policy/assertions/assert-ci-workflows.mjs index 23a2627f..80b4e6bd 100755 --- a/tools/policy/assertions/assert-ci-workflows.mjs +++ b/tools/policy/assertions/assert-ci-workflows.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env bun import {readFileSync} from 'node:fs'; -import {spawnSync} from 'node:child_process'; +import {execFileSync, spawnSync} from 'node:child_process'; import process from 'node:process'; function workspaceRoot() { @@ -35,6 +35,31 @@ function read(path) { return readFileSync(path, 'utf8'); } +function releaseGraphJson(args) { + const output = execFileSync( + 'tools/dev/bun.sh', + ['tools/release/release_graph_query.mjs', ...args], + { + cwd: root, + encoding: 'utf8', + maxBuffer: 100 * 1024 * 1024, + }, + ); + return JSON.parse(output); +} + +function releaseGraphLines(args) { + return execFileSync( + 'tools/dev/bun.sh', + ['tools/release/release_graph_query.mjs', ...args], + { + cwd: root, + encoding: 'utf8', + maxBuffer: 100 * 1024 * 1024, + }, + ).trim().split(/\r?\n/u).filter(Boolean); +} + function requireText(path, text, message = `${path} must contain ${text}`) { if (!read(path).includes(text)) { fail(message); @@ -106,6 +131,17 @@ function assertBlockContains(blocks, job, text, message) { } } +function assertSameItems(actual, expected, message) { + const actualSorted = [...actual].sort(); + const expectedSorted = [...expected].sort(); + if ( + actualSorted.length !== expectedSorted.length || + actualSorted.some((item, index) => item !== expectedSorted[index]) + ) { + fail(`${message}; expected=${JSON.stringify(expectedSorted)} actual=${JSON.stringify(actualSorted)}`); + } +} + function checkoutStep(blocks, job) { const block = jobBlock(blocks, job); const match = block.match(/ - name: Checkout repository\n[\s\S]*?(?=\n - name: |\n$)/); @@ -145,6 +181,36 @@ const mobileBlocks = jobBlocks(mobilePath); const beforePushTrigger = ci.split('push:', 1)[0] ?? ''; const ciHeadRef = 'ref: ${{ github.event.pull_request.head.sha || github.sha }}'; const mobileArtifactRef = 'ref: ${{ needs.resolve.outputs.sha }}'; +const nativeRuntimeCiArtifacts = releaseGraphLines([ + 'ci-artifact-names', + '--product', + 'liboliphaunt-native', + '--kind', + 'native-runtime', + '--family', + 'release-assets', + '--format', + 'lines', +]); +const nativeToolCiArtifacts = releaseGraphLines([ + 'ci-artifact-names', + '--product', + 'liboliphaunt-native', + '--kind', + 'native-tools', + '--family', + 'release-assets', + '--format', + 'lines', +]); +const nativeExpectedAssets = releaseGraphJson([ + 'expected-assets', + '--product', + 'liboliphaunt-native', + '--version', + '0.0.0', +]); +const wasixCargoContract = releaseGraphJson(['wasix-cargo-artifact-contract']); requireText(ciPath, 'name: CI'); requireText( @@ -196,6 +262,78 @@ requireText(ciPath, 'name: react-native-mobile-android-app-android-x86_64'); requireText(ciPath, 'name: react-native-mobile-ios-app'); requireText(ciPath, 'OLIPHAUNT_ANDROID_EMULATOR_API: "35"'); rejectText(ciPath, 'OLIPHAUNT_SKIP_TARGETS_COVERED_BY_PLANNED_JOBS'); +if (nativeToolCiArtifacts.length === 0) { + fail('native tools must declare CI release-asset artifact targets'); +} +for (const artifact of nativeToolCiArtifacts) { + if (!nativeRuntimeCiArtifacts.includes(artifact)) { + fail(`native tools artifact ${artifact} must share the native per-target release-asset upload name`); + } +} +assertSameItems( + nativeExpectedAssets + .filter((row) => row.kind === 'native-tools') + .map((row) => row.target), + ['linux-arm64-gnu', 'linux-x64-gnu', 'macos-arm64', 'windows-x64-msvc'], + 'native tools release assets must cover every desktop registry target', +); +assertBlockContains( + ciBlocks, + 'liboliphaunt-native-desktop', + 'name: liboliphaunt-native-release-assets-${{ matrix.target }}', + 'desktop native runtime/tools artifacts must share the per-target release-assets upload', +); +assertBlockContains( + ciBlocks, + 'liboliphaunt-native-release-assets', + 'pattern: liboliphaunt-native-release-assets-*', + 'aggregate native release assets must download every per-target runtime/tools upload', +); +assertBlockContains( + ciBlocks, + 'liboliphaunt-native-release-assets', + 'name: liboliphaunt-native-release-assets', + 'aggregate native release assets must expose one release-consumable artifact', +); +assertBlockContains( + ciBlocks, + 'wasix-rust-package', + 'run: OLIPHAUNT_CI_JOB_TARGETS_JSON=\'${{ needs.affected.outputs.job_targets }}\' MOON_CACHE=off .github/scripts/run-planned-moon-job.sh wasix-rust-package', + 'WASIX Rust package CI job must run the Moon-modeled package artifact task', +); +assertBlockContains( + ciBlocks, + 'wasix-rust-package', + 'name: oliphaunt-wasix-rust-package-artifacts', + 'WASIX Rust package CI job must upload the Cargo SDK/runtime artifact envelope', +); +assertBlockContains( + ciBlocks, + 'wasix-rust-package', + 'path: target/sdk-artifacts/oliphaunt-wasix-rust', + 'WASIX Rust package CI job must upload the staged package artifact root', +); +assertSameItems( + wasixCargoContract.publicCargoPackageNames, + [ + wasixCargoContract.runtimePackage, + wasixCargoContract.toolsPackage, + wasixCargoContract.icuPackage, + ...Object.values(wasixCargoContract.aotPackages), + ...Object.values(wasixCargoContract.toolsAotPackages), + ], + 'WASIX public Cargo packages must be exactly runtime, tools, ICU, runtime-AOT, and tools-AOT packages', +); +requireText( + 'tools/release/build-sdk-ci-artifacts.sh', + 'package_oliphaunt_wasix_sdk_crate.mjs --output-dir "$artifact_root"', + 'WASIX Rust package artifact builder must stage the registry-resolved WASIX SDK crate', +); +requireText( + 'tools/release/check-staged-artifacts.mjs', + 'WASIX_TOOLS_AOT_PACKAGES', + 'staged WASIX SDK artifact checks must validate tools-AOT registry dependencies', +); assertBlockContains(ciBlocks, 'check-targets', 'matrix: ${{ fromJson(needs.affected.outputs.check_matrix) }}', 'check targets must use the Moon-selected check matrix'); assertBlockContains(ciBlocks, 'policy-targets', 'matrix: ${{ fromJson(needs.affected.outputs.policy_matrix) }}', 'policy targets must use the Moon-selected policy matrix'); assertBlockContains(ciBlocks, 'test-targets', 'matrix: ${{ fromJson(needs.affected.outputs.test_matrix) }}', 'test targets must use the Moon-selected test matrix'); From ac3e3f3698746520920f9451e6a2fef3815db17f Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 17:02:09 +0000 Subject: [PATCH 202/308] fix: validate kotlin extension ids against catalog --- .../examples-ci-release-validation.md | 14 ++++++ src/extensions/tools/check-extension-model.py | 20 ++++++++ .../OliphauntAndroidRuntimeAssets.kt | 2 +- .../dev/oliphaunt/GeneratedExtensions.kt | 48 +++++++++++++++++++ .../kotlin/dev/oliphaunt/Oliphaunt.kt | 32 ++++++++----- .../dev/oliphaunt/OliphauntDatabaseTest.kt | 12 +++++ .../dev/oliphaunt/NativeDirectEngine.kt | 25 +--------- .../dev/oliphaunt/NativeDirectEngineTest.kt | 24 +++++++++- .../assertions/assert-source-inputs.mjs | 1 + .../check-final-source-architecture.mjs | 1 + tools/policy/check-sdk-parity.sh | 4 ++ 11 files changed, 144 insertions(+), 39 deletions(-) create mode 100644 src/sdks/kotlin/oliphaunt/src/commonMain/kotlin/dev/oliphaunt/GeneratedExtensions.kt diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index a9318ff9..1e48301c 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -83,6 +83,20 @@ the release/tooling surface after the runtime tool crate split. ## Current Evidence +- On 2026-06-27, Kotlin extension ID validation was brought into parity with + TypeScript and React Native generated-catalog validation. Kotlin now + generates `GeneratedExtensions.kt` from the extension model, rejects + syntactically valid but unpublished extension IDs such as `pg_search` before + public open, native-direct engine open, or Android runtime asset selection, + and keeps the generated source under extension-model and source-architecture + policy checks. Fresh checks passed: + `python3 src/extensions/tools/check-extension-model.py --check`, + `ANDROID_HOME=/home/sid/android-sdk ANDROID_SDK_ROOT=/home/sid/android-sdk bash src/sdks/kotlin/tools/check-sdk.sh test-unit`, + `ANDROID_HOME=/home/sid/android-sdk ANDROID_SDK_ROOT=/home/sid/android-sdk bash src/sdks/kotlin/tools/check-sdk.sh check-static`, + `bash tools/policy/check-sdk-parity.sh`, + `bash tools/policy/check-sdk-mobile-extension-surface.sh`, + `tools/dev/bun.sh tools/policy/check-final-source-architecture.mjs`, and + `git diff --check`. - On 2026-06-27, the open release DRY and SDK consistency tracker items were rechecked against current source. Fresh checks passed: `bash tools/policy/check-sdk-parity.sh`, diff --git a/src/extensions/tools/check-extension-model.py b/src/extensions/tools/check-extension-model.py index cf991ebc..35a787e3 100755 --- a/src/extensions/tools/check-extension-model.py +++ b/src/extensions/tools/check-extension-model.py @@ -43,6 +43,7 @@ GENERATED_RUST_SDK_MODULE = ROOT / "src/sdks/rust/src/generated/extensions.rs" GENERATED_TS_SDK_MODULE = ROOT / "src/sdks/js/src/generated/extensions.ts" GENERATED_KOTLIN_SDK_METADATA = ROOT / "src/sdks/kotlin/oliphaunt/src/generated/extensions.json" +GENERATED_KOTLIN_SDK_MODULE = ROOT / "src/sdks/kotlin/oliphaunt/src/commonMain/kotlin/dev/oliphaunt/GeneratedExtensions.kt" GENERATED_RN_SDK_MODULE = ROOT / "src/sdks/react-native/src/generated/extensions.ts" GENERATED_RN_PLUGIN_METADATA = ROOT / "src/sdks/react-native/src/generated/extensions.json" GENERATED_MOBILE_REGISTRY = ROOT / "src/extensions/generated/mobile/static-registry.json" @@ -1028,6 +1029,20 @@ def camel(row: dict) -> dict: return format_typescript_source(source, GENERATED_TS_SDK_MODULE) +def generated_kotlin_extension_module(metadata: dict) -> str: + names = sorted(str(row["sql-name"]) for row in metadata.get("extensions", [])) + body = "\n".join(f" {json.dumps(name)}," for name in names) + return ( + "// This file is generated by src/extensions/tools/check-extension-model.py.\n" + "// Do not edit by hand.\n\n" + "package dev.oliphaunt\n\n" + "internal val generatedExtensionSqlNames: Set = setOf(\n" + f"{body}\n" + ")\n\n" + "internal fun generatedExtensionSqlNameExists(sqlName: String): Boolean = generatedExtensionSqlNames.contains(sqlName)\n" + ) + + def rust_string_literal(value: str) -> str: return json.dumps(value) @@ -1640,6 +1655,11 @@ def validate_generated_sdk_metadata(catalog: dict, build_plan: dict, write: bool generated_typescript_extension_module(rn_metadata), write, ) + validate_generated_text_file( + GENERATED_KOTLIN_SDK_MODULE, + generated_kotlin_extension_module(kotlin_metadata), + write, + ) validate_generated_file(GENERATED_KOTLIN_SDK_METADATA, kotlin_metadata, write) validate_generated_file(GENERATED_RN_PLUGIN_METADATA, rn_metadata, write) validate_generated_file(GENERATED_MOBILE_REGISTRY, generated_mobile_registry(catalog), write) 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 8e5ef5b4..c1d57a5d 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 @@ -676,7 +676,7 @@ internal object OliphauntAndroidRuntimeAssets { return filePackageManifestOrNull(resourceRoot, RUNTIME_ASSET_ROOT) } - private fun validateExtensionIds(values: Collection): Set = validatePortableIds(values, label = "extension id") + private fun validateExtensionIds(values: Collection): Set = validateGeneratedExtensionIds(values, label = "liboliphaunt extension id").toSortedSet() private fun validateRuntimeFeatures(values: Collection): Set { val features = validatePortableIds(values, label = "runtime feature") diff --git a/src/sdks/kotlin/oliphaunt/src/commonMain/kotlin/dev/oliphaunt/GeneratedExtensions.kt b/src/sdks/kotlin/oliphaunt/src/commonMain/kotlin/dev/oliphaunt/GeneratedExtensions.kt new file mode 100644 index 00000000..0dfa59b3 --- /dev/null +++ b/src/sdks/kotlin/oliphaunt/src/commonMain/kotlin/dev/oliphaunt/GeneratedExtensions.kt @@ -0,0 +1,48 @@ +// This file is generated by src/extensions/tools/check-extension-model.py. +// Do not edit by hand. + +package dev.oliphaunt + +internal val generatedExtensionSqlNames: Set = setOf( + "amcheck", + "auto_explain", + "bloom", + "btree_gin", + "btree_gist", + "citext", + "cube", + "dict_int", + "dict_xsyn", + "earthdistance", + "file_fdw", + "fuzzystrmatch", + "hstore", + "intarray", + "isn", + "lo", + "ltree", + "pageinspect", + "pg_buffercache", + "pg_freespacemap", + "pg_hashids", + "pg_ivm", + "pg_surgery", + "pg_textsearch", + "pg_trgm", + "pg_uuidv7", + "pg_visibility", + "pg_walinspect", + "pgcrypto", + "pgtap", + "postgis", + "seg", + "tablefunc", + "tcn", + "tsm_system_rows", + "tsm_system_time", + "unaccent", + "uuid-ossp", + "vector", +) + +internal fun generatedExtensionSqlNameExists(sqlName: String): Boolean = generatedExtensionSqlNames.contains(sqlName) 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 3923003b..a3cb80a9 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,6 +180,24 @@ internal fun validateStartupGucs(gucs: List) { } } +private val portableExtensionId = Regex("[A-Za-z0-9._-]{1,128}") + +internal fun validateGeneratedExtensionIds( + extensions: Collection, + label: String = "Kotlin Oliphaunt extension id", +): List = extensions.map(String::trim) + .filter(String::isNotEmpty) + .onEach { extension -> + if (!portableExtensionId.matches(extension)) { + throw OliphauntException( + "$label '$extension' must contain 1 to 128 ASCII letters, digits, '.', '_' or '-'", + ) + } + if (!generatedExtensionSqlNameExists(extension)) { + throw OliphauntException("unknown $label '$extension'") + } + } + internal fun OliphauntConfig.postgresStartupArgs(sharedPreloadLibraries: Collection = emptyList()): List = runtimeFootprint.postgresStartupArgs() + durability.postgresStartupArgs() + startupGucs.flatMap { guc -> listOf("-c", "${guc.name.trim()}=${guc.value}") } + @@ -619,7 +637,7 @@ public class OliphauntDatabase private constructor( validateStartupIdentity(config.database, "database") validateStartupGucs(config.startupGucs) val normalizedConfig = config.copy( - extensions = validateExtensionIds(config.extensions), + extensions = validateGeneratedExtensionIds(config.extensions), ) return OliphauntDatabase(engine.open(normalizedConfig)) } @@ -641,18 +659,6 @@ public class OliphauntDatabase private constructor( engine: OliphauntEngine = defaultOliphauntEngine(EngineMode.NativeDirect), ): List = engine.supportedModes() - private fun validateExtensionIds(extensions: Collection): List = extensions.map(String::trim) - .filter(String::isNotEmpty) - .onEach { extension -> - if (!portableId.matches(extension)) { - throw OliphauntException( - "Kotlin Oliphaunt extension id '$extension' must contain only ASCII letters, digits, '.', '_' or '-'", - ) - } - } - - private val portableId = Regex("[A-Za-z0-9._-]{1,128}") - private const val sessionPinnedMessage: String = "physical session is pinned; use the active OliphauntTransaction" } 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 126be063..e465c32c 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 @@ -849,6 +849,18 @@ class OliphauntDatabaseTest { assertTrue(error.message.orEmpty().contains("extension id 'mobile/vector'")) assertEquals(0, engine.openCalls) + val unknownError = + assertFailsWith { + OliphauntDatabase.open( + config = OliphauntConfig(extensions = listOf("pg_search")), + engine = engine, + ) + } + assertTrue( + unknownError.message.orEmpty().contains("unknown Kotlin Oliphaunt extension id 'pg_search'"), + ) + assertEquals(0, engine.openCalls) + val database = OliphauntDatabase.open( config = OliphauntConfig(extensions = listOf(" pg_trgm ", "", "vector", "hstore")), diff --git a/src/sdks/kotlin/oliphaunt/src/nativeMain/kotlin/dev/oliphaunt/NativeDirectEngine.kt b/src/sdks/kotlin/oliphaunt/src/nativeMain/kotlin/dev/oliphaunt/NativeDirectEngine.kt index 5407c276..442b777f 100644 --- a/src/sdks/kotlin/oliphaunt/src/nativeMain/kotlin/dev/oliphaunt/NativeDirectEngine.kt +++ b/src/sdks/kotlin/oliphaunt/src/nativeMain/kotlin/dev/oliphaunt/NativeDirectEngine.kt @@ -83,7 +83,7 @@ public class NativeDirectEngine( validateStartupIdentity(config.username ?: username, "username") validateStartupIdentity(config.database ?: database, "database") validateStartupGucs(config.startupGucs) - validateExtensionIds(config.extensions) + validateGeneratedExtensionIds(config.extensions, label = "Kotlin native-direct extension id") val resolvedRuntimeDirectory = runtimeDirectory ?: env("OLIPHAUNT_INSTALL_DIR") @@ -407,29 +407,6 @@ private fun lastError(session: CPointer?): String = olip private fun env(name: String): String? = getenv(name)?.toKString()?.takeIf(String::isNotEmpty) -private fun validateExtensionIds(extensions: List) { - extensions - .map(String::trim) - .filter(String::isNotEmpty) - .forEach { extension -> - val valid = - extension.length <= 128 && - extension.all { char -> - char in 'A'..'Z' || - char in 'a'..'z' || - char in '0'..'9' || - char == '.' || - char == '_' || - char == '-' - } - if (!valid) { - throw OliphauntException( - "Kotlin native-direct extension id '$extension' must contain only ASCII letters, digits, '.', '_' or '-'", - ) - } - } -} - private fun ensureDirectory(path: String) { val parts = path.split('/').filter(String::isNotEmpty) var current = if (path.startsWith('/')) "/" else "" diff --git a/src/sdks/kotlin/oliphaunt/src/nativeTest/kotlin/dev/oliphaunt/NativeDirectEngineTest.kt b/src/sdks/kotlin/oliphaunt/src/nativeTest/kotlin/dev/oliphaunt/NativeDirectEngineTest.kt index b113cd6b..97c33a8c 100644 --- a/src/sdks/kotlin/oliphaunt/src/nativeTest/kotlin/dev/oliphaunt/NativeDirectEngineTest.kt +++ b/src/sdks/kotlin/oliphaunt/src/nativeTest/kotlin/dev/oliphaunt/NativeDirectEngineTest.kt @@ -82,7 +82,29 @@ class NativeDirectEngineTest { engine = engine, ) } - assertTrue(error.message.orEmpty().contains("must contain only ASCII")) + assertTrue(error.message.orEmpty().contains("must contain 1 to 128 ASCII")) + } + + @Test + fun extensionIdsMustExistInGeneratedCatalog() = runTest { + val engine = + NativeDirectEngine( + libraryPath = "/tmp/oliphaunt-missing.dylib", + runtimeDirectory = "/tmp/oliphaunt-runtime", + ) + + val error = + assertFailsWith { + engine.open( + OliphauntConfig( + mode = EngineMode.NativeDirect, + extensions = listOf("pg_search"), + ), + ) + } + assertTrue( + error.message.orEmpty().contains("unknown Kotlin native-direct extension id 'pg_search'"), + ) } @Test diff --git a/tools/policy/assertions/assert-source-inputs.mjs b/tools/policy/assertions/assert-source-inputs.mjs index 596df85b..8c525415 100755 --- a/tools/policy/assertions/assert-source-inputs.mjs +++ b/tools/policy/assertions/assert-source-inputs.mjs @@ -115,6 +115,7 @@ function checkExtensions() { 'src/extensions/generated/sdk/react-native.json', 'src/sdks/rust/src/generated/extensions.rs', 'src/sdks/js/src/generated/extensions.ts', + 'src/sdks/kotlin/oliphaunt/src/commonMain/kotlin/dev/oliphaunt/GeneratedExtensions.kt', 'src/sdks/kotlin/oliphaunt/src/generated/extensions.json', 'src/sdks/react-native/src/generated/extensions.ts', 'src/sdks/react-native/src/generated/extensions.json', diff --git a/tools/policy/check-final-source-architecture.mjs b/tools/policy/check-final-source-architecture.mjs index 47ca6513..744fe01d 100755 --- a/tools/policy/check-final-source-architecture.mjs +++ b/tools/policy/check-final-source-architecture.mjs @@ -101,6 +101,7 @@ const GENERATED_SDK_METADATA = [ ]; const GENERATED_SDK_PACKAGE_METADATA = [ 'src/sdks/js/src/generated/extensions.ts', + 'src/sdks/kotlin/oliphaunt/src/commonMain/kotlin/dev/oliphaunt/GeneratedExtensions.kt', 'src/sdks/kotlin/oliphaunt/src/generated/extensions.json', 'src/sdks/react-native/src/generated/extensions.ts', 'src/sdks/react-native/src/generated/extensions.json', diff --git a/tools/policy/check-sdk-parity.sh b/tools/policy/check-sdk-parity.sh index 2bef9acf..f449b973 100755 --- a/tools/policy/check-sdk-parity.sh +++ b/tools/policy/check-sdk-parity.sh @@ -1205,6 +1205,10 @@ 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/kotlin/oliphaunt/src/commonMain/kotlin/dev/oliphaunt/Oliphaunt.kt "generatedExtensionSqlNameExists(extension)" \ + "Kotlin SDK must validate selected extension identifiers against the generated catalog before engine open" +require_text src/sdks/kotlin/oliphaunt/src/commonTest/kotlin/dev/oliphaunt/OliphauntDatabaseTest.kt "pg_search" \ + "Kotlin SDK must test unknown generated-catalog extension identifiers before engine open" 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 8bd2c8f6071f5fd62a2ae338d47ae32c3af63e35 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 17:10:57 +0000 Subject: [PATCH 203/308] fix: tighten source input policy exceptions --- .../examples-ci-release-validation.md | 4 ++ .../assertions/assert-source-inputs.mjs | 63 +++++++++++++++---- tools/policy/check-docs.sh | 6 +- .../check-final-source-architecture.mjs | 6 +- 4 files changed, 63 insertions(+), 16 deletions(-) diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index 1e48301c..583db295 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -244,6 +244,10 @@ 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. +- Source-input policy now treats local Cargo file-registry URLs as an owned + example lockfile detail, while still rejecting stale upstream identifiers + in general tracked source. The passing guard is + `tools/dev/bun.sh tools/policy/assertions/assert-source-inputs.mjs`. - 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, diff --git a/tools/policy/assertions/assert-source-inputs.mjs b/tools/policy/assertions/assert-source-inputs.mjs index 8c525415..87085f54 100755 --- a/tools/policy/assertions/assert-source-inputs.mjs +++ b/tools/policy/assertions/assert-source-inputs.mjs @@ -42,6 +42,27 @@ function requireText(path, text) { } } +function gitGrep(args) { + const result = run('git', ['grep', '-I', '-n', ...args, '--', ':!target/**', ':!node_modules/**']); + if (result.status === 1) { + return []; + } + if (result.status !== 0) { + process.exit(result.status ?? 1); + } + return result.stdout.trim().split(/\r?\n/u).filter(Boolean); +} + +function grepLinePath(line) { + const separator = line.indexOf(':'); + return separator === -1 ? line : line.slice(0, separator); +} + +function unexpectedGrepLines(lines, allowedPaths) { + const allowed = new Set(allowedPaths); + return lines.filter((line) => !allowed.has(grepLinePath(line))); +} + function checkPostgres18() { requireText('src/postgres/versions/18/source.toml', 'version = "18.4"'); requireText('src/postgres/versions/18/source.toml', 'postgresql-18.4.tar.bz2'); @@ -139,6 +160,13 @@ function checkExtensions() { } function checkRepoPolicy() { + const localRegistryLockfiles = [ + 'examples/electron-wasix/src-wasix/Cargo.lock', + 'examples/tauri/src-tauri/Cargo.lock', + 'examples/tauri-wasix/src-tauri/Cargo.lock', + 'src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.lock', + ]; + const assets = run('git', ['ls-files', 'assets']); if (assets.status !== 0) { process.exit(assets.status ?? 1); @@ -154,12 +182,24 @@ function checkRepoPolicy() { fail(`src/third-party must not contain tracked files:\n${retiredThirdParty.stdout.trim()}`); } + const localRegistrySourcePrefix = 'registry+' + 'file://'; + requireText('tools/policy/check-docs.sh', 'root README is intentionally pinned'); + requireText('tools/release/sync-example-lockfiles.mjs', `const localRegistrySourcePrefix = '${localRegistrySourcePrefix}';`); + requireText('examples/tools/check-lockfiles.sh', 'tools/release/sync-example-lockfiles.mjs --check'); + + const localRegistryLines = gitGrep(['-e', localRegistrySourcePrefix]); + const unexpectedLocalRegistry = unexpectedGrepLines(localRegistryLines, [ + ...localRegistryLockfiles, + 'tools/release/sync-example-lockfiles.mjs', + ]); + if (unexpectedLocalRegistry.length > 0) { + console.error(unexpectedLocalRegistry.join('\n')); + fail('local Cargo registry file URLs may only appear in example lockfiles and their lockfile sync helper'); + } + const removedName = 'pg' + 'lite'; - const grep = run('git', [ - 'grep', - '-I', + const grepLines = gitGrep([ '-i', - '-n', '-e', `@electric-sql/${removedName}`, '-e', @@ -180,17 +220,16 @@ function checkRepoPolicy() { 'PG' + 'Lite', '-e', removedName, - '--', - ':!target/**', - ':!node_modules/**', ]); - if (grep.status === 0) { - console.error(grep.stdout); + const unexpectedLegacyLines = unexpectedGrepLines(grepLines, [ + 'README.md', + 'docs/internal/OLIPHAUNT_README.md', + ...localRegistryLockfiles, + ]); + if (unexpectedLegacyLines.length > 0) { + console.error(unexpectedLegacyLines.join('\n')); fail('removed upstream identifiers remain in tracked source'); } - if (grep.status !== 1) { - process.exit(grep.status ?? 1); - } } process.chdir(workspaceRoot()); diff --git a/tools/policy/check-docs.sh b/tools/policy/check-docs.sh index c44379ef..63d5f487 100755 --- a/tools/policy/check-docs.sh +++ b/tools/policy/check-docs.sh @@ -123,9 +123,9 @@ retired_docs_args=() for retired_doc in "${retired_docs_grep[@]}"; do retired_docs_args+=(-e "$retired_doc") done -# The root README is intentionally pinned to the main-branch pglite-oxide -# README until the Oliphaunt public README is ready. Its legacy docs links are -# allowed while the Oliphaunt-specific version lives under docs/internal/. +# The root README is intentionally pinned to the previous main-branch README +# until the Oliphaunt public README is ready. Its legacy docs links are allowed +# while the Oliphaunt-specific version lives under docs/internal/. if git grep -n -F "${retired_docs_args[@]}" -- docs src tools .github .moon | grep -v '^tools/policy/check-docs\.sh:' >/tmp/docs-retired-grep.$$ 2>/dev/null; then cat /tmp/docs-retired-grep.$$ >&2 diff --git a/tools/policy/check-final-source-architecture.mjs b/tools/policy/check-final-source-architecture.mjs index 744fe01d..f2ce6a4c 100755 --- a/tools/policy/check-final-source-architecture.mjs +++ b/tools/policy/check-final-source-architecture.mjs @@ -652,7 +652,11 @@ async function checkSdkLocalExtensionRules() { if (!SDK_RUNTIME_SOURCE_PREFIXES.some((prefix) => file.startsWith(prefix))) { continue; } - if (TRANSITIONAL_EXTENSION_RULE_FILES.has(file) || file.includes('/generated/')) { + if ( + TRANSITIONAL_EXTENSION_RULE_FILES.has(file) || + GENERATED_SDK_PACKAGE_METADATA.includes(file) || + file.includes('/generated/') + ) { continue; } if (file.includes('/tests/') || file.includes('/Tests/') || file.includes('/__tests__/')) { From 27f78920b213b9be1205824f7e921d100ca897da Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 17:15:44 +0000 Subject: [PATCH 204/308] chore: record helper migration inventory --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 44 +++++++++++++++---- .../list-helper-reference-candidates.mjs | 29 +++++++++--- 2 files changed, 59 insertions(+), 14 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index e7d72309..36025da4 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -1736,7 +1736,7 @@ until the current-state gates here are checked with fresh local evidence. - [ ] 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. +- [x] 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. @@ -1952,11 +1952,27 @@ until the current-state gates here are checked with fresh local evidence. - 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`. - The current inventory contains release orchestration/package validators, - product metadata adapters, the WASIX Cargo artifact packager, local registry - publishing, release policy checks, and the extension model generator. New - Python files must either be intentionally allowlisted or ported to Bun. The - per-Python-script migration decisions remain open. + The current inventory contains 9 tracked Python files: release orchestration, + release/package validators, the product metadata adapter, the WASIX Cargo + artifact packager, local registry publishing, release policy checks, and the + extension model generator. New Python files must either be intentionally + allowlisted or ported to Bun. The current migration order is: + 1. split `product_metadata.py` consumers onto already-existing Bun graph + helpers until the compatibility module has no direct callers; + 2. port release checkers in the release-graph cluster + (`check-release-policy.py`, `check_artifact_targets.py`, + `check_release_metadata.py`, `check_consumer_shape.py`) behind parity + smokes and then remove their Python compatibility imports; + 3. port `package_liboliphaunt_wasix_cargo_artifacts.py` after release graph + metadata is Bun-native, because it depends on exact package metadata and + crates.io size-limit enforcement; + 4. port `local_registry_publish.py` after artifact package generation and + release metadata are Bun-native, preserving the local registry e2e path; + 5. port `release.py` last, when the underlying validators and registry helpers + have Bun entrypoints; + 6. port `src/extensions/tools/check-extension-model.py` as a separate + generator migration, because it is the canonical multi-language extension + model and needs generated-output parity across SDKs. - 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 @@ -1998,7 +2014,7 @@ until the current-state gates here are checked with fresh local evidence. `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. + explicit Python entrypoint inventory. On 2026-06-26, focused validation passed with `check-tooling-stack.sh`, `check_release_metadata.py`, `check_artifact_targets.py`, `check_consumer_shape.py`, @@ -2021,8 +2037,7 @@ until the current-state gates here are checked with fresh local evidence. `tools/graph/graph.mjs`. On 2026-06-26, validation passed with the direct Bun helper smoke, pull-request-mode `ci_plan.mjs` smoke, graph checks, `check-tooling-stack.sh`, `check-repo-structure.sh`, - `check_artifact_targets.py`, and `check-release-policy.py`; the intentional - Python inventory contained 32 tracked files at that point. + `check_artifact_targets.py`, and `check-release-policy.py`. - Rust helper inventory is machine-checked by `tools/policy/check-rust-helper-crates.mjs` and currently limited to `tools/xtask` and `tools/perf/runner`. Both remain Rust-owned for now: @@ -2031,6 +2046,17 @@ until the current-state gates here are checked with fresh local evidence. 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. +- Helper dead-code discovery now has an active-source mode: + `tools/dev/bun.sh tools/policy/list-helper-reference-candidates.mjs --max-refs 0 --active-only` + ignores Markdown/history references and reports scripts with no code, CI, or + tooling callers. On 2026-06-27 it reported + `src/runtimes/liboliphaunt/native/bin/check-c-abi-conformance.sh`, + `src/runtimes/liboliphaunt/native/bin/smoke-macos-happy-path.sh`, + `tools/dev/install-hooks.sh`, and four policy readiness helpers + (`check-feature-powerset.sh`, `check-rust-lint.sh`, `check-semver.sh`, + `check-supply-chain.sh`). These are not deletion-proof yet because several are + documented human/readiness entrypoints; removal still requires a manual owner + decision or replacement CI wiring. - 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/list-helper-reference-candidates.mjs b/tools/policy/list-helper-reference-candidates.mjs index d794202c..43a246b0 100644 --- a/tools/policy/list-helper-reference-candidates.mjs +++ b/tools/policy/list-helper-reference-candidates.mjs @@ -11,16 +11,20 @@ function fail(message) { } function usage() { - console.log(`usage: tools/policy/list-helper-reference-candidates.mjs [--max-refs N] [--json] + console.log(`usage: tools/policy/list-helper-reference-candidates.mjs [--max-refs N] [--active-only] [--json] Lists tracked shell, Python, and JavaScript helper entrypoints with few textual references. The output is advisory: each candidate still needs manual review before removal because some entrypoints are intentionally invoked by humans or -external tools.`); +external tools. + +Use --active-only to ignore Markdown/docs references and focus on code, CI, and +tooling callers.`); } let maxRefs = 1; let json = false; +let activeOnly = false; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg === "--max-refs") { @@ -33,6 +37,8 @@ for (let index = 0; index < args.length; index += 1) { fail("--max-refs must be a non-negative integer"); } index += 1; + } else if (arg === "--active-only") { + activeOnly = true; } else if (arg === "--json") { json = true; } else if (arg === "--help" || arg === "-h") { @@ -106,8 +112,21 @@ function grepFixed(pattern) { return result.stdout.split(/\r?\n/u).filter(Boolean); } +function grepLinePath(line) { + const separator = line.indexOf(":"); + return separator === -1 ? line : line.slice(0, separator); +} + +function isActiveReference(line) { + if (!activeOnly) { + return true; + } + const file = grepLinePath(line); + return !file.endsWith(".md") && !file.startsWith("docs/"); +} + function externalReferenceCount(path, pattern) { - return grepFixed(pattern).filter((line) => !line.startsWith(`${path}:`)).length; + return grepFixed(pattern).filter((line) => !line.startsWith(`${path}:`) && isActiveReference(line)).length; } function referenceSuffixes(path) { @@ -170,9 +189,9 @@ const candidates = trackedHelpers() }); if (json) { - console.log(JSON.stringify({ maxRefs, candidates }, null, 2)); + console.log(JSON.stringify({ maxRefs, activeOnly, candidates }, null, 2)); } else { - console.log(`Low-reference helper candidates (maxRefs=${maxRefs}):`); + console.log(`Low-reference helper candidates (maxRefs=${maxRefs}, activeOnly=${activeOnly}):`); if (candidates.length === 0) { console.log(" none"); } From f0f6b34dfb4c62affdd47f39f6a08a318a9e43b5 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 17:26:52 +0000 Subject: [PATCH 205/308] chore: port helper wrappers to bun --- CONTRIBUTING.md | 2 +- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 12 ++- docs/maintainers/development.md | 18 +++-- tools/dev/install-hooks.mjs | 80 +++++++++++++++++++ tools/dev/install-hooks.sh | 25 ------ tools/policy/check-feature-powerset.mjs | 15 ++++ tools/policy/check-feature-powerset.sh | 10 --- tools/policy/check-rust-lint.mjs | 15 ++++ tools/policy/check-rust-lint.sh | 16 ---- tools/policy/check-semver.mjs | 14 ++++ tools/policy/check-semver.sh | 10 --- tools/policy/check-supply-chain.mjs | 7 ++ tools/policy/check-supply-chain.sh | 10 --- tools/policy/lib/run-command.mjs | 37 +++++++++ .../list-source-reference-candidates.mjs | 11 ++- 15 files changed, 199 insertions(+), 83 deletions(-) create mode 100755 tools/dev/install-hooks.mjs delete mode 100755 tools/dev/install-hooks.sh create mode 100755 tools/policy/check-feature-powerset.mjs delete mode 100755 tools/policy/check-feature-powerset.sh create mode 100755 tools/policy/check-rust-lint.mjs delete mode 100755 tools/policy/check-rust-lint.sh create mode 100755 tools/policy/check-semver.mjs delete mode 100755 tools/policy/check-semver.sh create mode 100755 tools/policy/check-supply-chain.mjs delete mode 100755 tools/policy/check-supply-chain.sh create mode 100644 tools/policy/lib/run-command.mjs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8f156975..34ae0e3c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,7 +18,7 @@ The runtime smoke starts embedded Postgres and is intentionally slower than unit Install local hooks with: ```sh -tools/dev/install-hooks.sh +tools/dev/bun.sh tools/dev/install-hooks.mjs ``` Hooks stay deliberately smaller than CI: pre-commit handles file hygiene and diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 36025da4..46471ee9 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -2054,9 +2054,15 @@ until the current-state gates here are checked with fresh local evidence. `src/runtimes/liboliphaunt/native/bin/smoke-macos-happy-path.sh`, `tools/dev/install-hooks.sh`, and four policy readiness helpers (`check-feature-powerset.sh`, `check-rust-lint.sh`, `check-semver.sh`, - `check-supply-chain.sh`). These are not deletion-proof yet because several are - documented human/readiness entrypoints; removal still requires a manual owner - decision or replacement CI wiring. + `check-supply-chain.sh`). The developer-hook installer and the four policy + readiness helpers were then ported to Bun entrypoints + (`install-hooks.mjs`, `check-feature-powerset.mjs`, `check-rust-lint.mjs`, + `check-semver.mjs`, and `check-supply-chain.mjs`) while preserving their + command semantics, with the policy wrappers sharing + `tools/policy/lib/run-command.mjs`. A fresh active-only scan after the port + reports only the two native compatibility wrappers. They are not deletion-proof + yet because they are documented human/native entrypoints; removal still + requires a manual owner decision or replacement CI wiring. - 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/maintainers/development.md b/docs/maintainers/development.md index 12e883f0..850d5618 100644 --- a/docs/maintainers/development.md +++ b/docs/maintainers/development.md @@ -11,7 +11,7 @@ moon run dev-tools:doctor tools/dev/bootstrap-tools.sh moon run :check && moon run :test moon run ci-workflows:check -tools/policy/check-supply-chain.sh +tools/dev/bun.sh tools/policy/check-supply-chain.mjs ``` Tool versions for Moon, Node, pnpm, Bun, and Deno are pinned in `.prototools`. @@ -54,7 +54,8 @@ The validation entrypoint is split by maintainer workflow: - `moon run repo:check`: file hygiene and formatting; - `tools/policy/check-wasm-artifacts.sh`: source-controlled asset input verification plus AOT crate template checks; -- `tools/policy/check-rust-lint.sh`: dependency invariants and clippy; +- `tools/dev/bun.sh tools/policy/check-rust-lint.mjs`: dependency invariants + and clippy; - `tools/policy/check-rust-test-topology.sh`: fast policy check proving Rust doctests and executable tests are owned by product Moon tasks instead of a broad root Cargo wrapper; @@ -149,9 +150,12 @@ The validation entrypoint is split by maintainer workflow: unavailable; - `tools/policy/check-crate-package.sh`: package all published crates and enforce crates.io size limits; -- `tools/policy/check-feature-powerset.sh`: cargo-hack feature combination checks; -- `tools/policy/check-semver.sh`: cargo-semver-checks public API compatibility; -- `tools/policy/check-supply-chain.sh`: cargo-deny dependency policy checks; +- `tools/dev/bun.sh tools/policy/check-feature-powerset.mjs`: cargo-hack + feature combination checks; +- `tools/dev/bun.sh tools/policy/check-semver.mjs`: cargo-semver-checks public + API compatibility; +- `tools/dev/bun.sh tools/policy/check-supply-chain.mjs`: cargo-deny dependency + policy checks; - `moon run :check && moon run :test && moon run :package && moon run :coverage`: default PR parity lane; - `moon run :check && moon run :test && moon run :smoke`: fast contributor lane for repo, lint, source tests, and examples; @@ -177,7 +181,7 @@ Gradle configuration-cache behavior itself. The hook split is intentionally small: - pre-commit: file hygiene and formatting -- release readiness: `tools/policy/check-rust-lint.sh`, +- release readiness: `tools/dev/bun.sh tools/policy/check-rust-lint.mjs`, `tools/policy/check-rust-test-topology.sh`, and `tools/policy/check-wasm-artifacts.sh` - CI/release: path-aware combinations of the same validation modes, workflow @@ -191,7 +195,7 @@ back to source builds. ```sh tools/dev/bootstrap-tools.sh -tools/dev/install-hooks.sh +tools/dev/bun.sh tools/dev/install-hooks.mjs ``` `src/bindings/wasix-rust/crates/oliphaunt-wasix/tests/runtime_smoke.rs` starts the real WASM backend and diff --git a/tools/dev/install-hooks.mjs b/tools/dev/install-hooks.mjs new file mode 100755 index 00000000..14f519d0 --- /dev/null +++ b/tools/dev/install-hooks.mjs @@ -0,0 +1,80 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; +import { accessSync, constants } from "node:fs"; +import path from "node:path"; +import process from "node:process"; + +function fail(message) { + console.error(message); + process.exit(1); +} + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { + stdio: "inherit", + ...options, + }); + if (result.error) { + fail(result.error.message); + } + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + +function output(command, args) { + const result = spawnSync(command, args, { + encoding: "utf8", + }); + if (result.error) { + fail(result.error.message); + } + if (result.status !== 0) { + fail(result.stderr.trim() || `${command} ${args.join(" ")} failed`); + } + return result.stdout.trim(); +} + +function hasCommand(command) { + 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).filter(Boolean)) { + for (const extension of extensions) { + const candidate = path.join(directory, `${command}${extension}`); + try { + accessSync(candidate, constants.X_OK); + return true; + } catch { + // Keep scanning PATH. + } + } + } + return false; +} + +const root = output("git", ["rev-parse", "--show-toplevel"]); +process.chdir(root); + +if (!hasCommand("prek")) { + fail(`missing required command: prek + +Install prek first, then rerun this script: + brew install prek + +Other installation methods are documented at https://prek.j178.dev/installation/`); +} + +const hooksPath = spawnSync( + "git", + ["config", "--local", "--get", "core.hooksPath"], + { encoding: "utf8" }, +); +if (hooksPath.status === 0 && hooksPath.stdout.trim() === ".githooks") { + run("git", ["config", "--local", "--unset", "core.hooksPath"]); +} + +run("prek", ["install", "--prepare-hooks", "--overwrite"]); +console.log("Installed prek hooks from prek.toml"); diff --git a/tools/dev/install-hooks.sh b/tools/dev/install-hooks.sh deleted file mode 100755 index 91182b2e..00000000 --- a/tools/dev/install-hooks.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env sh -set -eu - -root="$(git rev-parse --show-toplevel)" -cd "$root" - -if ! command -v prek >/dev/null 2>&1; then - cat >&2 <<'MSG' -missing required command: prek - -Install prek first, then rerun this script: - brew install prek - -Other installation methods are documented at https://prek.j178.dev/installation/ -MSG - exit 1 -fi - -hooks_path="$(git config --local --get core.hooksPath || true)" -if [ "$hooks_path" = ".githooks" ]; then - git config --local --unset core.hooksPath -fi - -prek install --prepare-hooks --overwrite -echo "Installed prek hooks from prek.toml" diff --git a/tools/policy/check-feature-powerset.mjs b/tools/policy/check-feature-powerset.mjs new file mode 100755 index 00000000..99039d33 --- /dev/null +++ b/tools/policy/check-feature-powerset.mjs @@ -0,0 +1,15 @@ +#!/usr/bin/env bun +import { chdirRepoRoot, run } from "./lib/run-command.mjs"; + +const PREFIX = "check-feature-powerset.mjs"; + +chdirRepoRoot(PREFIX); +run(PREFIX, "cargo", [ + "hack", + "check", + "--workspace", + "--feature-powerset", + "--no-dev-deps", + "--exclude-features", + "aot-serializer,template-runner", +]); diff --git a/tools/policy/check-feature-powerset.sh b/tools/policy/check-feature-powerset.sh deleted file mode 100755 index 1466a194..00000000 --- a/tools/policy/check-feature-powerset.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/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" - -cargo hack check --workspace --feature-powerset --no-dev-deps --exclude-features aot-serializer,template-runner diff --git a/tools/policy/check-rust-lint.mjs b/tools/policy/check-rust-lint.mjs new file mode 100755 index 00000000..8a7d1869 --- /dev/null +++ b/tools/policy/check-rust-lint.mjs @@ -0,0 +1,15 @@ +#!/usr/bin/env bun +import { chdirRepoRoot, run } from "./lib/run-command.mjs"; + +const PREFIX = "check-rust-lint.mjs"; + +chdirRepoRoot(PREFIX); +run(PREFIX, "bash", ["tools/policy/check-dependency-invariants.sh"], { + announce: true, +}); +run( + PREFIX, + "cargo", + ["clippy", "--workspace", "--all-targets", "--locked", "--", "-D", "warnings"], + { announce: true }, +); diff --git a/tools/policy/check-rust-lint.sh b/tools/policy/check-rust-lint.sh deleted file mode 100755 index b83217e0..00000000 --- a/tools/policy/check-rust-lint.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/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" - -run() { - printf '\n==> %s\n' "$*" - "$@" -} - -run tools/policy/check-dependency-invariants.sh -run cargo clippy --workspace --all-targets --locked -- -D warnings diff --git a/tools/policy/check-semver.mjs b/tools/policy/check-semver.mjs new file mode 100755 index 00000000..dd22c4aa --- /dev/null +++ b/tools/policy/check-semver.mjs @@ -0,0 +1,14 @@ +#!/usr/bin/env bun +import { chdirRepoRoot, run } from "./lib/run-command.mjs"; + +const PREFIX = "check-semver.mjs"; + +chdirRepoRoot(PREFIX); +run(PREFIX, "cargo", [ + "semver-checks", + "check-release", + "--package", + "oliphaunt-wasix", + "--manifest-path", + "src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml", +]); diff --git a/tools/policy/check-semver.sh b/tools/policy/check-semver.sh deleted file mode 100755 index 88a726a7..00000000 --- a/tools/policy/check-semver.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/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" - -cargo semver-checks check-release --package oliphaunt-wasix --manifest-path src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml diff --git a/tools/policy/check-supply-chain.mjs b/tools/policy/check-supply-chain.mjs new file mode 100755 index 00000000..c3675a3d --- /dev/null +++ b/tools/policy/check-supply-chain.mjs @@ -0,0 +1,7 @@ +#!/usr/bin/env bun +import { chdirRepoRoot, run } from "./lib/run-command.mjs"; + +const PREFIX = "check-supply-chain.mjs"; + +chdirRepoRoot(PREFIX); +run(PREFIX, "cargo", ["deny", "check"]); diff --git a/tools/policy/check-supply-chain.sh b/tools/policy/check-supply-chain.sh deleted file mode 100755 index 85f56d21..00000000 --- a/tools/policy/check-supply-chain.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/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" - -cargo deny check diff --git a/tools/policy/lib/run-command.mjs b/tools/policy/lib/run-command.mjs new file mode 100644 index 00000000..0029d7cf --- /dev/null +++ b/tools/policy/lib/run-command.mjs @@ -0,0 +1,37 @@ +import { spawnSync } from "node:child_process"; +import process from "node:process"; + +export function fail(prefix, message) { + console.error(`${prefix}: ${message}`); + process.exit(1); +} + +export function repoRoot(prefix) { + const result = spawnSync("git", ["rev-parse", "--show-toplevel"], { + encoding: "utf8", + }); + if (result.error) { + fail(prefix, result.error.message); + } + if (result.status !== 0 || !result.stdout.trim()) { + fail(prefix, "must run inside the Oliphaunt git checkout"); + } + return result.stdout.trim(); +} + +export function chdirRepoRoot(prefix) { + process.chdir(repoRoot(prefix)); +} + +export function run(prefix, command, args, { announce = false } = {}) { + if (announce) { + console.log(`\n==> ${[command, ...args].join(" ")}`); + } + const result = spawnSync(command, args, { stdio: "inherit" }); + if (result.error) { + fail(prefix, result.error.message); + } + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} diff --git a/tools/policy/list-source-reference-candidates.mjs b/tools/policy/list-source-reference-candidates.mjs index e54f4cf6..6a91b35d 100644 --- a/tools/policy/list-source-reference-candidates.mjs +++ b/tools/policy/list-source-reference-candidates.mjs @@ -1,5 +1,6 @@ #!/usr/bin/env bun import { spawnSync } from "node:child_process"; +import { statSync } from "node:fs"; import { basename, extname } from "node:path"; const args = process.argv.slice(2); @@ -107,6 +108,14 @@ function gitLsFiles() { .sort(); } +function isFile(path) { + try { + return statSync(path).isFile(); + } catch { + return false; + } +} + async function fileText(path) { try { return await Bun.file(path).text(); @@ -199,7 +208,7 @@ function referencePatterns(path) { return [...patterns].filter((pattern) => pattern.length > 1); } -const trackedFiles = gitLsFiles(); +const trackedFiles = gitLsFiles().filter((path) => isFile(path)); const corpus = await Promise.all( trackedFiles .filter((path) => isTextSearchPath(path)) From df05cdc6f7c4020cd38919105a74834c0de4f65a Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 17:27:58 +0000 Subject: [PATCH 206/308] docs: correct helper migration inventory --- docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 46471ee9..705c6d9c 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -2060,9 +2060,11 @@ until the current-state gates here are checked with fresh local evidence. `check-semver.mjs`, and `check-supply-chain.mjs`) while preserving their command semantics, with the policy wrappers sharing `tools/policy/lib/run-command.mjs`. A fresh active-only scan after the port - reports only the two native compatibility wrappers. They are not deletion-proof - yet because they are documented human/native entrypoints; removal still - requires a manual owner decision or replacement CI wiring. + reports the two native compatibility wrappers plus the five new Bun + human/readiness entrypoints because Markdown/docs callers are intentionally + ignored in that mode. They are not deletion-proof yet because they are + documented human/native entrypoints; removal still requires a manual owner + decision or replacement CI wiring. - 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 748895cf1a50bdb2f489f44586e8dc6ed6373773 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 17:33:10 +0000 Subject: [PATCH 207/308] chore: keep python bytecode out of source tools --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 5 +++++ tools/policy/check-policy-tools.sh | 4 +++- tools/policy/check-tooling-stack.sh | 19 +++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 705c6d9c..a2d665c6 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -1973,6 +1973,11 @@ until the current-state gates here are checked with fresh local evidence. 6. port `src/extensions/tools/check-extension-model.py` as a separate generator migration, because it is the canonical multi-language extension model and needs generated-output parity across SDKs. +- While those Python entrypoints remain, policy tooling now keeps Python compile + bytecode out of source/tool directories. `check-policy-tools.sh` routes + `py_compile` output through `PYTHONPYCACHEPREFIX` under its temp directory, + and `check-tooling-stack.sh` rejects source-tree `__pycache__` or `.pyc` + artifacts outside build output directories. - 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 diff --git a/tools/policy/check-policy-tools.sh b/tools/policy/check-policy-tools.sh index 455af426..51c25042 100755 --- a/tools/policy/check-policy-tools.sh +++ b/tools/policy/check-policy-tools.sh @@ -42,5 +42,7 @@ while IFS= read -r script; do done < <(find tools/policy -type f -name '*.py' | LC_ALL=C sort) if ((${#python_files[@]} > 0)); then - run python3 -m py_compile "${python_files[@]}" + run env \ + PYTHONPYCACHEPREFIX="$js_check_root/python-pycache" \ + python3 -m py_compile "${python_files[@]}" fi diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 88dfb267..1c241642 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -647,6 +647,25 @@ if git ls-files | fi rm -f /tmp/oliphaunt-generated-grep.$$ +python_bytecode_hits="/tmp/oliphaunt-python-bytecode-grep.$$" +find .github tools src examples \ + \( -path '*/node_modules/*' -o \ + -path '*/target/*' -o \ + -path '*/.gradle/*' -o \ + -path '*/.kotlin/*' -o \ + -path '*/.next/*' -o \ + -path '*/.source/*' -o \ + -path '*/dist/*' -o \ + -path '*/build/*' \) -prune -o \ + \( -type d -name '__pycache__' -o -type f -name '*.pyc' \) -print \ + >"$python_bytecode_hits" +if [ -s "$python_bytecode_hits" ]; then + cat "$python_bytecode_hits" >&2 + rm -f "$python_bytecode_hits" + fail "Python bytecode caches must not be left in source/tool directories; set PYTHONPYCACHEPREFIX or write bytecode under target/" +fi +rm -f "$python_bytecode_hits" + if git ls-files tools/ci tools/product | grep -q .; then git ls-files tools/ci tools/product >&2 fail "retired tools/ci and tools/product entrypoints must not be tracked" From 06d631a2afaadb73f25b9b76034c2bb171152dcf Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 17:36:56 +0000 Subject: [PATCH 208/308] chore: port android disk reclaim helper to bun --- .../reclaim-android-mobile-build-disk.mjs | 50 +++++++++++++++++++ .../reclaim-android-mobile-build-disk.sh | 24 --------- .github/workflows/ci.yml | 2 +- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 4 ++ tools/policy/check-tooling-stack.sh | 5 ++ 5 files changed, 60 insertions(+), 25 deletions(-) create mode 100755 .github/scripts/reclaim-android-mobile-build-disk.mjs delete mode 100644 .github/scripts/reclaim-android-mobile-build-disk.sh diff --git a/.github/scripts/reclaim-android-mobile-build-disk.mjs b/.github/scripts/reclaim-android-mobile-build-disk.mjs new file mode 100755 index 00000000..822b28f6 --- /dev/null +++ b/.github/scripts/reclaim-android-mobile-build-disk.mjs @@ -0,0 +1,50 @@ +#!/usr/bin/env bun +import { existsSync } from "node:fs"; +import process from "node:process"; +import { spawnSync } from "node:child_process"; + +const WORKSPACE = process.env.GITHUB_WORKSPACE || "."; + +function fail(message) { + console.error(`reclaim-android-mobile-build-disk.mjs: ${message}`); + process.exit(1); +} + +function run(command, args) { + const result = spawnSync(command, args, { stdio: "inherit" }); + if (result.error) { + fail(result.error.message); + } + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + +if (process.env.RUNNER_OS !== "Linux") { + process.exit(0); +} + +console.log("Disk before Android mobile cleanup:"); +run("df", ["-h", WORKSPACE]); + +run("sudo", [ + "rm", + "-rf", + "/opt/ghc", + "/opt/hostedtoolcache/CodeQL", + "/usr/local/share/boost", + "/usr/share/dotnet", +]); + +const androidHome = process.env.ANDROID_HOME; +if (androidHome && existsSync(androidHome)) { + run("sudo", [ + "rm", + "-rf", + `${androidHome}/emulator`, + `${androidHome}/system-images`, + ]); +} + +console.log("Disk after Android mobile cleanup:"); +run("df", ["-h", WORKSPACE]); diff --git a/.github/scripts/reclaim-android-mobile-build-disk.sh b/.github/scripts/reclaim-android-mobile-build-disk.sh deleted file mode 100644 index bc8224cf..00000000 --- a/.github/scripts/reclaim-android-mobile-build-disk.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if [[ "${RUNNER_OS:-}" != "Linux" ]]; then - exit 0 -fi - -echo "Disk before Android mobile cleanup:" -df -h "${GITHUB_WORKSPACE:-.}" - -sudo rm -rf \ - /opt/ghc \ - /opt/hostedtoolcache/CodeQL \ - /usr/local/share/boost \ - /usr/share/dotnet - -if [[ -n "${ANDROID_HOME:-}" && -d "$ANDROID_HOME" ]]; then - sudo rm -rf \ - "$ANDROID_HOME/emulator" \ - "$ANDROID_HOME/system-images" -fi - -echo "Disk after Android mobile cleanup:" -df -h "${GITHUB_WORKSPACE:-.}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82332440..09c300be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1561,7 +1561,7 @@ jobs: cache-save-if: ${{ env.RUST_CACHE_SAVE_IF }} - name: Reclaim Android mobile build disk - run: bash .github/scripts/reclaim-android-mobile-build-disk.sh + run: bun .github/scripts/reclaim-android-mobile-build-disk.mjs - name: Download Android liboliphaunt target uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index a2d665c6..21e944fa 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -2070,6 +2070,10 @@ until the current-state gates here are checked with fresh local evidence. ignored in that mode. They are not deletion-proof yet because they are documented human/native entrypoints; removal still requires a manual owner decision or replacement CI wiring. +- The Android mobile CI disk reclamation helper was ported from + `.github/scripts/reclaim-android-mobile-build-disk.sh` to + `.github/scripts/reclaim-android-mobile-build-disk.mjs`; CI now invokes it + through Bun, and `check-tooling-stack.sh` rejects the retired shell entrypoint. - 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-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 1c241642..5dda73fc 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -462,6 +462,11 @@ grep -Fq 'OLIPHAUNT_CI_JOB_TARGETS_JSON' .github/scripts/select-planned-moon-tar fail "planned CI Moon target selector must consume the affected planner target map" grep -Fq 'bun .github/scripts/select-planned-moon-targets.mjs "$job"' .github/scripts/run-planned-moon-job.sh || fail "planned CI Moon helper must delegate target selection to the Bun selector" +grep -Fq 'bun .github/scripts/reclaim-android-mobile-build-disk.mjs' .github/workflows/ci.yml || + fail "Android mobile disk reclaim step must use the Bun CI helper" +if [ -e .github/scripts/reclaim-android-mobile-build-disk.sh ]; then + fail "Android mobile disk reclaim helper must not use the retired shell entrypoint" +fi if grep -Fq 'pnpm moon' .github/scripts/run-moon-targets.sh; then fail "shared CI Moon helper must not launch Moon through pnpm" fi From 065ae0c8a6eb2888e8020f1d57ce27d1b4b8bd09 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 17:48:24 +0000 Subject: [PATCH 209/308] chore: port native ci target wrapper to bun --- .../reclaim-android-mobile-build-disk.mjs | 0 docs/internal/IMPLEMENTATION_CHECKLIST.md | 7 + src/runtimes/liboliphaunt/native/moon.yml | 2 +- .../native/tools/build-ci-target.mjs | 127 ++++++++++++++++++ .../native/tools/build-ci-target.sh | 91 ------------- tools/policy/check-policy-tools.sh | 7 +- tools/policy/check-tooling-stack.sh | 7 + 7 files changed, 148 insertions(+), 93 deletions(-) mode change 100755 => 100644 .github/scripts/reclaim-android-mobile-build-disk.mjs create mode 100755 src/runtimes/liboliphaunt/native/tools/build-ci-target.mjs delete mode 100755 src/runtimes/liboliphaunt/native/tools/build-ci-target.sh diff --git a/.github/scripts/reclaim-android-mobile-build-disk.mjs b/.github/scripts/reclaim-android-mobile-build-disk.mjs old mode 100755 new mode 100644 diff --git a/docs/internal/IMPLEMENTATION_CHECKLIST.md b/docs/internal/IMPLEMENTATION_CHECKLIST.md index fd99087c..bc3192d6 100644 --- a/docs/internal/IMPLEMENTATION_CHECKLIST.md +++ b/docs/internal/IMPLEMENTATION_CHECKLIST.md @@ -1174,6 +1174,13 @@ Run before claiming this architecture complete: a Windows runner. - [x] GitHub required aggregate green. +## Tooling Cleanup Progress + +- [x] Native mobile CI target staging now uses the Bun + `build-ci-target.mjs` wrapper from Moon. The retired shell wrapper is blocked + by `tools/policy/check-tooling-stack.sh`, while the product-owned native + build scripts remain in their existing platform shell/PowerShell lanes. + ## Immediate Next Work 1. Run a release dry-run after release tags/artifacts are available for the diff --git a/src/runtimes/liboliphaunt/native/moon.yml b/src/runtimes/liboliphaunt/native/moon.yml index a0578585..d2e7dac5 100644 --- a/src/runtimes/liboliphaunt/native/moon.yml +++ b/src/runtimes/liboliphaunt/native/moon.yml @@ -147,7 +147,7 @@ tasks: - "liboliphaunt-native:check" - "extension-runtime-contract:check" - "source-inputs:source-fetch-native-runtime" - command: "bash -c 'src/runtimes/liboliphaunt/native/tools/build-ci-target.sh \"$OLIPHAUNT_CI_TARGET\"'" + command: "bun src/runtimes/liboliphaunt/native/tools/build-ci-target.mjs \"$OLIPHAUNT_CI_TARGET\"" inputs: - "/src/postgres/versions/18/**/*" - "/src/sources/third-party/shared/**/*" diff --git a/src/runtimes/liboliphaunt/native/tools/build-ci-target.mjs b/src/runtimes/liboliphaunt/native/tools/build-ci-target.mjs new file mode 100755 index 00000000..8994db0e --- /dev/null +++ b/src/runtimes/liboliphaunt/native/tools/build-ci-target.mjs @@ -0,0 +1,127 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, rmSync } from "node:fs"; +import path from "node:path"; +import process from "node:process"; + +const PREFIX = "build-ci-target.mjs"; +const TARGETS = new Set(["android-arm64-v8a", "android-x86_64", "ios-xcframework"]); + +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", "pipe"], + }); + if (result.error) { + fail(`${PREFIX}: ${result.error.message}`); + } + if (result.status !== 0 || result.stdout.trim() === "") { + fail("must run inside the Oliphaunt git checkout"); + } + return result.stdout.trim(); +} + +function formatArg(arg) { + return /^[A-Za-z0-9_./:=+-]+$/.test(arg) ? arg : JSON.stringify(arg); +} + +function run(command, args = [], { env = {} } = {}) { + const envArgs = Object.entries(env).map(([key, value]) => `${key}=${formatArg(value)}`); + console.log(`\n==> ${[...envArgs, command, ...args].map(formatArg).join(" ")}`); + const result = spawnSync(command, args, { + stdio: "inherit", + env: { ...process.env, ...env }, + }); + if (result.error) { + fail(`${PREFIX}: ${result.error.message}`); + } + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + +function stagePath(root, stageRoot, source) { + const absoluteSource = path.resolve(source); + const relative = path.relative(root, absoluteSource); + if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) { + fail(`refusing to stage path outside repository: ${source}`); + } + if (!existsSync(absoluteSource)) { + fail(`missing CI target artifact input: ${absoluteSource}`); + } + const destination = path.join(stageRoot, relative); + mkdirSync(path.dirname(destination), { recursive: true }); + run("rsync", ["-a", "--delete", `${absoluteSource}/`, `${destination}/`]); +} + +function buildLinuxRuntimeAssets() { + run("src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh", ["--runtime-only"]); +} + +function buildMacosRuntimeAssets() { + run("src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh", ["--runtime-only"], { + env: { OLIPHAUNT_BUILD_EXTENSIONS: process.env.OLIPHAUNT_BUILD_EXTENSIONS ?? "0" }, + }); +} + +const root = repoRoot(); +process.chdir(root); + +const target = process.argv[2] ?? ""; +if (!TARGETS.has(target)) { + fail( + "usage: src/runtimes/liboliphaunt/native/tools/build-ci-target.mjs [android-arm64-v8a|android-x86_64|ios-xcframework]", + 2, + ); +} + +const mobileExtensions = + process.env.OLIPHAUNT_CI_MOBILE_EXTENSIONS ?? process.env.OLIPHAUNT_MOBILE_STATIC_EXTENSIONS ?? ""; +if (mobileExtensions !== "") { + fail( + "base liboliphaunt CI target builds do not accept selected extensions; publish exact extension artifacts through the extension artifact lane", + 2, + ); +} + +const stageRoot = path.join(root, "target/liboliphaunt-native-ci", target); +rmSync(stageRoot, { recursive: true, force: true }); +mkdirSync(stageRoot, { recursive: true }); + +run("bun", ["tools/policy/fetch-sources.mjs", "native-runtime"]); + +if (target === "android-arm64-v8a") { + run("src/runtimes/liboliphaunt/native/bin/build-postgres18-android-arm64.sh", [], { + env: { + OLIPHAUNT_ANDROID_ABI: "arm64-v8a", + OLIPHAUNT_ANDROID_ARM64_ROOT: path.join(root, "target/liboliphaunt-pg18-android-arm64"), + }, + }); + buildLinuxRuntimeAssets(); + stagePath(root, stageRoot, path.join(root, "target/liboliphaunt-pg18-android-arm64/out")); + stagePath(root, stageRoot, path.join(root, "target/liboliphaunt-pg18-linux-x64-gnu/install")); +} else if (target === "android-x86_64") { + run("src/runtimes/liboliphaunt/native/bin/build-postgres18-android-x86_64.sh", [], { + env: { + OLIPHAUNT_ANDROID_ABI: "x86_64", + OLIPHAUNT_ANDROID_X86_64_ROOT: path.join(root, "target/liboliphaunt-pg18-android-x86_64"), + }, + }); + buildLinuxRuntimeAssets(); + stagePath(root, stageRoot, path.join(root, "target/liboliphaunt-pg18-android-x86_64/out")); + stagePath(root, stageRoot, path.join(root, "target/liboliphaunt-pg18-linux-x64-gnu/install")); +} else if (target === "ios-xcframework") { + run("src/runtimes/liboliphaunt/native/bin/build-ios-xcframework.sh"); + buildMacosRuntimeAssets(); + stagePath(root, stageRoot, path.join(root, "target/liboliphaunt-ios-xcframework/out")); + stagePath(root, stageRoot, path.join(root, "target/liboliphaunt-ios-simulator/out")); + stagePath(root, stageRoot, path.join(root, "target/liboliphaunt-ios-device/out")); + stagePath(root, stageRoot, path.join(root, "target/liboliphaunt-pg18/install")); +} + +console.log(`\nStaged liboliphaunt CI target artifact: ${stageRoot}`); diff --git a/src/runtimes/liboliphaunt/native/tools/build-ci-target.sh b/src/runtimes/liboliphaunt/native/tools/build-ci-target.sh deleted file mode 100755 index a98a345a..00000000 --- a/src/runtimes/liboliphaunt/native/tools/build-ci-target.sh +++ /dev/null @@ -1,91 +0,0 @@ -#!/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" - -target="${1:-}" -case "$target" in - android-arm64-v8a|android-x86_64|ios-xcframework) - ;; - *) - echo "usage: src/runtimes/liboliphaunt/native/tools/build-ci-target.sh [android-arm64-v8a|android-x86_64|ios-xcframework]" >&2 - exit 2 - ;; -esac - -stage_root="$root/target/liboliphaunt-native-ci/$target" -mobile_extensions="${OLIPHAUNT_CI_MOBILE_EXTENSIONS:-${OLIPHAUNT_MOBILE_STATIC_EXTENSIONS:-}}" -if [ -n "$mobile_extensions" ]; then - echo "base liboliphaunt CI target builds do not accept selected extensions; publish exact extension artifacts through the extension artifact lane" >&2 - exit 2 -fi - -run() { - printf '\n==> %s\n' "$*" - "$@" -} - -stage_path() { - local source="$1" - local relative="${source#$root/}" - [ "$relative" != "$source" ] || { - echo "refusing to stage path outside repository: $source" >&2 - exit 1 - } - [ -e "$source" ] || { - echo "missing CI target artifact input: $source" >&2 - exit 1 - } - mkdir -p "$stage_root/$(dirname "$relative")" - rsync -a --delete "$source/" "$stage_root/$relative/" -} - -build_linux_runtime_assets() { - run src/runtimes/liboliphaunt/native/bin/build-postgres18-linux.sh --runtime-only -} - -build_macos_runtime_assets() { - run env \ - OLIPHAUNT_BUILD_EXTENSIONS="${OLIPHAUNT_BUILD_EXTENSIONS:-0}" \ - src/runtimes/liboliphaunt/native/bin/build-postgres18-macos.sh --runtime-only -} - -rm -rf "$stage_root" -mkdir -p "$stage_root" - -run bun tools/policy/fetch-sources.mjs native-runtime - -case "$target" in - android-arm64-v8a) - run env \ - OLIPHAUNT_ANDROID_ABI=arm64-v8a \ - OLIPHAUNT_ANDROID_ARM64_ROOT="$root/target/liboliphaunt-pg18-android-arm64" \ - src/runtimes/liboliphaunt/native/bin/build-postgres18-android-arm64.sh - build_linux_runtime_assets - stage_path "$root/target/liboliphaunt-pg18-android-arm64/out" - stage_path "$root/target/liboliphaunt-pg18-linux-x64-gnu/install" - ;; - android-x86_64) - run env \ - OLIPHAUNT_ANDROID_ABI=x86_64 \ - OLIPHAUNT_ANDROID_X86_64_ROOT="$root/target/liboliphaunt-pg18-android-x86_64" \ - src/runtimes/liboliphaunt/native/bin/build-postgres18-android-x86_64.sh - build_linux_runtime_assets - stage_path "$root/target/liboliphaunt-pg18-android-x86_64/out" - stage_path "$root/target/liboliphaunt-pg18-linux-x64-gnu/install" - ;; - ios-xcframework) - run src/runtimes/liboliphaunt/native/bin/build-ios-xcframework.sh - build_macos_runtime_assets - stage_path "$root/target/liboliphaunt-ios-xcframework/out" - stage_path "$root/target/liboliphaunt-ios-simulator/out" - stage_path "$root/target/liboliphaunt-ios-device/out" - stage_path "$root/target/liboliphaunt-pg18/install" - ;; -esac - -printf '\nStaged liboliphaunt CI target artifact: %s\n' "$stage_root" diff --git a/tools/policy/check-policy-tools.sh b/tools/policy/check-policy-tools.sh index 51c25042..d0aec5ba 100755 --- a/tools/policy/check-policy-tools.sh +++ b/tools/policy/check-policy-tools.sh @@ -34,7 +34,12 @@ 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 .github/scripts examples/tools tools/policy tools/graph -type f -name '*.mjs' | LC_ALL=C sort) +done < <( + { + find .github/scripts examples/tools tools/policy tools/graph -type f -name '*.mjs' + printf '%s\n' src/runtimes/liboliphaunt/native/tools/build-ci-target.mjs + } | LC_ALL=C sort +) python_files=() while IFS= read -r script; do diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 5dda73fc..435bd078 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -52,6 +52,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/runtimes/liboliphaunt/native/tools/build-ci-target.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 @@ -217,6 +218,12 @@ 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" +grep -Fq 'command: "bun src/runtimes/liboliphaunt/native/tools/build-ci-target.mjs' src/runtimes/liboliphaunt/native/moon.yml && + grep -Fq 'OLIPHAUNT_CI_TARGET' src/runtimes/liboliphaunt/native/moon.yml || + fail "native CI target release task must use the Bun build-ci-target wrapper" +if [ -e src/runtimes/liboliphaunt/native/tools/build-ci-target.sh ]; then + fail "native CI target wrapper must not use the retired shell 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 770baff3d7c6dc9779a339f7120894feebaf6a33 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 17:52:49 +0000 Subject: [PATCH 210/308] chore: retire native smoke compatibility wrappers --- docs/internal/DONE.md | 19 ++++++++++--------- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 16 ++++++++-------- src/runtimes/liboliphaunt/native/README.md | 9 ++++----- .../native/bin/check-c-abi-conformance.sh | 9 --------- .../native/bin/smoke-macos-happy-path.sh | 5 ----- tools/policy/check-repo-structure.sh | 4 +++- 6 files changed, 25 insertions(+), 37 deletions(-) delete mode 100755 src/runtimes/liboliphaunt/native/bin/check-c-abi-conformance.sh delete mode 100755 src/runtimes/liboliphaunt/native/bin/smoke-macos-happy-path.sh diff --git a/docs/internal/DONE.md b/docs/internal/DONE.md index f8cbf801..740490cc 100644 --- a/docs/internal/DONE.md +++ b/docs/internal/DONE.md @@ -1243,12 +1243,13 @@ links against only `src/runtimes/liboliphaunt/native/include/oliphaunt.h`: - `oliphaunt/smoke/liboliphaunt_abi_conformance.c` verifies ABI/version constants, capability bits, public struct field types, exported function prototypes, and safe global/no-handle calls without including PostgreSQL server headers; -- `src/runtimes/liboliphaunt/native/bin/check-c-abi-conformance.sh` builds the conformance program - with strict C11 warnings and links it to the current `liboliphaunt.dylib`; -- `src/runtimes/liboliphaunt/native/tools/check-track.sh quick` now runs that conformance check - before the heavier native happy-path smoke, so C ABI drift fails in the fast - native lane before Rust, Swift, Kotlin, or React Native bindings trust the - runtime. +- `src/runtimes/liboliphaunt/native/tools/run-host-c-smoke.mjs --abi-only` + builds the conformance program with strict C11 warnings and links it to the + current `liboliphaunt` shared library; +- `src/runtimes/liboliphaunt/native/tools/check-track.sh quick` now runs that + conformance check before the heavier native happy-path smoke, so C ABI drift + fails in the fast native lane before Rust, Swift, Kotlin, or React Native + bindings trust the runtime. ## Direct Streaming Cancellation Regression @@ -1730,9 +1731,9 @@ PostgreSQL artifact lane exists: no `PG_VERSION`; - macOS keeps the direct `initdb` tooling fallback, so desktop smoke and local native iteration continue to work from an empty PGDATA root; -- `src/runtimes/liboliphaunt/native/bin/check-c-abi-conformance.sh` now performs a fast iOS simulator - syntax check over the liboliphaunt C shim files, catching forbidden mobile C - APIs without rebuilding PostgreSQL for iOS. +- `src/runtimes/liboliphaunt/native/tools/run-host-c-smoke.mjs --abi-only` now + performs a fast iOS simulator syntax check over the liboliphaunt C shim files, + catching forbidden mobile C APIs without rebuilding PostgreSQL for iOS. ## React Native Chunked JSI Streaming diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 21e944fa..ca6c7dd5 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -2059,17 +2059,17 @@ until the current-state gates here are checked with fresh local evidence. `src/runtimes/liboliphaunt/native/bin/smoke-macos-happy-path.sh`, `tools/dev/install-hooks.sh`, and four policy readiness helpers (`check-feature-powerset.sh`, `check-rust-lint.sh`, `check-semver.sh`, - `check-supply-chain.sh`). The developer-hook installer and the four policy - readiness helpers were then ported to Bun entrypoints + `check-supply-chain.sh`). The native wrapper pair was then retired in favor + of the canonical `tools/run-host-c-smoke.mjs --abi-only` and + `bin/smoke-host-happy-path.sh` entrypoints, with repo-structure guards + blocking the compatibility names from returning. The developer-hook installer + and the four policy readiness helpers were ported to Bun entrypoints (`install-hooks.mjs`, `check-feature-powerset.mjs`, `check-rust-lint.mjs`, `check-semver.mjs`, and `check-supply-chain.mjs`) while preserving their command semantics, with the policy wrappers sharing - `tools/policy/lib/run-command.mjs`. A fresh active-only scan after the port - reports the two native compatibility wrappers plus the five new Bun - human/readiness entrypoints because Markdown/docs callers are intentionally - ignored in that mode. They are not deletion-proof yet because they are - documented human/native entrypoints; removal still requires a manual owner - decision or replacement CI wiring. + `tools/policy/lib/run-command.mjs`. A fresh active-only scan after these + changes still reports the five new Bun human/readiness entrypoints because + Markdown/docs callers are intentionally ignored in that mode. - The Android mobile CI disk reclamation helper was ported from `.github/scripts/reclaim-android-mobile-build-disk.sh` to `.github/scripts/reclaim-android-mobile-build-disk.mjs`; CI now invokes it diff --git a/src/runtimes/liboliphaunt/native/README.md b/src/runtimes/liboliphaunt/native/README.md index 9bfdd6b9..40f81faa 100644 --- a/src/runtimes/liboliphaunt/native/README.md +++ b/src/runtimes/liboliphaunt/native/README.md @@ -43,12 +43,11 @@ should bind to the same C ABI instead of reaching into PostgreSQL internals. - `bin/build-external-pgrx-extensions-macos.sh`: opt-in pgrx artifact harness for SDK-known external extension candidates, producing both normal server modules and liboliphaunt-linked embedded modules. -- `bin/check-c-abi-conformance.sh`: consumer-style C ABI check that includes - only `oliphaunt.h`, links the public dylib, and verifies stable constants, - structs, exported symbols, and safe global calls. +- `tools/run-host-c-smoke.mjs --abi-only`: consumer-style C ABI check that + includes only `oliphaunt.h`, links the public dylib, and verifies stable + constants, structs, exported symbols, and safe global calls. - `bin/smoke-host-happy-path.sh`: host C ABI smoke harness for macOS, Linux, - and Windows. `bin/smoke-macos-happy-path.sh` remains as a compatibility - wrapper. + and Windows. ## Build diff --git a/src/runtimes/liboliphaunt/native/bin/check-c-abi-conformance.sh b/src/runtimes/liboliphaunt/native/bin/check-c-abi-conformance.sh deleted file mode 100755 index f12e0c2f..00000000 --- a/src/runtimes/liboliphaunt/native/bin/check-c-abi-conformance.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env sh -set -eu - -script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" -. "$script_dir/common.sh" -repo_root="$(oliphaunt_resolve_repo_root "$script_dir")" -cd "$repo_root" - -node src/runtimes/liboliphaunt/native/tools/run-host-c-smoke.mjs --abi-only diff --git a/src/runtimes/liboliphaunt/native/bin/smoke-macos-happy-path.sh b/src/runtimes/liboliphaunt/native/bin/smoke-macos-happy-path.sh deleted file mode 100755 index 7691ef5c..00000000 --- a/src/runtimes/liboliphaunt/native/bin/smoke-macos-happy-path.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env sh -set -eu - -script_dir="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" -exec "$script_dir/smoke-host-happy-path.sh" "$@" diff --git a/tools/policy/check-repo-structure.sh b/tools/policy/check-repo-structure.sh index 93444429..59729dc3 100755 --- a/tools/policy/check-repo-structure.sh +++ b/tools/policy/check-repo-structure.sh @@ -117,7 +117,9 @@ done for path in \ tools/dev/smoke-react-native-expo-android.sh \ tools/dev/smoke-react-native-expo-ios.sh \ - tools/dev/mobile-extension-runtime.sh + tools/dev/mobile-extension-runtime.sh \ + src/runtimes/liboliphaunt/native/bin/check-c-abi-conformance.sh \ + src/runtimes/liboliphaunt/native/bin/smoke-macos-happy-path.sh do reject_path "$path" done From 23c460147fe147d9246080453bf187ee1394ca32 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 18:01:35 +0000 Subject: [PATCH 211/308] chore: port release artifact downloader to bun --- .github/scripts/download-build-artifacts.mjs | 293 ++++++++++++++++++ .github/scripts/download-build-artifacts.sh | 192 ------------ .github/workflows/release.yml | 12 +- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 2 +- tools/policy/check-release-policy.py | 20 +- tools/policy/check-repo-structure.sh | 4 +- tools/release/check_artifact_targets.py | 8 +- 7 files changed, 317 insertions(+), 214 deletions(-) create mode 100644 .github/scripts/download-build-artifacts.mjs delete mode 100755 .github/scripts/download-build-artifacts.sh diff --git a/.github/scripts/download-build-artifacts.mjs b/.github/scripts/download-build-artifacts.mjs new file mode 100644 index 00000000..7580e5ed --- /dev/null +++ b/.github/scripts/download-build-artifacts.mjs @@ -0,0 +1,293 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { + chmodSync, + copyFileSync, + createReadStream, + existsSync, + mkdirSync, + mkdtempSync, + readdirSync, + rmSync, + statSync, + utimesSync, +} from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import process from "node:process"; + +const USAGE = + "usage: download-build-artifacts.mjs [--run-id ] [--job ] --artifact [--artifact ...]"; + +function fail(message, code = 1) { + console.error(message); + process.exit(code); +} + +function parseArgs(argv) { + if (argv.length < 3) { + fail(USAGE, 2); + } + const [workflow, sha, destination, ...rest] = argv; + const args = { + workflow, + sha, + destination, + artifacts: [], + requiredJob: "", + selectedRunId: "", + }; + for (let index = 0; index < rest.length; ) { + const arg = rest[index]; + if (arg === "--run-id") { + args.selectedRunId = valueAfter(rest, index, "--run-id requires a run id"); + index += 2; + } else if (arg === "--job") { + args.requiredJob = valueAfter(rest, index, "--job requires a name"); + index += 2; + } else if (arg === "--artifact") { + args.artifacts.push(valueAfter(rest, index, "--artifact requires a name")); + index += 2; + } else { + fail(`unknown argument: ${arg}`, 2); + } + } + if (args.artifacts.length === 0) { + fail("at least one --artifact is required", 2); + } + return args; +} + +function valueAfter(argv, index, message) { + const value = argv[index + 1]; + if (value === undefined || value.startsWith("--")) { + fail(message, 2); + } + return value; +} + +function requireEnv(name) { + const value = process.env[name]; + if (value === undefined || value === "") { + fail(`${name} is required`); + } + return value; +} + +function run(command, args, { capture = false } = {}) { + const result = spawnSync(command, args, { + encoding: "utf8", + stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit", + env: process.env, + }); + if (result.error) { + throw result.error; + } + if (result.status !== 0) { + if (capture) { + process.stderr.write(result.stderr ?? ""); + process.stderr.write(result.stdout ?? ""); + } + throw new Error(`${[command, ...args].join(" ")} exited with status ${result.status}`); + } + return result.stdout ?? ""; +} + +function gh(args, options) { + return run("gh", args, options); +} + +function artifactNames(repo, runId) { + try { + return gh( + [ + "api", + `repos/${repo}/actions/runs/${runId}/artifacts?per_page=100`, + "--paginate", + "--jq", + ".artifacts[].name", + ], + { capture: true }, + ) + .split(/\r?\n/u) + .filter(Boolean); + } catch (error) { + fail(`failed to list artifacts for run ${runId}: ${error.message}`); + } +} + +function artifactPresent(repo, runId, artifact) { + return artifactNames(repo, runId).includes(artifact); +} + +function requiredJobSuccess(repo, runId, requiredJob) { + if (requiredJob === "") { + return true; + } + let data; + try { + data = JSON.parse(gh(["run", "view", runId, "--repo", repo, "--json", "jobs"], { capture: true })); + } catch { + return false; + } + const job = (data.jobs ?? []).find((candidate) => candidate?.name === requiredJob); + return job?.conclusion === "success"; +} + +function candidateRunIds(repo, workflow, sha, requiredJob) { + const runs = JSON.parse( + gh( + [ + "run", + "list", + "--repo", + repo, + "--workflow", + workflow, + "--commit", + sha, + "--limit", + "20", + "--json", + "databaseId,status,conclusion,event,createdAt", + ], + { capture: true }, + ), + ); + return runs + .filter((run) => requiredJob !== "" || (run.status === "completed" && run.conclusion === "success")) + .map((run) => String(run.databaseId)) + .filter(Boolean); +} + +function sortedFiles(root) { + const files = []; + function visit(directory) { + const entries = readdirSync(directory, { withFileTypes: true }).sort((left, right) => + left.name.localeCompare(right.name), + ); + for (const entry of entries) { + const entryPath = path.join(directory, entry.name); + if (entry.isDirectory()) { + visit(entryPath); + } else if (entry.isFile()) { + files.push(entryPath); + } + } + } + visit(root); + return files; +} + +function fileSha256(file) { + return new Promise((resolve, reject) => { + const hash = createHash("sha256"); + const stream = createReadStream(file); + stream.on("error", reject); + stream.on("data", (chunk) => hash.update(chunk)); + stream.on("end", () => resolve(hash.digest("hex"))); + }); +} + +async function filesEqual(left, right) { + const leftStat = statSync(left); + const rightStat = statSync(right); + return leftStat.size === rightStat.size && (await fileSha256(left)) === (await fileSha256(right)); +} + +function copyPreserve(source, target) { + const sourceStat = statSync(source); + copyFileSync(source, target); + chmodSync(target, sourceStat.mode); + utimesSync(target, sourceStat.atime, sourceStat.mtime); +} + +function mergeChecksumManifest(existing, incoming) { + const result = spawnSync("bun", [".github/scripts/merge-checksum-manifest.mjs", existing, incoming], { + stdio: "inherit", + env: process.env, + }); + return !result.error && result.status === 0; +} + +async function mergeDownloadedArtifact(artifact, sourceDir, destination) { + for (const source of sortedFiles(sourceDir)) { + const relativePath = path.relative(sourceDir, source); + const target = path.join(destination, relativePath); + mkdirSync(path.dirname(target), { recursive: true }); + if (existsSync(target)) { + if (statSync(target).isFile() && (await filesEqual(source, target))) { + continue; + } + if ( + statSync(target).isFile() && + statSync(source).isFile() && + path.basename(target).endsWith("-release-assets.sha256") + ) { + if (!mergeChecksumManifest(target, source)) { + return false; + } + continue; + } + console.error(`artifact ${artifact} would overwrite ${relativePath} with different bytes`); + return false; + } + copyPreserve(source, target); + } + return true; +} + +function selectRunId(repo, args) { + if (args.selectedRunId !== "") { + const runId = args.selectedRunId; + if (!requiredJobSuccess(repo, runId, args.requiredJob)) { + fail(`${args.workflow} run ${runId} does not satisfy required job ${args.requiredJob || ""}`); + } + for (const artifact of args.artifacts) { + if (!artifactPresent(repo, runId, artifact)) { + fail(`${args.workflow} run ${runId} is missing required artifact ${artifact}`); + } + } + return runId; + } + + for (const candidate of candidateRunIds(repo, args.workflow, args.sha, args.requiredJob)) { + if (!requiredJobSuccess(repo, candidate, args.requiredJob)) { + continue; + } + if (args.artifacts.every((artifact) => artifactPresent(repo, candidate, artifact))) { + return candidate; + } + } + fail( + `no ${args.workflow} workflow run found for ${args.sha} with required job/artifacts: ${args.requiredJob || ""} / ${args.artifacts.join(" ")}`, + ); +} + +async function main() { + const args = parseArgs(Bun.argv.slice(2)); + requireEnv("GH_TOKEN"); + const repo = requireEnv("GH_REPO"); + const runId = selectRunId(repo, args); + mkdirSync(args.destination, { recursive: true }); + + for (const artifact of args.artifacts) { + console.log(`Downloading ${args.workflow} artifact ${artifact} from run ${runId}`); + const artifactDir = mkdtempSync(path.join(os.tmpdir(), "oliphaunt-artifact-")); + try { + gh(["run", "download", runId, "--repo", repo, "--name", artifact, "--dir", artifactDir]); + if (!(await mergeDownloadedArtifact(artifact, artifactDir, args.destination))) { + process.exit(1); + } + } finally { + rmSync(artifactDir, { recursive: true, force: true }); + } + } +} + +try { + await main(); +} catch (error) { + fail(error instanceof Error ? error.message : String(error)); +} diff --git a/.github/scripts/download-build-artifacts.sh b/.github/scripts/download-build-artifacts.sh deleted file mode 100755 index 669871fc..00000000 --- a/.github/scripts/download-build-artifacts.sh +++ /dev/null @@ -1,192 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -workflow="${1:?usage: download-build-artifacts.sh [--run-id ] [--job ] --artifact [--artifact ...]}" -sha="${2:?usage: download-build-artifacts.sh [--run-id ] [--job ] --artifact [--artifact ...]}" -destination="${3:?usage: download-build-artifacts.sh [--run-id ] [--job ] --artifact [--artifact ...]}" -shift 3 - -artifacts=() -required_job="" -selected_run_id="" -while [[ $# -gt 0 ]]; do - case "$1" in - --run-id) - selected_run_id="${2:?--run-id requires a run id}" - shift 2 - ;; - --job) - required_job="${2:?--job requires a name}" - shift 2 - ;; - --artifact) - artifacts+=("${2:?--artifact requires a name}") - shift 2 - ;; - *) - echo "unknown argument: $1" >&2 - exit 2 - ;; - esac -done - -if [[ "${#artifacts[@]}" -eq 0 ]]; then - echo "at least one --artifact is required" >&2 - exit 2 -fi - -: "${GH_TOKEN:?GH_TOKEN is required}" -: "${GH_REPO:?GH_REPO is required}" - -artifact_present() { - local run_id="$1" - local artifact="$2" - - local artifact_names - artifact_names="$( - gh api "repos/$GH_REPO/actions/runs/$run_id/artifacts?per_page=100" \ - --paginate \ - --jq '.artifacts[].name' - )" || { - echo "failed to list artifacts for $workflow run $run_id" >&2 - exit 1 - } - printf '%s\n' "$artifact_names" | - grep -Fxq -- "$artifact" -} - -merge_checksum_manifest() { - local existing="$1" - local incoming="$2" - bun .github/scripts/merge-checksum-manifest.mjs "$existing" "$incoming" -} - -merge_downloaded_artifact() { - local artifact="$1" - local source_dir="$2" - - local source - while IFS= read -r source; do - [[ -n "$source" ]] || continue - local relative_path="${source#"$source_dir"/}" - local target="$destination/$relative_path" - mkdir -p "$(dirname "$target")" - if [[ -e "$target" ]]; then - if [[ -f "$target" ]] && cmp -s "$source" "$target"; then - continue - fi - if [[ -f "$target" && -f "$source" && "$(basename "$target")" == *-release-assets.sha256 ]]; then - if ! merge_checksum_manifest "$target" "$source"; then - return 1 - fi - continue - fi - echo "artifact $artifact would overwrite $relative_path with different bytes" >&2 - return 1 - fi - cp -p "$source" "$target" - done < <(find "$source_dir" -type f -print | sort) -} - -required_job_success() { - local run_id="$1" - if [[ -z "$required_job" ]]; then - return 0 - fi - - local jobs_file - jobs_file="$(mktemp)" - if ! gh run view "$run_id" --repo "$GH_REPO" --json jobs > "$jobs_file"; then - rm -f "$jobs_file" - return 1 - fi - - local conclusion - if ! conclusion="$( - bun -e ' -const fs = require("node:fs"); -const data = JSON.parse(fs.readFileSync(Bun.argv[1], "utf8")); -const required = Bun.argv[2] ?? ""; -const job = (data.jobs ?? []).find((candidate) => candidate?.name === required); -console.log(job?.conclusion ?? ""); -' "$jobs_file" "$required_job" - )"; then - rm -f "$jobs_file" - return 1 - fi - rm -f "$jobs_file" - [[ "$conclusion" == "success" ]] -} - -run_id="$selected_run_id" -if [[ -n "$run_id" ]]; then - if ! required_job_success "$run_id"; then - echo "$workflow run $run_id does not satisfy required job ${required_job:-}" >&2 - exit 1 - fi - for artifact in "${artifacts[@]}"; do - if ! artifact_present "$run_id" "$artifact"; then - echo "$workflow run $run_id is missing required artifact $artifact" >&2 - exit 1 - fi - done -else - while IFS= read -r candidate; do - [[ -n "$candidate" ]] || continue - if ! required_job_success "$candidate"; then - continue - fi - missing=0 - for artifact in "${artifacts[@]}"; do - if ! artifact_present "$candidate" "$artifact"; then - missing=1 - break - fi - done - if [[ "$missing" -eq 0 ]]; then - run_id="$candidate" - break - fi - done < <( - if [[ -n "$required_job" ]]; then - gh run list \ - --repo "$GH_REPO" \ - --workflow "$workflow" \ - --commit "$sha" \ - --limit 20 \ - --json databaseId,status,conclusion,event,createdAt \ - --jq '.[].databaseId' - else - gh run list \ - --repo "$GH_REPO" \ - --workflow "$workflow" \ - --commit "$sha" \ - --limit 20 \ - --json databaseId,status,conclusion,event,createdAt \ - --jq '.[] | select(.status == "completed" and .conclusion == "success") | .databaseId' - fi - ) -fi - -if [[ -z "$run_id" ]]; then - echo "no $workflow workflow run found for $sha with required job/artifacts: ${required_job:-} / ${artifacts[*]}" >&2 - exit 1 -fi - -mkdir -p "$destination" -for artifact in "${artifacts[@]}"; do - echo "Downloading $workflow artifact $artifact from run $run_id" - artifact_dir="$(mktemp -d)" - if ! gh run download "$run_id" \ - --repo "$GH_REPO" \ - --name "$artifact" \ - --dir "$artifact_dir"; then - rm -rf "$artifact_dir" - exit 1 - fi - if ! merge_downloaded_artifact "$artifact" "$artifact_dir"; then - rm -rf "$artifact_dir" - exit 1 - fi - rm -rf "$artifact_dir" -done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a2831361..aaa23866 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -328,7 +328,7 @@ jobs: GH_REPO: ${{ github.repository }} CI_RUN_ID: ${{ steps.ci_build_gate.outputs.run_id }} run: | - .github/scripts/download-build-artifacts.sh \ + bun .github/scripts/download-build-artifacts.mjs \ CI \ "$RELEASE_HEAD_SHA" \ target/oliphaunt-wasix/release-assets \ @@ -343,7 +343,7 @@ jobs: GH_REPO: ${{ github.repository }} CI_RUN_ID: ${{ steps.ci_build_gate.outputs.run_id }} run: | - .github/scripts/download-build-artifacts.sh \ + bun .github/scripts/download-build-artifacts.mjs \ CI \ "$RELEASE_HEAD_SHA" \ target/extension-artifacts \ @@ -365,7 +365,7 @@ jobs: while IFS= read -r artifact; do artifact_args+=(--artifact "$artifact") done < <(tools/dev/bun.sh tools/release/release_graph_query.mjs ci-artifact-names --product "$product" --family sdk-package --format lines) - .github/scripts/download-build-artifacts.sh \ + bun .github/scripts/download-build-artifacts.mjs \ CI \ "$RELEASE_HEAD_SHA" \ "target/sdk-artifacts/$product" \ @@ -384,7 +384,7 @@ jobs: GH_REPO: ${{ github.repository }} CI_RUN_ID: ${{ steps.ci_build_gate.outputs.run_id }} run: | - .github/scripts/download-build-artifacts.sh \ + bun .github/scripts/download-build-artifacts.mjs \ CI \ "$RELEASE_HEAD_SHA" \ target/liboliphaunt/release-assets \ @@ -425,7 +425,7 @@ jobs: while IFS= read -r artifact; do artifact_args+=(--artifact "$artifact") done < <(tools/dev/bun.sh tools/release/release_graph_query.mjs ci-artifact-names --product "$product" --kind "$kind" --family release-assets --format lines) - .github/scripts/download-build-artifacts.sh \ + bun .github/scripts/download-build-artifacts.mjs \ CI \ "$RELEASE_HEAD_SHA" \ "$destination" \ @@ -457,7 +457,7 @@ jobs: while IFS= read -r artifact; do artifact_args+=(--artifact "$artifact") done < <(tools/dev/bun.sh tools/release/release_graph_query.mjs ci-artifact-names --product oliphaunt-node-direct --kind node-direct-addon --family npm-package --format lines) - .github/scripts/download-build-artifacts.sh \ + bun .github/scripts/download-build-artifacts.mjs \ CI \ "$RELEASE_HEAD_SHA" \ target/oliphaunt-node-direct/npm-packages \ diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index ca6c7dd5..9d2f7792 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -1926,7 +1926,7 @@ until the current-state gates here are checked with fresh local evidence. 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 +- `.github/scripts/download-build-artifacts.mjs` 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. diff --git a/tools/policy/check-release-policy.py b/tools/policy/check-release-policy.py index 47e04336..e75203df 100644 --- a/tools/policy/check-release-policy.py +++ b/tools/policy/check-release-policy.py @@ -949,7 +949,7 @@ def check_release_workflow_policy() -> None: if "target/release-assets/native" in publish_block: fail("Release workflow must download native helper artifacts into product-owned release asset roots") - download_calls = list(re.finditer(r"[.]github/scripts/download-build-artifacts[.]sh", publish_block)) + download_calls = list(re.finditer(r"bun [.]github/scripts/download-build-artifacts[.]mjs", publish_block)) if not download_calls: fail("Release workflow must download staged builder artifacts from the CI workflow") for index, call in enumerate(download_calls): @@ -967,18 +967,18 @@ def check_release_workflow_policy() -> None: 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") + build_artifact_script = read_text(".github/scripts/download-build-artifacts.mjs") for snippet in ( "--run-id", - "selected_run_id", - 'required_job_success "$run_id"', - 'artifact_present "$run_id" "$artifact"', - 'actions/runs/$run_id/artifacts?per_page=100', - 'gh run view "$run_id" --repo "$GH_REPO" --json jobs > "$jobs_file"', + "selectedRunId", + "requiredJobSuccess(repo, runId", + "artifactPresent(repo, runId, artifact)", + "actions/runs/${runId}/artifacts?per_page=100", + '"run", "view", runId, "--repo", repo, "--json", "jobs"', "Bun.argv", - "merge_downloaded_artifact", - "merge_checksum_manifest", - "*-release-assets.sha256", + "mergeDownloadedArtifact", + "mergeChecksumManifest", + '-release-assets.sha256"', "would overwrite", ): if snippet not in build_artifact_script: diff --git a/tools/policy/check-repo-structure.sh b/tools/policy/check-repo-structure.sh index 59729dc3..68438828 100755 --- a/tools/policy/check-repo-structure.sh +++ b/tools/policy/check-repo-structure.sh @@ -542,7 +542,9 @@ 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"' +require_text .github/scripts/download-build-artifacts.mjs 'merge-checksum-manifest.mjs' +require_text .github/workflows/release.yml 'bun .github/scripts/download-build-artifacts.mjs' +reject_path .github/scripts/download-build-artifacts.sh 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' diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py index 77300b76..d0a5372a 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -1047,13 +1047,13 @@ def validate_ci_release_artifacts() -> None: "release workflow must not rebuild Node direct assets; it must consume CI artifacts", ) require_text( - ".github/scripts/download-build-artifacts.sh", - "artifact_present", + ".github/scripts/download-build-artifacts.mjs", + "artifactPresent", "shared artifact downloader must select a successful CI run containing every requested artifact", ) require_text( - ".github/scripts/download-build-artifacts.sh", - "required_job_success", + ".github/scripts/download-build-artifacts.mjs", + "requiredJobSuccess", "shared artifact downloader must support the builder-gate handoff when non-builder checks fail", ) require_text( From a8d76d6befa141113b88311dbc96d718f3a1a8fa Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 18:11:33 +0000 Subject: [PATCH 212/308] chore: port wasix runtime artifact downloader to bun --- ...download-wasix-runtime-build-artifacts.mjs | 46 +++++++++++++++++++ .../download-wasix-runtime-build-artifacts.sh | 18 -------- .github/workflows/release.yml | 2 +- docs/internal/DONE.md | 2 +- docs/internal/IMPLEMENTATION_CHECKLIST.md | 2 +- .../policy/assertions/assert-ci-workflows.mjs | 4 +- tools/policy/check-release-policy.py | 6 ++- tools/policy/check-repo-structure.sh | 2 + tools/release/check_artifact_targets.py | 4 +- 9 files changed, 59 insertions(+), 27 deletions(-) create mode 100644 .github/scripts/download-wasix-runtime-build-artifacts.mjs delete mode 100755 .github/scripts/download-wasix-runtime-build-artifacts.sh diff --git a/.github/scripts/download-wasix-runtime-build-artifacts.mjs b/.github/scripts/download-wasix-runtime-build-artifacts.mjs new file mode 100644 index 00000000..2c3db4e9 --- /dev/null +++ b/.github/scripts/download-wasix-runtime-build-artifacts.mjs @@ -0,0 +1,46 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; +import process from "node:process"; + +function fail(message, code = 1) { + console.error(message); + process.exit(code); +} + +function requireEnv(name) { + const value = process.env[name]; + if (value === undefined || value === "") { + fail(`${name} is required`); + } + return value; +} + +function run(command, args) { + const result = spawnSync(command, args, { + stdio: "inherit", + env: process.env, + }); + if (result.error) { + fail(result.error.message); + } + process.exit(result.status ?? 1); +} + +requireEnv("GITHUB_TOKEN"); +const releaseSha = process.env.RELEASE_HEAD_SHA ?? process.env.GITHUB_SHA ?? ""; +if (releaseSha === "") { + fail("RELEASE_HEAD_SHA or GITHUB_SHA is required", 2); +} + +// Installs the portable and AOT WASIX runtime outputs from the selected release +// CI workflow whose artifact builder gate passed. This is a release artifact +// handoff, not a release-time runtime rebuild. +const args = ["run", "-p", "xtask", "--", "assets", "download"]; +if (process.env.CI_RUN_ID) { + args.push("--run-id", process.env.CI_RUN_ID); +} else { + args.push("--sha", releaseSha); +} +args.push("--required-job", "Builds", "--all-targets"); + +run("cargo", args); diff --git a/.github/scripts/download-wasix-runtime-build-artifacts.sh b/.github/scripts/download-wasix-runtime-build-artifacts.sh deleted file mode 100755 index 79de43a9..00000000 --- a/.github/scripts/download-wasix-runtime-build-artifacts.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -: "${GITHUB_TOKEN:?GITHUB_TOKEN is required}" -release_sha="${RELEASE_HEAD_SHA:-${GITHUB_SHA:-}}" -if [[ -z "$release_sha" ]]; then - echo "RELEASE_HEAD_SHA or GITHUB_SHA is required" >&2 - exit 2 -fi - -# Installs the portable and AOT WASIX runtime outputs from the selected release -# CI workflow whose artifact builder gate passed. This is a release artifact -# handoff, not a release-time runtime rebuild. -if [[ -n "${CI_RUN_ID:-}" ]]; then - cargo run -p xtask -- assets download --run-id "$CI_RUN_ID" --required-job Builds --all-targets -else - cargo run -p xtask -- assets download --sha "$release_sha" --required-job Builds --all-targets -fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aaa23866..99321114 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -319,7 +319,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CI_RUN_ID: ${{ steps.ci_build_gate.outputs.run_id }} - run: .github/scripts/download-wasix-runtime-build-artifacts.sh + run: bun .github/scripts/download-wasix-runtime-build-artifacts.mjs - name: Download WASIX release assets if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && steps.release_plan.outputs.product_liboliphaunt_wasix == 'true' }} diff --git a/docs/internal/DONE.md b/docs/internal/DONE.md index 740490cc..181741bc 100644 --- a/docs/internal/DONE.md +++ b/docs/internal/DONE.md @@ -1013,7 +1013,7 @@ Latest local release work: Cargo product validation from the root policy lane. `pnpm moon run liboliphaunt-wasix:smoke` is now the hard runtime gate and requires portable assets plus the host AOT pack; -- `.github/scripts/download-wasix-runtime-build-artifacts.sh` is a thin wrapper +- `.github/scripts/download-wasix-runtime-build-artifacts.mjs` is a thin wrapper over `xtask assets download`; exact-SHA, latest-compatible, host-target, and all-target WASIX runtime artifact downloads share one implementation; - AOT serialization is now owned by a maintainer-only `xtask` feature. The diff --git a/docs/internal/IMPLEMENTATION_CHECKLIST.md b/docs/internal/IMPLEMENTATION_CHECKLIST.md index bc3192d6..dfb4a6f1 100644 --- a/docs/internal/IMPLEMENTATION_CHECKLIST.md +++ b/docs/internal/IMPLEMENTATION_CHECKLIST.md @@ -337,7 +337,7 @@ or CI/build output proves the contract. not shadow earlier complete runs. - [x] WASIX runtime release download filters same-SHA CI runs by the `Builds` job before installing portable/AOT runtime outputs. Evidence: - `.github/scripts/download-wasix-runtime-build-artifacts.sh` invokes + `.github/scripts/download-wasix-runtime-build-artifacts.mjs` invokes `xtask assets download --required-job Builds`, `xtask` verifies the required job conclusion before trying a run, and `tools/release/check_artifact_targets.py` enforces the handoff. diff --git a/tools/policy/assertions/assert-ci-workflows.mjs b/tools/policy/assertions/assert-ci-workflows.mjs index 80b4e6bd..567179c1 100755 --- a/tools/policy/assertions/assert-ci-workflows.mjs +++ b/tools/policy/assertions/assert-ci-workflows.mjs @@ -173,7 +173,7 @@ const mobilePath = '.github/workflows/mobile-e2e.yml'; const releasePath = '.github/workflows/release.yml'; const releaseIntentPath = '.github/scripts/check-release-intent.sh'; const ciSummaryActionPath = '.github/actions/collect-ci-summary/action.yml'; -const wasixDownloadPath = '.github/scripts/download-wasix-runtime-build-artifacts.sh'; +const wasixDownloadPath = '.github/scripts/download-wasix-runtime-build-artifacts.mjs'; const ci = read(ciPath); const ciBlocks = jobBlocks(ciPath); @@ -477,4 +477,4 @@ requireText(releasePath, 'CI_RUN_ID: ${{ steps.ci_build_gate.outputs.run_id }}') requireText(releasePath, '--job Builds'); requireText(wasixDownloadPath, 'CI_RUN_ID'); -requireText(wasixDownloadPath, '--required-job Builds'); +requireText(wasixDownloadPath, 'args.push("--required-job", "Builds", "--all-targets")'); diff --git a/tools/policy/check-release-policy.py b/tools/policy/check-release-policy.py index e75203df..96c3c773 100644 --- a/tools/policy/check-release-policy.py +++ b/tools/policy/check-release-policy.py @@ -1083,10 +1083,12 @@ def check_release_workflow_policy() -> None: if snippet not in release_head_script: fail(f"release commit resolver must pin safe publish-from-commit behavior: missing {snippet!r}") - wasix_download_script = read_text(".github/scripts/download-wasix-runtime-build-artifacts.sh") - for snippet in ("RELEASE_HEAD_SHA", "CI_RUN_ID", '--run-id "$CI_RUN_ID"', "--required-job Builds"): + wasix_download_script = read_text(".github/scripts/download-wasix-runtime-build-artifacts.mjs") + for snippet in ("RELEASE_HEAD_SHA", "CI_RUN_ID", 'args.push("--run-id", process.env.CI_RUN_ID)', "--required-job", "Builds"): if snippet not in wasix_download_script: fail(f"WASIX runtime artifact handoff must consume the selected CI run id: missing {snippet!r}") + if "bun .github/scripts/download-wasix-runtime-build-artifacts.mjs" not in publish_block: + fail("Release workflow must run WASIX runtime artifact handoff through the Bun wrapper") guarded_publish_steps = { "Create release-please target branch", diff --git a/tools/policy/check-repo-structure.sh b/tools/policy/check-repo-structure.sh index 68438828..a8f78851 100755 --- a/tools/policy/check-repo-structure.sh +++ b/tools/policy/check-repo-structure.sh @@ -545,6 +545,8 @@ require_text .github/scripts/run-moon-targets.sh 'exec "$moon_bin" run "$@"' require_text .github/scripts/download-build-artifacts.mjs 'merge-checksum-manifest.mjs' require_text .github/workflows/release.yml 'bun .github/scripts/download-build-artifacts.mjs' reject_path .github/scripts/download-build-artifacts.sh +require_text .github/workflows/release.yml 'bun .github/scripts/download-wasix-runtime-build-artifacts.mjs' +reject_path .github/scripts/download-wasix-runtime-build-artifacts.sh 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' diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py index d0a5372a..02f6e2c0 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -1067,8 +1067,8 @@ def validate_ci_release_artifacts() -> None: "release workflow artifact downloads must select artifacts from a run whose builds job succeeded", ) require_text( - ".github/scripts/download-wasix-runtime-build-artifacts.sh", - "--required-job Builds", + ".github/scripts/download-wasix-runtime-build-artifacts.mjs", + 'args.push("--required-job", "Builds", "--all-targets")', "WASIX runtime artifact handoff must download from a CI run whose builds job succeeded", ) require_text( From e5d417b2ac0ee93d050241e3dffa7c39718d509b Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 18:16:59 +0000 Subject: [PATCH 213/308] chore: allowlist intentional helper entrypoints --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 13 +++- tools/policy/check-tooling-stack.sh | 2 + tools/policy/helper-entrypoints.allowlist | 9 +++ .../list-helper-reference-candidates.mjs | 64 +++++++++++++++++-- 4 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 tools/policy/helper-entrypoints.allowlist diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 9d2f7792..b26946e8 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -2067,9 +2067,16 @@ until the current-state gates here are checked with fresh local evidence. (`install-hooks.mjs`, `check-feature-powerset.mjs`, `check-rust-lint.mjs`, `check-semver.mjs`, and `check-supply-chain.mjs`) while preserving their command semantics, with the policy wrappers sharing - `tools/policy/lib/run-command.mjs`. A fresh active-only scan after these - changes still reports the five new Bun human/readiness entrypoints because - Markdown/docs callers are intentionally ignored in that mode. + `tools/policy/lib/run-command.mjs`. Before the checked allowlist below, a + fresh active-only scan after these changes still reported the five new Bun + human/readiness entrypoints because Markdown/docs callers are intentionally + ignored in that mode. +- Helper dead-code discovery now also has a checked intentional-entrypoint + allowlist at `tools/policy/helper-entrypoints.allowlist`. The default + active-source scan hides known human/readiness entrypoints, while + `--include-allowlisted` still shows them for audit. This keeps the scan useful + for real removal candidates after manual entrypoints have already been + reviewed. - The Android mobile CI disk reclamation helper was ported from `.github/scripts/reclaim-android-mobile-build-disk.sh` to `.github/scripts/reclaim-android-mobile-build-disk.mjs`; CI now invokes it diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 435bd078..0c881858 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -46,6 +46,7 @@ require_file tools/policy/check-python-entrypoints.mjs require_file tools/policy/check-rust-helper-crates.mjs require_file tools/policy/check-sdk-manifest.mjs require_file tools/policy/check-native-boundaries.mjs +require_file tools/policy/helper-entrypoints.allowlist require_file tools/policy/python-entrypoints.allowlist require_file tools/policy/rust-helper-crates.allowlist require_file tools/runtime/preflight.sh @@ -272,6 +273,7 @@ grep -Fq 'install_cargo_tool ripgrep rg "$RIPGREP_VERSION"' tools/dev/bootstrap- bun tools/policy/check-python-entrypoints.mjs bun tools/policy/check-rust-helper-crates.mjs bun tools/policy/check-sdk-manifest.mjs +bun tools/policy/list-helper-reference-candidates.mjs --max-refs 0 --active-only bun tools/policy/list-source-reference-candidates.mjs --max-refs 0 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" diff --git a/tools/policy/helper-entrypoints.allowlist b/tools/policy/helper-entrypoints.allowlist new file mode 100644 index 00000000..82aa394e --- /dev/null +++ b/tools/policy/helper-entrypoints.allowlist @@ -0,0 +1,9 @@ +# Intentional low-reference helper entrypoints. +# Format: pathdomaindecisionrationale +# Keep this list small. Entries are hidden from the default helper dead-code +# scan but visible with --include-allowlisted. +tools/dev/install-hooks.mjs developer-tooling keep-human-entrypoint installs local git hooks on demand and is intentionally invoked by maintainers instead of CI +tools/policy/check-feature-powerset.mjs policy-readiness keep-human-entrypoint manual readiness entrypoint kept for focused feature powerset checks outside the aggregate policy lane +tools/policy/check-rust-lint.mjs policy-readiness keep-human-entrypoint manual readiness entrypoint kept for focused Rust lint checks outside the aggregate policy lane +tools/policy/check-semver.mjs policy-readiness keep-human-entrypoint manual readiness entrypoint kept for focused semver checks outside the aggregate policy lane +tools/policy/check-supply-chain.mjs policy-readiness keep-human-entrypoint manual readiness entrypoint kept for focused supply-chain checks outside the aggregate policy lane diff --git a/tools/policy/list-helper-reference-candidates.mjs b/tools/policy/list-helper-reference-candidates.mjs index 43a246b0..26282ce1 100644 --- a/tools/policy/list-helper-reference-candidates.mjs +++ b/tools/policy/list-helper-reference-candidates.mjs @@ -1,9 +1,10 @@ #!/usr/bin/env bun import { spawnSync } from "node:child_process"; -import { statSync } from "node:fs"; +import { readFileSync, statSync } from "node:fs"; import { basename } from "node:path"; const args = process.argv.slice(2); +const ALLOWLIST = "tools/policy/helper-entrypoints.allowlist"; function fail(message) { console.error(`list-helper-reference-candidates.mjs: ${message}`); @@ -11,7 +12,7 @@ function fail(message) { } function usage() { - console.log(`usage: tools/policy/list-helper-reference-candidates.mjs [--max-refs N] [--active-only] [--json] + console.log(`usage: tools/policy/list-helper-reference-candidates.mjs [--max-refs N] [--active-only] [--include-allowlisted] [--json] Lists tracked shell, Python, and JavaScript helper entrypoints with few textual references. The output is advisory: each candidate still needs manual review @@ -19,12 +20,14 @@ before removal because some entrypoints are intentionally invoked by humans or external tools. Use --active-only to ignore Markdown/docs references and focus on code, CI, and -tooling callers.`); +tooling callers. By default, entries in ${ALLOWLIST} are hidden; pass +--include-allowlisted when auditing intentional human or readiness entrypoints.`); } let maxRefs = 1; let json = false; let activeOnly = false; +let includeAllowlisted = false; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg === "--max-refs") { @@ -39,6 +42,8 @@ for (let index = 0; index < args.length; index += 1) { index += 1; } else if (arg === "--active-only") { activeOnly = true; + } else if (arg === "--include-allowlisted") { + includeAllowlisted = true; } else if (arg === "--json") { json = true; } else if (arg === "--help" || arg === "-h") { @@ -91,6 +96,48 @@ function trackedHelpers() { .sort(); } +function parseAllowlist() { + const entries = new Map(); + const text = readFileSync(ALLOWLIST, "utf8"); + const tracked = new Set(trackedHelpers()); + for (const [index, rawLine] of text.split(/\r?\n/u).entries()) { + const line = rawLine.trimEnd(); + if (!line || line.startsWith("#")) { + continue; + } + const fields = line.split("\t"); + if (fields.length !== 4) { + fail(`${ALLOWLIST}:${index + 1} must use pathdomaindecisionrationale`); + } + const [path, domain, decision, rationale] = fields; + if (path.startsWith("/") || path.includes("..") || !/\.(?:mjs|py|sh)$/u.test(path)) { + fail(`${ALLOWLIST}:${index + 1} is not a repo-relative helper path: ${path}`); + } + if (!tracked.has(path)) { + fail(`${ALLOWLIST}:${index + 1} references an untracked helper: ${path}`); + } + if (!/^[a-z][a-z0-9-]*$/u.test(domain)) { + fail(`${ALLOWLIST}:${index + 1} has invalid domain ${JSON.stringify(domain)}`); + } + if (!/^[a-z][a-z0-9-]*$/u.test(decision)) { + fail(`${ALLOWLIST}:${index + 1} has invalid decision ${JSON.stringify(decision)}`); + } + if (rationale.length < 24) { + fail(`${ALLOWLIST}:${index + 1} needs a concrete rationale`); + } + if (entries.has(path)) { + fail(`${ALLOWLIST}:${index + 1} duplicates ${path}`); + } + entries.set(path, { path, domain, decision, rationale }); + } + const paths = [...entries.keys()]; + const sorted = [...paths].sort(); + if (paths.join("\n") !== sorted.join("\n")) { + fail(`${ALLOWLIST} must be sorted lexicographically`); + } + return entries; +} + function isFile(path) { try { return statSync(path).isFile(); @@ -152,6 +199,7 @@ function strongestSuffixReference(path) { return best; } +const allowlisted = parseAllowlist(); const candidates = trackedHelpers() .map((path) => { const pathReferences = externalReferenceCount(path, path); @@ -160,6 +208,7 @@ const candidates = trackedHelpers() return { path, basename: basename(path), + allowlisted: allowlisted.has(path), pathReferences, basenameReferences, suffixPattern: suffixReference.pattern, @@ -168,6 +217,7 @@ const candidates = trackedHelpers() }) .filter( (candidate) => + (includeAllowlisted || !candidate.allowlisted) && candidate.pathReferences <= maxRefs && candidate.basenameReferences <= maxRefs && candidate.suffixReferences <= maxRefs, @@ -189,15 +239,17 @@ const candidates = trackedHelpers() }); if (json) { - console.log(JSON.stringify({ maxRefs, activeOnly, candidates }, null, 2)); + console.log(JSON.stringify({ maxRefs, activeOnly, includeAllowlisted, candidates }, null, 2)); } else { - console.log(`Low-reference helper candidates (maxRefs=${maxRefs}, activeOnly=${activeOnly}):`); + console.log( + `Low-reference helper candidates (maxRefs=${maxRefs}, activeOnly=${activeOnly}, includeAllowlisted=${includeAllowlisted}):`, + ); if (candidates.length === 0) { console.log(" none"); } for (const candidate of candidates) { console.log( - ` ${candidate.path} pathRefs=${candidate.pathReferences} suffixRefs=${candidate.suffixReferences} basenameRefs=${candidate.basenameReferences}`, + ` ${candidate.path} pathRefs=${candidate.pathReferences} suffixRefs=${candidate.suffixReferences} basenameRefs=${candidate.basenameReferences} allowlisted=${candidate.allowlisted}`, ); } } From f7ef215d24399b7779d0dd49d2185df5a2e2313b Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 18:29:32 +0000 Subject: [PATCH 214/308] chore: move publish coverage to release graph --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 5 ++ tools/release/check_release_metadata.py | 17 +++-- tools/release/product_metadata.py | 56 +++++++++++++++ tools/release/release-graph.mjs | 71 +++++++++++++++++++ tools/release/release.py | 50 +------------ tools/release/release_graph_query.mjs | 26 +++++++ 6 files changed, 173 insertions(+), 52 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index b26946e8..84281dd4 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -2144,6 +2144,11 @@ until the current-state gates here are checked with fresh local evidence. - 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. +- Publish-step-to-registry-target coverage now comes from the Bun release graph + through `release_graph_query.mjs publish-step-target-coverage`. `release.py` + consumes the Python compatibility adapter instead of carrying a duplicate + table, and `check_release_metadata.py` no longer imports the Python release + orchestrator just to compare publish target coverage. - 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 diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 6d9b76ce..273bc61d 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -12,7 +12,6 @@ from typing import NoReturn import product_metadata -import release ROOT = Path(__file__).resolve().parents[2] @@ -299,6 +298,16 @@ def validate_graph_files() -> None: or 'products = source.get("products")' in product_metadata_source ): fail("product config metadata must be adapted through the Bun release graph product-configs query") + release_source = read_text("tools/release/release.py") + if ( + "publish-step-target-coverage [--product PRODUCT]" not in release_graph_query + or "export function publishStepTargetCoverageRows(" not in release_graph_source + or "product_metadata.publish_step_target_coverage(product)" not in release_source + or "product_metadata.supported_publish_targets(product)" not in release_source + or '"liboliphaunt-native": {' in release_source + or 'return {"github-release-assets": {"github-release-assets"}' in release_source + ): + fail("publish target coverage must be shared through the Bun release graph query instead of duplicated in release.py") if ( '"moon-release-metadata"' not in product_metadata_source or "moon-release-metadata [--product PRODUCT]" not in release_graph_query @@ -461,14 +470,14 @@ def validate_publish_target_coverage() -> None: saw_extension = False for product, config in product_metadata.graph_products().items(): declared = set(product_metadata.string_list(config, "publish_targets", product)) - supported = release.supported_publish_targets(product) + supported = product_metadata.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): + step_coverage = product_metadata.publish_step_target_coverage(product) + if product_metadata.is_extension_product(product): saw_extension = True continue for step in step_coverage: diff --git a/tools/release/product_metadata.py b/tools/release/product_metadata.py index c1742a8c..6c1b593c 100644 --- a/tools/release/product_metadata.py +++ b/tools/release/product_metadata.py @@ -89,6 +89,62 @@ def moon_release_metadata(product: str) -> dict[str, Any]: return row +@lru_cache(maxsize=None) +def _publish_step_target_coverage_rows(product: str | None = None) -> tuple[dict[str, Any], ...]: + args = () if product is None else ("--product", product) + rows = _release_graph_query_rows("publish-step-target-coverage", args) + seen: set[tuple[str, str]] = set() + parsed: list[dict[str, Any]] = [] + for row in rows: + product_id = row.get("product") + step = row.get("step") + publish_targets = row.get("publishTargets") + extension = row.get("extension") + if not isinstance(product_id, str) or not product_id: + fail("release graph publish-step-target-coverage rows must declare a non-empty product") + if product is not None and product_id != product: + fail(f"release graph publish-step-target-coverage returned row for {product_id}, expected {product}") + if not isinstance(step, str) or not step: + fail(f"release graph publish-step-target-coverage {product_id}.step must be a non-empty string") + if not isinstance(publish_targets, list) or not publish_targets or not all( + isinstance(item, str) and item for item in publish_targets + ): + fail(f"release graph publish-step-target-coverage {product_id}.{step}.publishTargets must be a non-empty string list") + if not isinstance(extension, bool): + fail(f"release graph publish-step-target-coverage {product_id}.{step}.extension must be true or false") + key = (product_id, step) + if key in seen: + fail(f"release graph publish-step-target-coverage returned duplicate row for {product_id}.{step}") + seen.add(key) + parsed.append(dict(row)) + return tuple(parsed) + + +def publish_step_target_coverage(product: str) -> dict[str, set[str]]: + coverage: dict[str, set[str]] = {} + for row in _publish_step_target_coverage_rows(product): + step = row["step"] + publish_targets = row["publishTargets"] + assert isinstance(step, str) + assert isinstance(publish_targets, list) + coverage[step] = set(publish_targets) + return coverage + + +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 is_extension_product(product: str) -> bool: + rows = _publish_step_target_coverage_rows(product) + if not rows: + return product.startswith("oliphaunt-extension-") + return bool(rows[0].get("extension")) + + def load_graph() -> dict[str, Any]: """Compatibility return value for callers that still accept a graph arg.""" diff --git a/tools/release/release-graph.mjs b/tools/release/release-graph.mjs index c387d165..5d5d0ee0 100644 --- a/tools/release/release-graph.mjs +++ b/tools/release/release-graph.mjs @@ -454,6 +454,77 @@ export function moonProjectRows({ project = undefined } = {}, prefix = "release- }); } +const PUBLISH_STEP_TARGET_COVERAGE = { + "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": ["jsr", "npm"], + }, + "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"], + }, +}; + +const EXTENSION_PUBLISH_STEP_TARGET_COVERAGE = { + "github-release-assets": ["github-release-assets"], + "maven-central": ["maven-central"], +}; + +export function isExtensionProduct(product) { + return product.startsWith("oliphaunt-extension-"); +} + +export function publishStepTargetCoverageRows({ product = undefined } = {}, prefix = "release-graph") { + const products = loadGraph(prefix).products; + if (product !== undefined && !(product in products)) { + fail(prefix, `unknown release product ${product}`); + } + const productIds = product === undefined ? Object.keys(products).sort(compareText) : [product]; + const rows = []; + for (const productId of productIds) { + const extension = isExtensionProduct(productId); + const coverage = extension ? EXTENSION_PUBLISH_STEP_TARGET_COVERAGE : (PUBLISH_STEP_TARGET_COVERAGE[productId] ?? {}); + for (const [step, publishTargets] of Object.entries(coverage).sort(([left], [right]) => compareText(left, right))) { + rows.push({ + product: productId, + step, + publishTargets: [...publishTargets].sort(compareText), + extension, + }); + } + } + return rows; +} + function assertObject(value, context, prefix) { if (value === null || Array.isArray(value) || typeof value !== "object") { fail(prefix, `${context} must be a table`); diff --git a/tools/release/release.py b/tools/release/release.py index f5ba1676..d1c8d26f 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -481,57 +481,11 @@ def selected_extension_products(products: list[str]) -> list[str]: 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, {}) + return product_metadata.publish_step_target_coverage(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 + return product_metadata.supported_publish_targets(product) def extension_sql_name(product: str) -> str: diff --git a/tools/release/release_graph_query.mjs b/tools/release/release_graph_query.mjs index e6241b1b..1f63a2f8 100644 --- a/tools/release/release_graph_query.mjs +++ b/tools/release/release_graph_query.mjs @@ -23,6 +23,7 @@ import { moonProjectRows, moonReleaseMetadataRows, normalizeFiles, + publishStepTargetCoverageRows, productConfigRows, releaseOrder, releaseProductProjectId, @@ -216,6 +217,28 @@ function runMoonProjects(argv) { printJson(moonProjectRows({ project }, TOOL)); } +function runPublishStepTargetCoverage(argv) { + let product; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === "--product") { + if (index + 1 >= argv.length) { + fail("--product requires a value"); + } + product = argv[index + 1]; + index += 1; + } else if (value.startsWith("--product=")) { + product = value.slice("--product=".length); + } else { + fail(`unknown argument ${value}`); + } + } + if (product !== undefined && product.length === 0) { + fail("--product values must be non-empty"); + } + printJson(publishStepTargetCoverageRows({ product }, TOOL)); +} + function runReleaseOrder(argv) { const graph = loadGraph(TOOL); const selected = assertStringList( @@ -772,6 +795,7 @@ Commands: product-configs [--product PRODUCT] moon-release-metadata [--product PRODUCT] moon-projects [--project PROJECT] + publish-step-target-coverage [--product PRODUCT] release-order --products-json JSON plan [--changed-file PATH...] plans-for-paths --paths-json JSON @@ -806,6 +830,8 @@ function main(argv) { runMoonReleaseMetadata(rest); } else if (command === "moon-projects") { runMoonProjects(rest); + } else if (command === "publish-step-target-coverage") { + runPublishStepTargetCoverage(rest); } else if (command === "release-order") { runReleaseOrder(rest); } else if (command === "plan") { From c1c8f7987db52c5aa2846a9299bedc0d7704c929 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 18:35:15 +0000 Subject: [PATCH 215/308] chore: remove duplicate release metadata readers --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 5 ++++ tools/release/check_release_metadata.py | 23 +------------------ 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 84281dd4..6e472507 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -2149,6 +2149,11 @@ until the current-state gates here are checked with fresh local evidence. consumes the Python compatibility adapter instead of carrying a duplicate table, and `check_release_metadata.py` no longer imports the Python release orchestrator just to compare publish target coverage. +- The release metadata checker no longer carries its own Gradle + `VERSION_NAME` parser or unused Cargo manifest-name reader. Kotlin product + version parsing stays on the Bun `product-versions` query path, and + `check_release_metadata.py` guards that the shared Bun parser still handles + `gradle.properties`. - 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 diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 273bc61d..38a30ef4 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -210,25 +210,6 @@ def cargo_manifest_version(path: str) -> str: return package["version"] -def cargo_manifest_name(path: str) -> str: - manifest = tomllib.loads(read_text(path)) - package = manifest.get("package") - if not isinstance(package, dict) or not isinstance(package.get("name"), str): - fail(f"{path} must declare [package].name") - return package["name"] - - -def gradle_property(path: str, name: str) -> str: - for raw_line in read_text(path).splitlines(): - line = raw_line.strip() - if not line or line.startswith("#") or "=" not in line: - continue - key, value = line.split("=", 1) - if key.strip() == name: - return value.strip() - fail(f"{path} must declare {name}") - - def validate_graph_files() -> None: products = product_metadata.graph_products() for product in products: @@ -287,6 +268,7 @@ def validate_graph_files() -> None: or "product_version_specs(" in product_metadata_source or "release_owned_version_specs(" in product_metadata_source or "import tomllib" in product_metadata_source + or 'property.trim() === "VERSION_NAME"' not in release_artifact_targets ): fail("current product version values must be read through the Bun release graph product-versions query") if ( @@ -896,9 +878,6 @@ def validate_swift(swift_version: str, liboliphaunt_version: str) -> None: def validate_kotlin(kotlin_version: str, liboliphaunt_version: str) -> None: - actual = gradle_property("src/sdks/kotlin/gradle.properties", "VERSION_NAME") - if actual != kotlin_version: - fail("Kotlin VERSION_NAME must match oliphaunt-kotlin product version") plugin_liboliphaunt_version = read_text( "src/sdks/kotlin/oliphaunt-android-gradle-plugin/src/main/resources/dev/oliphaunt/android/liboliphaunt.version" ).strip() From e89867f453e808e0af00e4546d24ea62358f4f24 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 18:46:06 +0000 Subject: [PATCH 216/308] chore: query release policy metadata via bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 6 + tools/policy/check-release-policy.py | 105 ++++++++++++++++-- tools/release/check_release_metadata.py | 3 +- 3 files changed, 103 insertions(+), 11 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 6e472507..832f73f2 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -2324,3 +2324,9 @@ until the current-state gates here are checked with fresh local evidence. 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. +- On 2026-06-27, `check-release-policy.py` stopped importing the Python + `product_metadata.py` compatibility adapter. It now reads product configs, + extension metadata, and artifact targets directly through + `release_graph_query.mjs`, and `check_release_metadata.py` guards that the + policy checker does not reintroduce the adapter while the larger checker + cluster is being ported. diff --git a/tools/policy/check-release-policy.py b/tools/policy/check-release-policy.py index 96c3c773..7544d416 100644 --- a/tools/policy/check-release-policy.py +++ b/tools/policy/check-release-policy.py @@ -5,14 +5,10 @@ import re import pathlib import subprocess -import sys import tomllib ROOT = pathlib.Path(__file__).resolve().parents[2] -sys.path.insert(0, str(ROOT / "tools/release")) - -import product_metadata # noqa: E402 BASE_PRODUCTS = { @@ -256,6 +252,34 @@ def release_product_projects() -> dict[str, str]: return value +def release_product_configs() -> dict[str, dict]: + value = bun_json(["tools/release/release_graph_query.mjs", "product-configs"]) + if not isinstance(value, list) or not all(isinstance(item, dict) for item in value): + fail("release graph product-configs query did not return an object list") + rows: dict[str, dict] = {} + for row in value: + product = row.get("product") + config_id = row.get("id") + if not isinstance(product, str) or not product: + fail("release graph product-configs rows must declare non-empty products") + if product in rows: + fail(f"release graph product-configs query returned duplicate product {product}") + if config_id != product: + fail(f"release graph product-configs {product}.id must match the product id") + for key in ("kind", "owner", "path", "changelog_path", "tag_prefix"): + if not isinstance(row.get(key), str) or not row[key]: + fail(f"release graph product-configs {product}.{key} must be a non-empty string") + for key in ("publish_targets", "release_artifacts", "version_files"): + if not isinstance(row.get(key), list) or not row[key] or not all( + isinstance(item, str) and item for item in row[key] + ): + fail(f"release graph product-configs {product}.{key} must be a non-empty string list") + rows[product] = row + if not rows: + fail("release graph returned no product configs") + return rows + + def moon_project_rows() -> dict[str, dict]: value = bun_json(["tools/release/release_graph_query.mjs", "moon-projects"]) if not isinstance(value, list) or not all(isinstance(item, dict) for item in value): @@ -280,6 +304,65 @@ def moon_project_rows() -> dict[str, dict]: return rows +def extension_metadata_rows() -> dict[str, dict]: + value = bun_json(["tools/release/release_graph_query.mjs", "extension-metadata"]) + if not isinstance(value, list) or not all(isinstance(item, dict) for item in value): + fail("release graph extension-metadata query did not return an object list") + rows: dict[str, dict] = {} + for row in value: + product = row.get("product") + if not isinstance(product, str) or not product: + fail("release graph extension-metadata rows must declare non-empty products") + if product in rows: + fail(f"release graph extension-metadata query returned duplicate product {product}") + for key in ("sqlName", "class", "versioning", "sourcePath"): + if not isinstance(row.get(key), str) or not row[key]: + fail(f"release graph extension-metadata {product}.{key} must be a non-empty string") + compatibility = row.get("compatibility") + if not isinstance(compatibility, dict): + fail(f"release graph extension-metadata {product}.compatibility must be an object") + rows[product] = row + if not rows: + fail("release graph returned no extension metadata rows") + return rows + + +def extension_product_ids() -> list[str]: + return sorted(extension_metadata_rows()) + + +def artifact_target_rows( + *, + product: str, + kind: str, + published_only: bool, +) -> list[dict]: + args = [ + "tools/release/release_graph_query.mjs", + "artifact-targets", + "--product", + product, + "--kind", + kind, + ] + if published_only: + args.append("--published-only") + value = bun_json(args) + if not isinstance(value, list) or not all(isinstance(item, dict) for item in value): + fail("release graph artifact-targets query did not return an object list") + for row in value: + target_id = row.get("id") + if not isinstance(target_id, str) or not target_id: + fail("release graph artifact-targets rows must declare non-empty ids") + if row.get("product") != product or row.get("kind") != kind: + fail(f"release graph artifact-targets returned unexpected row {target_id}") + if not isinstance(row.get("target"), str) or not row["target"]: + fail(f"release graph artifact-targets {target_id}.target must be a non-empty string") + if not isinstance(row.get("extension_artifacts", True), bool): + fail(f"release graph artifact-targets {target_id}.extension_artifacts must be true or false") + return value + + def release_plans_for_single_paths(paths: list[str]) -> dict[str, dict]: value = bun_json( [ @@ -438,10 +521,11 @@ def assert_text_order(text: str, snippets: list[str], message: str) -> None: def check_release_metadata(graph: dict) -> None: - products = product_metadata.graph_products(graph) + products = release_product_configs() if set(products) != expected_products(): fail(f"release product set mismatch: expected {sorted(expected_products())}, got {sorted(products)}") - modeled_extension_products = set(product_metadata.extension_product_ids(graph)) + extension_metadata = extension_metadata_rows() + modeled_extension_products = set(extension_product_ids()) expected_extension_products = expected_extension_products_from_sdk_catalog() if modeled_extension_products != expected_extension_products: fail( @@ -478,7 +562,8 @@ def check_release_metadata(graph: dict) -> None: if release.get("packagePath") != config.get("path"): fail(f"{project_id} packagePath expected {config.get('path')}, got {release.get('packagePath')}") if config.get("kind") == "exact-extension-artifact": - product_metadata.extension_metadata(product, graph) + if product not in extension_metadata: + fail(f"{product} exact-extension product is missing release graph extension metadata") layer = project.get("layer") if layer != "library": fail(f"{project_id} must be a library layer project; exact extension artifacts are publishable runtime-compatible products") @@ -1484,13 +1569,13 @@ def check_ci_builder_planning() -> None: extension_jobs = ci_plan.plan_jobs_for_affected(set(), extension_tasks) full_targets = extension_native_targets(extension_jobs, extension_tasks) expected_full_targets = { - target.target - for target in product_metadata.artifact_targets( + target["target"] + for target in artifact_target_rows( product="liboliphaunt-native", kind="native-runtime", published_only=True, ) - if target.extension_artifacts + if target.get("extension_artifacts", True) } if full_targets != expected_full_targets: fail(f"extension package build must request all supported native extension artifacts, got {sorted(full_targets)}") diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 38a30ef4..87306a3a 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -250,7 +250,8 @@ def validate_graph_files() -> None: or 'config.get("kind") == "exact-extension-artifact"' in product_metadata_source or "extension_products = product_metadata.extension_product_ids()" not in check_artifact_targets or "return set(product_metadata.extension_product_ids())" not in check_consumer_shape - or "modeled_extension_products = set(product_metadata.extension_product_ids(graph))" not in release_policy + or "modeled_extension_products = set(extension_product_ids())" not in release_policy + or "import product_metadata" in release_policy or "function extensionMetadata(" in build_extension_ci_artifacts or "function extensionSourceIdentity(" in build_extension_ci_artifacts or "function extensionMetadata(" in check_staged_artifacts From 277033ff2d9155f8e812565299875dbd73a578fb Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 18:54:55 +0000 Subject: [PATCH 217/308] chore: query artifact targets via bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 6 + tools/release/check_artifact_targets.py | 215 +++++++++++++++--- tools/release/check_release_metadata.py | 5 +- 3 files changed, 198 insertions(+), 28 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 832f73f2..ab67cc6d 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -2330,3 +2330,9 @@ until the current-state gates here are checked with fresh local evidence. `release_graph_query.mjs`, and `check_release_metadata.py` guards that the policy checker does not reintroduce the adapter while the larger checker cluster is being ported. +- On 2026-06-27, `check_artifact_targets.py` also stopped importing + `product_metadata.py`. It now uses small local wrappers over + `release_graph_query.mjs` for artifact targets, extension artifact targets, + SDK package rows, product config paths, Moon release metadata, and current + versions; the release metadata checker now rejects reintroducing the adapter + in the artifact-target checker. diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py index 02f6e2c0..89fe3bca 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -7,11 +7,11 @@ import subprocess import sys import tomllib +from functools import lru_cache from pathlib import Path +from types import SimpleNamespace from typing import NoReturn -import product_metadata - ROOT = Path(__file__).resolve().parents[2] @@ -41,6 +41,169 @@ def bun_json(args: list[str]) -> object: return json.loads(output) +@lru_cache(maxsize=None) +def release_graph_rows(command: str, args: tuple[str, ...] = ()) -> tuple[dict, ...]: + value = bun_json(["tools/release/release_graph_query.mjs", command, *args]) + if not isinstance(value, list) or not all(isinstance(row, dict) for row in value): + fail(f"release graph {command} query did not return an object list") + return tuple(value) + + +def object_row(row: dict) -> SimpleNamespace: + normalized = dict(row) + for key in ( + "triple", + "runner", + "library_relative_path", + "executable_relative_path", + "npm_package", + "npm_os", + "npm_cpu", + "npm_libc", + "llvm_url", + ): + normalized.setdefault(key, None) + normalized.setdefault("extension_artifacts", True) + return SimpleNamespace(**normalized) + + +def artifact_target_args( + *, + product: str | None = None, + kind: str | None = None, + surface: str | None = None, + published_only: bool = False, +) -> tuple[str, ...]: + args: list[str] = [] + if product is not None: + args.extend(["--product", product]) + if kind is not None: + args.extend(["--kind", kind]) + if surface is not None: + args.extend(["--surface", surface]) + if published_only: + args.append("--published-only") + return tuple(args) + + +def artifact_targets( + *, + product: str | None = None, + kind: str | None = None, + surface: str | None = None, + published_only: bool = False, +) -> list[SimpleNamespace]: + return [ + object_row(row) + for row in release_graph_rows( + "artifact-targets", + artifact_target_args( + product=product, + kind=kind, + surface=surface, + published_only=published_only, + ), + ) + ] + + +def raw_artifact_target_tables() -> list[dict]: + return [dict(row) for row in release_graph_rows("raw-artifact-targets")] + + +def legacy_central_artifact_target_rows() -> tuple[dict, ...]: + return release_graph_rows("legacy-central-artifact-targets") + + +def moon_release_metadata(product: str) -> dict: + rows = release_graph_rows("moon-release-metadata", ("--product", product)) + if len(rows) != 1: + fail(f"release graph moon-release-metadata returned {len(rows)} rows for {product}") + row = dict(rows[0]) + row.pop("product", None) + return row + + +def extension_product_ids() -> list[str]: + rows = release_graph_rows("extension-metadata") + products = [] + for row in rows: + product = row.get("product") + if not isinstance(product, str) or not product: + fail("release graph extension-metadata rows must declare non-empty products") + products.append(product) + if len(products) != len(set(products)): + fail("release graph extension-metadata query returned duplicate products") + return sorted(products) + + +def extension_artifact_targets( + *, + product: str | None = None, + family: str | None = None, + published_only: bool = False, +) -> list[SimpleNamespace]: + args: list[str] = [] + if product is not None: + args.extend(["--product", product]) + if family is not None: + args.extend(["--family", family]) + if published_only: + args.append("--published-only") + return [ + object_row(row) + for row in release_graph_rows("extension-targets", tuple(args)) + ] + + +def product_config(product: str) -> dict: + rows = release_graph_rows("product-configs", ("--product", product)) + if len(rows) != 1: + fail(f"release graph product-configs returned {len(rows)} rows for {product}") + return dict(rows[0]) + + +def package_path(product: str) -> Path: + path = product_config(product).get("path") + if not isinstance(path, str) or not path: + fail(f"release graph product-configs {product}.path must be a non-empty string") + return ROOT / path + + +def sdk_package_products() -> list[str]: + products = [] + for row in release_graph_rows("sdk-package-products"): + product = row.get("product") + if not isinstance(product, str) or not product: + fail("release graph sdk-package-products rows must declare non-empty products") + products.append(product) + if len(products) != len(set(products)): + fail("release graph sdk-package-products query returned duplicate products") + return products + + +def ci_sdk_package_artifact_names() -> list[str]: + artifacts = [] + for row in release_graph_rows("sdk-package-products"): + artifact = row.get("artifactName") + if not isinstance(artifact, str) or not artifact: + fail("release graph sdk-package-products rows must declare non-empty artifactName") + artifacts.append(artifact) + if len(artifacts) != len(set(artifacts)): + fail("release graph sdk-package-products query returned duplicate artifacts") + return artifacts + + +def read_current_version(product: str) -> str: + rows = release_graph_rows("product-versions", ("--product", product)) + if len(rows) != 1: + fail(f"release graph product-versions returned {len(rows)} rows for {product}") + version = rows[0].get("version") + if not isinstance(version, str) or not version: + fail(f"release graph product-versions {product}.version must be a non-empty string") + return version + + 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): @@ -81,12 +244,12 @@ def reject_text(path: str, text: str, message: str) -> None: def validate_target_shape() -> None: - targets = product_metadata.artifact_targets() + targets = artifact_targets() if not targets: fail("artifact target metadata must define targets") raw_targets = { raw.get("id"): raw - for raw in product_metadata.raw_artifact_target_tables() + for raw in raw_artifact_target_tables() if isinstance(raw, dict) and isinstance(raw.get("id"), str) } @@ -140,7 +303,7 @@ def validate_target_shape() -> None: def validate_moon_runtime_targets() -> None: - graph_targets = product_metadata.legacy_central_artifact_target_rows() + graph_targets = legacy_central_artifact_target_rows() central_targets = [ raw.get("id") for raw in graph_targets @@ -173,7 +336,7 @@ def validate_moon_runtime_targets() -> None: "oliphaunt-node-direct": "node-direct-addon", } for product, preset in expected_presets.items(): - release = product_metadata.moon_release_metadata(product) + release = moon_release_metadata(product) targets = release.get("artifactTargets") if not isinstance(targets, dict): fail(f"{product} Moon release metadata must declare artifactTargets") @@ -191,13 +354,13 @@ def wasm_extension_target_id(runtime_target: str) -> str: def validate_extension_artifact_targets() -> None: - extension_products = product_metadata.extension_product_ids() + extension_products = extension_product_ids() if not extension_products: fail("exact-extension release products must be modeled as release products") expected_native_targets = { target.target - for target in product_metadata.artifact_targets( + for target in artifact_targets( product="liboliphaunt-native", kind="native-runtime", published_only=True, @@ -206,7 +369,7 @@ def validate_extension_artifact_targets() -> None: } expected_wasix_targets = { wasm_extension_target_id(target.target) - for target in product_metadata.artifact_targets( + for target in artifact_targets( product="liboliphaunt-wasix", published_only=True, ) @@ -218,7 +381,7 @@ def validate_extension_artifact_targets() -> None: fail("published WASIX runtime targets are required before extension artifacts can be published") for product in extension_products: - rows = product_metadata.extension_artifact_targets(product=product) + rows = extension_artifact_targets(product=product) published_native_targets = { target.target for target in rows if target.family == "native" and target.published } @@ -255,7 +418,7 @@ def validate_extension_artifact_targets() -> None: if row.kind != expected_kind: fail(f"{product} {row.target} must use extension artifact kind {expected_kind}, got {row.kind}") if row.published and row.kind == "native-static-registry": - static_recipe = ROOT / product_metadata.package_path(product) / "targets" / "native-static-registry.toml" + static_recipe = package_path(product) / "targets" / "native-static-registry.toml" if static_recipe.is_file(): static_data = read_toml(static_recipe) status = static_data.get("status") @@ -420,10 +583,10 @@ def validate_ci_release_artifacts() -> None: for snippet, message in required_ci_snippets.items(): if snippet not in ci: fail(message) - for artifact in product_metadata.ci_sdk_package_artifact_names(): + for artifact in ci_sdk_package_artifact_names(): if artifact not in ci: fail(f"CI must upload SDK package artifact {artifact}") - for product in product_metadata.sdk_package_products(): + for product in 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( @@ -484,7 +647,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 product_metadata.sdk_package_products(): + for project_id in sdk_package_products(): moon_file = ( "src/bindings/wasix-rust/moon.yml" if project_id == "oliphaunt-wasix-rust" @@ -668,7 +831,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 product_metadata.sdk_package_products(): + for product_id in sdk_package_products(): require_text( "tools/release/release.py", f'validate_staged_sdk_package("{product_id}")', @@ -1172,7 +1335,7 @@ def validate_target_matrices() -> None: liboliphaunt_targets = {item["target"] for item in liboliphaunt_matrix["include"]} expected_liboliphaunt_targets = { target.target - for target in product_metadata.artifact_targets( + for target in artifact_targets( product="liboliphaunt-native", kind="native-runtime", published_only=True, @@ -1193,7 +1356,7 @@ def validate_target_matrices() -> None: } expected_extension_native_pairs = { (target.product, target.target) - for target in product_metadata.extension_artifact_targets(family="native", published_only=True) + for target in extension_artifact_targets(family="native", published_only=True) } if extension_native_pairs != expected_extension_native_pairs: fail( @@ -1205,7 +1368,7 @@ def validate_target_matrices() -> None: broker_targets = {item["target"] for item in broker_matrix["include"]} expected_broker_targets = { target.target - for target in product_metadata.artifact_targets( + for target in artifact_targets( product="oliphaunt-broker", kind="broker-helper", published_only=True, @@ -1221,7 +1384,7 @@ def validate_target_matrices() -> None: node_direct_targets = {item["target"] for item in node_direct_matrix["include"]} expected_node_direct_targets = { target.target - for target in product_metadata.artifact_targets( + for target in artifact_targets( product="oliphaunt-node-direct", kind="node-direct-addon", published_only=True, @@ -1242,7 +1405,7 @@ def validate_target_matrices() -> None: } expected_extension_wasix_pairs = { (target.product, target.target) - for target in product_metadata.extension_artifact_targets(family="wasix", published_only=True) + for target in extension_artifact_targets(family="wasix", published_only=True) } if extension_wasix_pairs != expected_extension_wasix_pairs: fail( @@ -1252,7 +1415,7 @@ def validate_target_matrices() -> None: def validate_typescript_runtime_targets() -> None: - for target in product_metadata.artifact_targets( + for target in artifact_targets( product="liboliphaunt-native", kind="native-runtime", surface="typescript-native-direct", @@ -1280,7 +1443,7 @@ def validate_typescript_runtime_targets() -> None: reject_text(path, target.npm_package, f"TypeScript native resolver must not advertise unpublished target {target.id}") reject_text(path, target.target, f"TypeScript native resolver must not expose unpublished target id {target.target}") - for target in product_metadata.artifact_targets( + for target in artifact_targets( product="oliphaunt-broker", kind="broker-helper", surface="typescript-broker", @@ -1303,7 +1466,7 @@ def validate_typescript_runtime_targets() -> None: reject_text(path, target.npm_package, f"TypeScript broker resolver must not advertise unpublished target {target.id}") reject_text(path, target.target, f"TypeScript broker resolver must not expose unpublished target id {target.target}") - for target in product_metadata.artifact_targets( + for target in artifact_targets( product="oliphaunt-node-direct", kind="node-direct-addon", surface="npm-optional", @@ -1335,7 +1498,7 @@ def validate_rust_broker_targets() -> None: ) require_text( manifest, - f'broker-version = "{product_metadata.read_current_version("oliphaunt-broker")}"', + f'broker-version = "{read_current_version("oliphaunt-broker")}"', "Rust SDK package metadata must pin the compatible broker helper version", ) require_text( @@ -1343,7 +1506,7 @@ def validate_rust_broker_targets() -> None: "OLIPHAUNT_BROKER_ASSET_DIR", "Rust broker resolver must support package-shaped broker artifact fixtures", ) - for target in product_metadata.artifact_targets( + for target in artifact_targets( product="oliphaunt-broker", kind="broker-helper", surface="rust-broker", @@ -1409,7 +1572,7 @@ def validate_expected_product_assets() -> None: for product, assets in expected.items(): actual = { target.asset - for target in product_metadata.artifact_targets( + for target in artifact_targets( product=product, surface="github-release", published_only=True, diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 87306a3a..c3f128a2 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -248,10 +248,11 @@ def validate_graph_files() -> None: or "export function extensionSourceIdentity(" not in release_artifact_targets or "exactExtensionProducts(TOOL)" not in release_graph_query or 'config.get("kind") == "exact-extension-artifact"' in product_metadata_source - or "extension_products = product_metadata.extension_product_ids()" not in check_artifact_targets + or "extension_products = extension_product_ids()" not in check_artifact_targets or "return set(product_metadata.extension_product_ids())" not in check_consumer_shape or "modeled_extension_products = set(extension_product_ids())" not in release_policy or "import product_metadata" in release_policy + or "import product_metadata" in check_artifact_targets or "function extensionMetadata(" in build_extension_ci_artifacts or "function extensionSourceIdentity(" in build_extension_ci_artifacts or "function extensionMetadata(" in check_staged_artifacts @@ -313,7 +314,7 @@ def validate_graph_files() -> None: if ( '"legacy-central-artifact-targets"' not in product_metadata_source or "legacy-central-artifact-targets" not in release_graph_query - or "product_metadata.legacy_central_artifact_target_rows()" not in check_artifact_targets + or 'release_graph_rows("legacy-central-artifact-targets")' not in check_artifact_targets or ("product_metadata." + "load_graph()") in check_artifact_targets or ("def " + "load_graph()") in check_release_metadata_source or ("product_metadata." + "load_graph()") in check_release_metadata_source From dd0e2d40ff8d7792bc0cccff1e990c4df290e5aa Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 19:14:22 +0000 Subject: [PATCH 218/308] chore: query consumer shape metadata via bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 27 + tools/release/check_consumer_shape.py | 538 +++++++++++++++--- tools/release/check_release_metadata.py | 7 +- tools/release/release_graph_query.mjs | 24 +- 4 files changed, 527 insertions(+), 69 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index ab67cc6d..b6c26be0 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,33 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Removed `check_consumer_shape.py`'s import of the Python + `product_metadata.py` compatibility module. The consumer-shape checker now + reads product configs, product versions, artifact targets, extension targets, + expected assets, TypeScript optional runtime package versions, and the WASIX + Cargo artifact contract through cached local wrappers over + `release_graph_query.mjs`. `release_graph_query.mjs + wasix-extension-package-names` now supports a bulk all-extension mode so the + exact-extension consumer-shape pass keeps Bun as the package-name authority + without spawning one process per extension target; the single-product + `--product/--target` mode remains available. `check_release_metadata.py` + rejects reintroducing the `product_metadata.py` import in + `check_consumer_shape.py` and requires the bulk WASIX extension package-name + query path. Fresh checks passed: bulk and single-product + `wasix-extension-package-names` query smoke, `python3 -m py_compile` for + touched Python helpers, timed full `python3 + tools/release/check_consumer_shape.py` at 8.58s, `python3 + tools/release/check_release_metadata.py`, `python3 + tools/release/check_artifact_targets.py`, `python3 + tools/policy/check-release-policy.py`, `bash + tools/policy/check-policy-tools.sh`, `bash + tools/policy/check-tooling-stack.sh`, `bash tools/policy/check-docs.sh`, + `tools/release/release.py check`, `git diff --check`, and + `tools/dev/bun.sh tools/policy/check-python-entrypoints.mjs --json`. The + Python entrypoint inventory still reports 9 entrypoints because this slice + removes one compatibility import rather than deleting an entrypoint. A + subagent review was attempted for this slice, but the current session remained + at the agent thread limit, so the pass used local repository evidence. - 2026-06-27: Re-ran the Linux-local release/local-registry validation batch after the latest tooling migrations. Fresh checks passed: `tools/release/local_registry_publish.py publish --surface cargo --strict`, diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 8f91fa4f..bacc9622 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -11,13 +11,14 @@ import argparse import json +import subprocess import sys import tomllib from dataclasses import dataclass +from functools import lru_cache from pathlib import Path -from typing import NoReturn - -import product_metadata +from types import SimpleNamespace +from typing import Any, NoReturn ROOT = Path(__file__).resolve().parents[2] @@ -94,6 +95,415 @@ def read_json(path: str) -> dict: return value +def bun_json(args: list[str]) -> object: + try: + output = subprocess.check_output( + ["tools/dev/bun.sh", *args], + cwd=ROOT, + text=True, + stderr=subprocess.PIPE, + ) + except subprocess.CalledProcessError as error: + detail = (error.stderr or "").strip() + if detail: + fail(f"Bun metadata query failed: {detail}") + fail(f"Bun metadata query failed with exit code {error.returncode}") + try: + return json.loads(output) + except json.JSONDecodeError as error: + fail(f"Bun metadata query did not return valid JSON: {error}") + + +@lru_cache(maxsize=None) +def release_graph_json(command: str, args: tuple[str, ...] = ()) -> Any: + return bun_json(["tools/release/release_graph_query.mjs", command, *args]) + + +@lru_cache(maxsize=None) +def release_graph_rows(command: str, args: tuple[str, ...] = ()) -> tuple[dict[str, Any], ...]: + value = release_graph_json(command, args) + if not isinstance(value, list) or not all(isinstance(row, dict) for row in value): + fail(f"release graph {command} query must return a JSON object list") + return tuple(value) + + +def string_list(value: Any, label: str) -> list[str]: + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + fail(f"{label} must be a string list") + return list(value) + + +@dataclass(frozen=True) +class ArtifactTarget: + id: str + product: str + kind: str + target: str + asset: str + published: bool + surfaces: tuple[str, ...] + triple: str | None = None + runner: str | None = None + library_relative_path: str | None = None + executable_relative_path: str | None = None + npm_package: str | None = None + npm_os: str | None = None + npm_cpu: str | None = None + npm_libc: str | None = None + llvm_url: str | None = None + extension_artifacts: bool = True + + def asset_name(self, version: str) -> str: + return self.asset.format(version=version) + + +def artifact_target_from_row(row: dict[str, Any]) -> ArtifactTarget: + target_id = row.get("id") + if not isinstance(target_id, str) or not target_id: + fail("artifact target row must declare a non-empty id") + surfaces = string_list(row.get("surfaces"), f"artifact target {target_id}.surfaces") + values: dict[str, str] = {} + for key in ["product", "kind", "target", "asset"]: + value = row.get(key) + if not isinstance(value, str) or not value: + fail(f"artifact target {target_id}.{key} must be a non-empty string") + values[key] = value + published = row.get("published") + if not isinstance(published, bool): + fail(f"artifact target {target_id}.published must be true or false") + optional: dict[str, str | None] = {} + for key in [ + "triple", + "runner", + "library_relative_path", + "executable_relative_path", + "npm_package", + "npm_os", + "npm_cpu", + "npm_libc", + "llvm_url", + ]: + value = row.get(key) + if value is not None and not isinstance(value, str): + fail(f"artifact target {target_id}.{key} must be a string when present") + optional[key] = value + extension_artifacts = row.get("extension_artifacts", True) + if not isinstance(extension_artifacts, bool): + fail(f"artifact target {target_id}.extension_artifacts must be true or false") + return ArtifactTarget( + id=target_id, + product=values["product"], + kind=values["kind"], + target=values["target"], + asset=values["asset"], + published=published, + surfaces=tuple(surfaces), + extension_artifacts=extension_artifacts, + **optional, + ) + + +def artifact_target_args( + *, + product: str | None = None, + kind: str | None = None, + surface: str | None = None, + published_only: bool = False, +) -> tuple[str, ...]: + args: list[str] = [] + if product is not None: + args.extend(["--product", product]) + if kind is not None: + args.extend(["--kind", kind]) + if surface is not None: + args.extend(["--surface", surface]) + if published_only: + args.append("--published-only") + return tuple(args) + + +def artifact_targets( + *, + product: str | None = None, + kind: str | None = None, + surface: str | None = None, + published_only: bool = False, +) -> list[ArtifactTarget]: + return [ + artifact_target_from_row(row) + for row in release_graph_rows( + "artifact-targets", + artifact_target_args( + product=product, + kind=kind, + surface=surface, + published_only=published_only, + ), + ) + ] + + +@lru_cache(maxsize=None) +def product_config_rows() -> tuple[dict[str, Any], ...]: + rows = release_graph_rows("product-configs") + seen: set[str] = set() + for row in rows: + product = row.get("product") + if not isinstance(product, str) or not product: + fail("release graph product-configs rows must declare a non-empty product") + if product in seen: + fail(f"release graph product-configs query returned duplicate product {product}") + seen.add(product) + if not rows: + fail("release graph product-configs query returned no products") + return rows + + +@lru_cache(maxsize=1) +def product_ids() -> tuple[str, ...]: + return tuple(str(row["product"]) for row in product_config_rows()) + + +def product_config(product: str) -> dict[str, Any]: + matches = [row for row in product_config_rows() if row.get("product") == product] + if len(matches) != 1: + fail(f"release graph product-configs query returned {len(matches)} rows for {product}") + return dict(matches[0]) + + +def package_path(product: str) -> str: + path = product_config(product).get("path") + if not isinstance(path, str) or not path: + fail(f"release graph product-configs {product}.path must be a non-empty string") + return path + + +def product_string_list(product: str, key: str) -> list[str]: + return string_list(product_config(product).get(key, []), f"{product}.{key}") + + +@lru_cache(maxsize=1) +def product_version_rows() -> tuple[dict[str, Any], ...]: + rows = release_graph_rows("product-versions") + seen: set[str] = set() + for row in rows: + product = row.get("product") + version = row.get("version") + if not isinstance(product, str) or not product: + fail("release graph product-versions rows must declare a non-empty product") + if not isinstance(version, str) or not version: + fail(f"release graph product-versions {product}.version must be a non-empty string") + if product in seen: + fail(f"release graph product-versions query returned duplicate product {product}") + seen.add(product) + if not rows: + fail("release graph product-versions query returned no products") + return rows + + +def read_current_version(product: str) -> str: + matches = [row for row in product_version_rows() if row.get("product") == product] + if len(matches) != 1: + fail(f"release graph product-versions query returned {len(matches)} rows for {product}") + version = matches[0].get("version") + if not isinstance(version, str) or not version: + fail(f"release graph product-versions {product}.version must be a non-empty string") + return version + + +def typescript_optional_runtime_package_versions() -> dict[str, str]: + versions: dict[str, str] = {} + for row in release_graph_rows("typescript-optional-runtime-package-versions"): + package_name = row.get("packageName") + version = row.get("version") + if not isinstance(package_name, str) or not package_name: + fail("typescript-optional-runtime-package-versions rows must declare a non-empty packageName") + if not isinstance(version, str) or not version: + fail(f"typescript-optional-runtime-package-versions {package_name}.version must be non-empty") + if package_name in versions: + fail(f"duplicate TypeScript optional runtime package target {package_name}") + versions[package_name] = version + if not versions: + fail("release graph returned no TypeScript optional runtime package versions") + return versions + + +@lru_cache(maxsize=1) +def wasix_cargo_artifact_contract() -> dict[str, Any]: + value = release_graph_json("wasix-cargo-artifact-contract") + if not isinstance(value, dict): + fail("release graph wasix-cargo-artifact-contract query must return a JSON object") + return value + + +def wasix_contract_string_list(key: str) -> tuple[str, ...]: + return tuple(string_list(wasix_cargo_artifact_contract().get(key), f"WASIX Cargo artifact contract {key}")) + + +def wasix_contract_string_map(key: str) -> dict[str, str]: + value = wasix_cargo_artifact_contract().get(key) + if not isinstance(value, dict) or not all( + isinstance(item_key, str) + and item_key + and isinstance(item_value, str) + and item_value + for item_key, item_value in value.items() + ): + fail(f"WASIX Cargo artifact contract {key} must be a string map") + return dict(value) + + +def wasix_public_cargo_package_names() -> tuple[str, ...]: + return wasix_contract_string_list("publicCargoPackageNames") + + +def wasix_public_aot_cargo_dependencies() -> dict[str, str]: + return wasix_contract_string_map("publicAotCargoDependencies") + + +def wasix_public_tools_aot_cargo_dependencies() -> dict[str, str]: + return wasix_contract_string_map("publicToolsAotCargoDependencies") + + +def wasix_public_tools_feature_dependencies() -> set[str]: + return set(wasix_contract_string_list("publicToolsFeatureDependencies")) + + +def wasix_core_runtime_archive_files() -> tuple[str, ...]: + return wasix_contract_string_list("coreRuntimeArchiveFiles") + + +def wasix_tools_payload_files() -> tuple[str, ...]: + return wasix_contract_string_list("toolsPayloadFiles") + + +def wasix_forbidden_runtime_archive_tool_files() -> tuple[str, ...]: + return wasix_contract_string_list("forbiddenRuntimeArchiveToolFiles") + + +def wasix_tools_aot_artifacts() -> set[str]: + return set(wasix_contract_string_list("toolsAotArtifacts")) + + +def wasix_expected_extension_aot_targets() -> tuple[str, ...]: + return wasix_contract_string_list("expectedExtensionAotTargets") + + +def expected_assets( + product: str, + version: str, + *, + surface: str = "github-release", +) -> list[str]: + rows = release_graph_rows( + "expected-assets", + ("--product", product, "--version", version, "--surface", surface), + ) + names: list[str] = [] + for row in rows: + asset_name = row.get("assetName") + if not isinstance(asset_name, str) or not asset_name: + fail(f"release graph expected-assets {product}/{surface} row must declare a non-empty assetName") + names.append(asset_name) + if not names: + fail(f"release graph returned no expected assets for {product}/{surface}") + if len(names) != len(set(names)): + fail(f"release graph expected-assets returned duplicate asset names for {product}/{surface}") + return sorted(names) + + +def extension_artifact_targets( + *, + product: str | None = None, + family: str | None = None, + published_only: bool = False, +) -> tuple[SimpleNamespace, ...]: + rows = [] + for row in release_graph_rows("extension-targets"): + if product is not None and row.get("product") != product: + continue + if family is not None and row.get("family") != family: + continue + if published_only and row.get("published") is not True: + continue + rows.append(SimpleNamespace(**row)) + return tuple(rows) + + +def published_android_maven_targets(product: str) -> tuple[SimpleNamespace, ...]: + return tuple( + sorted( + ( + target + for target in extension_artifact_targets( + product=product, + family="native", + published_only=True, + ) + if target.kind == "native-static-registry" and target.target.startswith("android-") + ), + key=lambda target: target.target, + ) + ) + + +def extension_product_ids() -> list[str]: + products: list[str] = [] + for row in release_graph_rows("extension-metadata"): + product = row.get("product") + if not isinstance(product, str) or not product: + fail("release graph extension-metadata rows must declare a non-empty product") + products.append(product) + if len(products) != len(set(products)): + fail("release graph extension-metadata query returned duplicate products") + return sorted(products) + + +@lru_cache(maxsize=1) +def wasix_extension_package_rows() -> tuple[dict[str, Any], ...]: + rows = release_graph_rows("wasix-extension-package-names") + seen: set[str] = set() + for row in rows: + product = row.get("product") + package_name = row.get("packageName") + aot_packages = row.get("aotPackages") + if not isinstance(product, str) or not product: + fail("release graph wasix-extension-package-names rows must declare a non-empty product") + if product in seen: + fail(f"release graph wasix-extension-package-names returned duplicate product {product}") + seen.add(product) + if not isinstance(package_name, str) or not package_name: + fail(f"release graph wasix-extension-package-names {product}.packageName must be non-empty") + if not isinstance(aot_packages, list) or not all(isinstance(item, dict) for item in aot_packages): + fail(f"release graph wasix-extension-package-names {product}.aotPackages must be an object list") + if not rows: + fail("release graph returned no WASIX extension package names") + return rows + + +def wasix_extension_package_contract(product: str) -> dict[str, Any]: + matches = [row for row in wasix_extension_package_rows() if row.get("product") == product] + if len(matches) != 1: + fail(f"release graph wasix-extension-package-names returned {len(matches)} rows for {product}") + return dict(matches[0]) + + +def wasix_extension_package_name(product: str) -> str: + return str(wasix_extension_package_contract(product).get("packageName")) + + +def wasix_extension_aot_package_name(product: str, target: str) -> str: + rows = wasix_extension_package_contract(product).get("aotPackages") + assert isinstance(rows, list) + matches = [row for row in rows if row.get("target") == target] + if len(matches) != 1: + fail(f"release graph returned {len(matches)} WASIX extension AOT package names for {product}/{target}") + package_name = matches[0].get("packageName") + if not isinstance(package_name, str) or not package_name: + fail(f"release graph wasix-extension-package-names {product}/{target}.packageName must be non-empty") + return package_name + + def read_toml(path: str) -> dict: try: return tomllib.loads(read_text(path)) @@ -121,7 +531,7 @@ def parse_products_json(raw: str | None) -> list[str]: fail(f"--products-json must be valid JSON: {error}") 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()) + known = set(product_ids()) unknown = sorted(set(value) - known) if unknown: fail(f"unknown release products: {', '.join(unknown)}") @@ -254,7 +664,7 @@ def validate_fixture_contract( def product_registry_packages(product: str) -> list[str]: - config = product_metadata.product_config(product) + config = product_config(product) packages = config.get("registry_packages", []) if not isinstance(packages, list): fail(f"{product}.registry_packages must be a list") @@ -262,7 +672,7 @@ def product_registry_packages(product: str) -> list[str]: def product_publish_targets(product: str) -> list[str]: - config = product_metadata.product_config(product) + config = product_config(product) targets = config.get("publish_targets", []) if not isinstance(targets, list): fail(f"{product}.publish_targets must be a list") @@ -271,7 +681,7 @@ def product_publish_targets(product: str) -> list[str]: def npm_registry_packages(product: str, kind: str, surface: str) -> set[str]: packages = set() - for target in product_metadata.artifact_targets( + for target in artifact_targets( product=product, kind=kind, surface=surface, @@ -284,19 +694,19 @@ def npm_registry_packages(product: str, kind: str, surface: str) -> set[str]: def liboliphaunt_native_expected_registry_packages() -> set[str]: - runtime_targets = product_metadata.artifact_targets( + runtime_targets = artifact_targets( product="liboliphaunt-native", kind="native-runtime", surface="rust-native-direct", published_only=True, ) - tools_targets = product_metadata.artifact_targets( + tools_targets = artifact_targets( product="liboliphaunt-native", kind="native-tools", surface="typescript-native-direct", published_only=True, ) - android_targets = product_metadata.artifact_targets( + android_targets = artifact_targets( product="liboliphaunt-native", kind="native-runtime", surface="maven", @@ -354,7 +764,7 @@ def native_npm_tool_split_failures( def broker_expected_registry_packages() -> set[str]: - targets = product_metadata.artifact_targets( + targets = artifact_targets( product="oliphaunt-broker", kind="broker-helper", published_only=True, @@ -401,7 +811,7 @@ def check_npm_package_common( findings, product, "npm-version", - package.get("version") == product_metadata.read_current_version(product), + package.get("version") == read_current_version(product), "npm package version must match the release metadata product version.", f"{path}: version={package.get('version')!r}", severity="P0", @@ -450,7 +860,7 @@ def check_liboliphaunt(findings: list[Finding]) -> None: findings, product, "version-source", - version == product_metadata.read_current_version(product), + version == read_current_version(product), "liboliphaunt VERSION must be the release metadata version source.", f"src/runtimes/liboliphaunt/native/VERSION={version!r}", severity="P0", @@ -766,7 +1176,7 @@ def check_rust(findings: list[Finding]) -> None: build_manifest = read_toml("src/sdks/rust/crates/oliphaunt-build/Cargo.toml") package = manifest.get("package", {}) build_package = build_manifest.get("package", {}) - product_version = product_metadata.read_current_version(product) + product_version = read_current_version(product) require( findings, product, @@ -931,8 +1341,8 @@ def check_broker(findings: list[Finding]) -> None: f"src/runtimes/broker/release.toml registry_packages={product_registry_packages(product)!r}", severity="P0", ) - version = product_metadata.read_current_version(product) - for target in product_metadata.artifact_targets( + version = read_current_version(product) + for target in artifact_targets( product=product, kind="broker-helper", surface="rust-broker", @@ -987,7 +1397,7 @@ def check_broker(findings: list[Finding]) -> None: def check_node_direct(findings: list[Finding]) -> None: product = "oliphaunt-node-direct" package = read_json("src/runtimes/node-direct/package.json") - version = product_metadata.read_current_version(product) + version = read_current_version(product) require( findings, product, @@ -1012,7 +1422,7 @@ def check_node_direct(findings: list[Finding]) -> None: product, "node-direct-liboliphaunt-pin", isinstance(metadata, dict) - and metadata.get("liboliphauntVersion") == product_metadata.read_current_version("liboliphaunt-native"), + and metadata.get("liboliphauntVersion") == read_current_version("liboliphaunt-native"), "Node direct source package must pin the compatible native liboliphaunt runtime version.", f"src/runtimes/node-direct/package.json oliphaunt={metadata!r}", severity="P0", @@ -1036,13 +1446,13 @@ def check_node_direct(findings: list[Finding]) -> None: and { "node-api-prebuilds", "npm-optional-platform-packages", - }.issubset(set(product_metadata.product_config(product).get("release_artifacts", []))), + }.issubset(set(product_config(product).get("release_artifacts", []))), "Node direct must publish both GitHub prebuild assets and optional npm platform packages.", "src/runtimes/node-direct/release.toml", severity="P0", ) - node_targets = product_metadata.artifact_targets( + node_targets = artifact_targets( product=product, kind="node-direct-addon", surface="npm-optional", @@ -1118,7 +1528,7 @@ def check_swift(findings: list[Finding]) -> None: findings, product, "swift-version", - version == product_metadata.read_current_version(product), + version == read_current_version(product), "Swift SDK VERSION must be the release metadata product version.", f"src/sdks/swift/VERSION={version!r}", severity="P0", @@ -1127,7 +1537,7 @@ def check_swift(findings: list[Finding]) -> None: findings, product, "swift-liboliphaunt-pin", - lib_version == product_metadata.read_current_version("liboliphaunt-native"), + lib_version == read_current_version("liboliphaunt-native"), "Swift SDK must pin the compatible liboliphaunt release.", f"src/sdks/swift/LIBOLIPHAUNT_VERSION={lib_version!r}", severity="P0", @@ -1198,7 +1608,7 @@ def check_kotlin(findings: list[Finding]) -> None: findings, product, "kotlin-version", - props.get("VERSION_NAME") == product_metadata.read_current_version(product), + props.get("VERSION_NAME") == read_current_version(product), "Kotlin SDK version must match the release metadata product version.", f"src/sdks/kotlin/gradle.properties VERSION_NAME={props.get('VERSION_NAME')!r}", severity="P0", @@ -1210,7 +1620,7 @@ def check_kotlin(findings: list[Finding]) -> None: findings, product, "android-liboliphaunt-pin", - pinned_lib == product_metadata.read_current_version("liboliphaunt-native"), + pinned_lib == read_current_version("liboliphaunt-native"), "Android Gradle plugin must pin the compatible liboliphaunt release.", f"liboliphaunt.version={pinned_lib!r}", severity="P0", @@ -1398,8 +1808,8 @@ def check_react_native(findings: list[Finding]) -> None: product, "rn-sdk-compatibility", isinstance(metadata, dict) - and metadata.get("swiftSdkVersion") == product_metadata.read_current_version("oliphaunt-swift") - and metadata.get("kotlinSdkVersion") == product_metadata.read_current_version("oliphaunt-kotlin"), + and metadata.get("swiftSdkVersion") == read_current_version("oliphaunt-swift") + and metadata.get("kotlinSdkVersion") == read_current_version("oliphaunt-kotlin"), "React Native package must pin compatible Swift and Kotlin SDK versions.", f"src/sdks/react-native/package.json oliphaunt={metadata!r}", severity="P0", @@ -1479,7 +1889,7 @@ def check_typescript(findings: list[Finding]) -> None: f"src/sdks/js/package.json dependencies={package.get('dependencies')!r}", severity="P0", ) - expected_optional = product_metadata.typescript_optional_runtime_package_versions() + expected_optional = typescript_optional_runtime_package_versions() optional_dependencies = package.get("optionalDependencies", {}) require( findings, @@ -1498,11 +1908,11 @@ def check_typescript(findings: list[Finding]) -> None: product, "ts-sdk-compatibility", isinstance(metadata, dict) - and metadata.get("liboliphauntVersion") == product_metadata.read_current_version("liboliphaunt-native") + and metadata.get("liboliphauntVersion") == read_current_version("liboliphaunt-native") and metadata.get("icuPackage") == "@oliphaunt/icu" - and metadata.get("icuVersion") == product_metadata.read_current_version("liboliphaunt-native") - and metadata.get("brokerVersion") == product_metadata.read_current_version("oliphaunt-broker") - and metadata.get("nodeDirectAddonVersion") == product_metadata.read_current_version("oliphaunt-node-direct"), + and metadata.get("icuVersion") == read_current_version("liboliphaunt-native") + and metadata.get("brokerVersion") == read_current_version("oliphaunt-broker") + and metadata.get("nodeDirectAddonVersion") == read_current_version("oliphaunt-node-direct"), "TypeScript SDK must pin compatible liboliphaunt, optional ICU, broker-helper, and Node direct versions.", f"src/sdks/js/package.json oliphaunt={metadata!r}", severity="P0", @@ -1523,7 +1933,7 @@ def check_typescript(findings: list[Finding]) -> None: findings, product, "jsr-version", - jsr.get("version") == product_metadata.read_current_version(product), + jsr.get("version") == read_current_version(product), "JSR version must match the TypeScript release metadata product version.", f"src/sdks/js/jsr.json version={jsr.get('version')!r}", severity="P0", @@ -1559,7 +1969,7 @@ def check_wasm(findings: list[Finding]) -> None: findings, product, "wasm-version", - package.get("version") == product_metadata.read_current_version(product), + package.get("version") == read_current_version(product), "WASM crate version must match the release metadata product version.", f"oliphaunt-wasix Cargo.toml package.version={package.get('version')!r}", severity="P0", @@ -1604,7 +2014,7 @@ def check_wasm(findings: list[Finding]) -> None: severity="P0", ) expected_tools_feature = ( - product_metadata.wasix_public_tools_feature_dependencies() + wasix_public_tools_feature_dependencies() ) require( findings, @@ -1657,7 +2067,7 @@ def check_wasm(findings: list[Finding]) -> None: ], severity="P0", ) - runtime_version = product_metadata.read_current_version("liboliphaunt-wasix") + runtime_version = read_current_version("liboliphaunt-wasix") dependencies = manifest.get("dependencies", {}) target_tables = manifest.get("target", {}) expected_runtime_dependency = dependencies.get("liboliphaunt-wasix-portable") @@ -1699,10 +2109,10 @@ def check_wasm(findings: list[Finding]) -> None: severity="P0", ) expected_aot_dependencies = ( - product_metadata.wasix_public_aot_cargo_dependencies() + wasix_public_aot_cargo_dependencies() ) expected_tools_aot_dependencies = ( - product_metadata.wasix_public_tools_aot_cargo_dependencies() + wasix_public_tools_aot_cargo_dependencies() ) missing_aot_dependencies = [] for cfg, crate in expected_aot_dependencies.items(): @@ -1803,7 +2213,7 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: findings, product, "wasix-runtime-version", - version == product_metadata.read_current_version(product), + version == read_current_version(product), "WASIX runtime VERSION must be the release metadata product version.", f"src/runtimes/liboliphaunt/wasix/VERSION={version!r}", severity="P0", @@ -1840,7 +2250,7 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: product, "wasix-assets-crate", asset_package.get("name") == "liboliphaunt-wasix-portable" - and asset_package.get("version") == product_metadata.read_current_version(product), + and asset_package.get("version") == 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}", severity="P0", @@ -1850,7 +2260,7 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: product, "wasix-tools-crate", tools_package.get("name") == "oliphaunt-wasix-tools" - and tools_package.get("version") == product_metadata.read_current_version(product), + and tools_package.get("version") == 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", @@ -1910,7 +2320,7 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: registry_packages = set(product_registry_packages(product)) expected_registry_packages = { f"crates:{name}" - for name in product_metadata.wasix_public_cargo_package_names() + for name in wasix_public_cargo_package_names() } require( findings, @@ -1940,13 +2350,13 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: findings, product, "wasix-portable-runtime-tool-contract", - product_metadata.wasix_core_runtime_archive_files() + wasix_core_runtime_archive_files() == ("oliphaunt/bin/initdb", "oliphaunt/bin/postgres") - and product_metadata.wasix_tools_payload_files() + and wasix_tools_payload_files() == ("bin/pg_dump.wasix.wasm", "bin/psql.wasix.wasm") - and product_metadata.wasix_forbidden_runtime_archive_tool_files() + and wasix_forbidden_runtime_archive_tool_files() == ("oliphaunt/bin/pg_ctl", "oliphaunt/bin/pg_dump", "oliphaunt/bin/psql") - and product_metadata.wasix_tools_aot_artifacts() + and wasix_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 @@ -2016,8 +2426,8 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: "tools/release/package_liboliphaunt_wasix_cargo_artifacts.py", severity="P0", ) - version = product_metadata.read_current_version(product) - expected_assets = set(product_metadata.expected_assets(product, version, surface="github-release")) + version = read_current_version(product) + expected_release_assets = set(expected_assets(product, version, surface="github-release")) require( findings, product, @@ -2030,28 +2440,28 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: f"liboliphaunt-wasix-{version}-runtime-aot-linux-arm64-gnu.tar.zst", f"liboliphaunt-wasix-{version}-runtime-aot-windows-x64-msvc.tar.zst", f"liboliphaunt-wasix-{version}-release-assets.sha256", - }.issubset(expected_assets), + }.issubset(expected_release_assets), "WASIX runtime release metadata must expose portable, target AOT, and checksum GitHub release assets.", - f"src/runtimes/liboliphaunt/wasix/moon.yml: {sorted(expected_assets)!r}", + f"src/runtimes/liboliphaunt/wasix/moon.yml: {sorted(expected_release_assets)!r}", severity="P0", ) def check_exact_extension(findings: list[Finding], product: str) -> None: - config = product_metadata.product_config(product) - package_path = product_metadata.package_path(product) + config = product_config(product) + product_path = package_path(product) sql_name = config.get("extension_sql_name") expected_registry_packages = { f"maven:dev.oliphaunt.extensions:{product}-{target.target}" - for target in product_metadata.published_android_maven_targets(product) + for target in published_android_maven_targets(product) } - version_path = f"{package_path}/VERSION" + version_path = f"{product_path}/VERSION" version = read_text(version_path).strip() require( findings, product, "extension-version", - version == product_metadata.read_current_version(product), + version == read_current_version(product), "Exact-extension VERSION must be the release metadata product version.", f"{version_path}={version!r}", severity="P0", @@ -2067,10 +2477,10 @@ def check_exact_extension(findings: list[Finding], product: str) -> None: and isinstance(sql_name, str) and sql_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}", + f"{product_path}/release.toml registry_packages={sorted(product_registry_packages(product))!r}", severity="P0", ) - targets = product_metadata.extension_artifact_targets(product=product, published_only=True) + targets = extension_artifact_targets(product=product, published_only=True) native_targets = {target.target for target in targets if target.family == "native"} wasix_targets = {target.target for target in targets if target.family == "wasix"} require( @@ -2087,13 +2497,13 @@ def check_exact_extension(findings: list[Finding], product: str) -> None: }.issubset(native_targets) and wasix_targets == {"wasix-portable"}, "Exact-extension artifact targets must cover mobile and non-Windows native artifact surfaces plus WASIX portable; default targets are derived from runtime metadata unless a product owns an override file.", - f"{package_path}/release.toml: native={sorted(native_targets)!r} wasix={sorted(wasix_targets)!r}", + f"{product_path}/release.toml: native={sorted(native_targets)!r} wasix={sorted(wasix_targets)!r}", severity="P0", ) - wasix_package = product_metadata.wasix_extension_package_name(product) + wasix_package = wasix_extension_package_name(product) wasix_aot_packages = { - product_metadata.wasix_extension_aot_package_name(product, target) - for target in product_metadata.wasix_expected_extension_aot_targets() + wasix_extension_aot_package_name(product, target) + for target in wasix_expected_extension_aot_targets() } native_qualified_registry_packages = [ package for package in product_registry_packages(product) if "-native-" in package @@ -2112,11 +2522,11 @@ def check_exact_extension(findings: list[Finding], product: str) -> None: and wasix_aot_packages == { f"{product}-wasix-aot-{target}" - for target in product_metadata.wasix_expected_extension_aot_targets() + for target in wasix_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}", + f"{product_path}/release.toml registry={sorted(product_registry_packages(product))!r} wasix={wasix_package!r} wasix_aot={sorted(wasix_aot_packages)!r}", severity="P0", ) require( @@ -2127,7 +2537,7 @@ def check_exact_extension(findings: list[Finding], product: str) -> None: and all(target.kind == "native-dynamic" for target in targets if target.target.startswith(("linux-", "macos-", "windows-"))) and all(target.kind == "wasix-runtime" for target in targets if target.family == "wasix"), "Exact-extension target metadata must distinguish mobile static-registry artifacts, desktop dynamic artifacts, and WASIX runtime artifacts.", - f"{package_path}/release.toml: {[f'{target.target}:{target.kind}' for target in targets]!r}", + f"{product_path}/release.toml: {[f'{target.target}:{target.kind}' for target in targets]!r}", severity="P0", ) @@ -2147,7 +2557,7 @@ def check_exact_extension(findings: list[Finding], product: str) -> None: def exact_extension_products() -> set[str]: - return set(product_metadata.extension_product_ids()) + return set(extension_product_ids()) def known_consumer_products() -> set[str]: diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index c3f128a2..197baa9f 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -249,10 +249,11 @@ def validate_graph_files() -> None: or "exactExtensionProducts(TOOL)" not in release_graph_query or 'config.get("kind") == "exact-extension-artifact"' in product_metadata_source or "extension_products = extension_product_ids()" not in check_artifact_targets - or "return set(product_metadata.extension_product_ids())" not in check_consumer_shape + or "return set(extension_product_ids())" not in check_consumer_shape or "modeled_extension_products = set(extension_product_ids())" not in release_policy or "import product_metadata" in release_policy or "import product_metadata" in check_artifact_targets + or "import product_metadata" in check_consumer_shape or "function extensionMetadata(" in build_extension_ci_artifacts or "function extensionSourceIdentity(" in build_extension_ci_artifacts or "function extensionMetadata(" in check_staged_artifacts @@ -378,7 +379,9 @@ def validate_graph_files() -> None: fail("registry package name selection must come from the shared Bun release graph query") if ( '"wasix-extension-package-names"' not in product_metadata_source - or "wasix-extension-package-names --product PRODUCT [--target TARGET...]" not in release_graph_query + or "wasix-extension-package-names [--product PRODUCT [--target TARGET...]]" not in release_graph_query + or "exactExtensionProducts(TOOL).map" not in release_graph_query + or 'release_graph_rows("wasix-extension-package-names")' not in check_consumer_shape or "wasixExtensionPackageName(product)" not in release_graph_query or "wasixExtensionAotPackageName(product, target)" not in release_graph_query or 'return f"{product}-wasix"' in product_metadata_source diff --git a/tools/release/release_graph_query.mjs b/tools/release/release_graph_query.mjs index 1f63a2f8..aba6094c 100644 --- a/tools/release/release_graph_query.mjs +++ b/tools/release/release_graph_query.mjs @@ -29,6 +29,7 @@ import { releaseProductProjectId, } from "./release-graph.mjs"; import { + expectedExtensionAotTargets, wasixCargoArtifactContract, wasixExtensionAotPackageName, wasixExtensionPackageName, @@ -412,14 +413,31 @@ function runWasixExtensionPackageNames(argv) { fail(`unknown argument ${value}`); } } - if (product === undefined || product.length === 0) { - fail("--product is required"); + if (product !== undefined && product.length === 0) { + fail("--product values must be non-empty"); } for (const target of targets) { if (target.length === 0) { fail("--target values must be non-empty"); } } + if (product === undefined) { + if (targets.length > 0) { + fail("--target requires --product"); + } + const aotTargets = expectedExtensionAotTargets(); + printJson( + exactExtensionProducts(TOOL).map((productId) => ({ + product: productId, + packageName: wasixExtensionPackageName(productId), + aotPackages: aotTargets.map((target) => ({ + target, + packageName: wasixExtensionAotPackageName(productId, target), + })), + })), + ); + return; + } printJson({ product, packageName: wasixExtensionPackageName(product), @@ -812,7 +830,7 @@ Commands: local-publish-artifacts [--aggregate-only] expected-assets --product PRODUCT --version VERSION [--surface SURFACE] [--kind KIND...] [--include-unpublished] registry-packages --product PRODUCT [--kind KIND] - wasix-extension-package-names --product PRODUCT [--target TARGET...] + wasix-extension-package-names [--product PRODUCT [--target TARGET...]] compatibility-version-entries [--require-source-product] wasix-cargo-artifact-contract `; From f85a04832256281b469aba68e5288a07a28a1e8b Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 19:27:01 +0000 Subject: [PATCH 219/308] chore: query extension metadata via bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 23 ++++++ src/extensions/artifacts/native/moon.yml | 3 + src/extensions/artifacts/wasix/moon.yml | 3 + src/extensions/model/moon.yml | 3 + src/extensions/tools/check-extension-model.py | 72 +++++++++++++++++-- tools/release/check_release_metadata.py | 19 +++++ 6 files changed, 116 insertions(+), 7 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index b6c26be0..0fbb11b7 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,29 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Removed `check-extension-model.py`'s import of the Python + `product_metadata.py` compatibility module. The extension model checker now + validates exact-extension release metadata shape directly from the canonical + Bun `release_graph_query.mjs extension-metadata` rows, preserving the + existing source-identity contract while avoiding the Python adapter. The + extension model, native extension artifact, and WASIX extension artifact Moon + check tasks now include `release_graph_query.mjs`, + `release-artifact-targets.mjs`, and `release-graph.mjs` as cache inputs so + release metadata changes invalidate the extension checker correctly. + `check_release_metadata.py` rejects reintroducing the import and guards those + Moon inputs. Fresh checks passed: `python3 -m py_compile` for touched Python + helpers, timed `python3 src/extensions/tools/check-extension-model.py + --check` at 2.39s, `tools/dev/bun.sh + tools/policy/assertions/assert-source-inputs.mjs extensions`, `python3 + tools/release/check_release_metadata.py`, `bash + tools/policy/check-policy-tools.sh`, `bash + tools/policy/check-tooling-stack.sh`, `bash tools/policy/check-docs.sh`, + `tools/release/release.py check`, `tools/dev/bun.sh + tools/policy/check-python-entrypoints.mjs --json`, and `git diff --check`. + The Python entrypoint inventory still reports 9 entrypoints because this + slice removes one compatibility import rather than deleting an entrypoint. A + subagent review was attempted for this slice, but the current session remained + at the agent thread limit, so the pass used local repository evidence. - 2026-06-27: Removed `check_consumer_shape.py`'s import of the Python `product_metadata.py` compatibility module. The consumer-shape checker now reads product configs, product versions, artifact targets, extension targets, diff --git a/src/extensions/artifacts/native/moon.yml b/src/extensions/artifacts/native/moon.yml index 04c9fd8a..ef142674 100644 --- a/src/extensions/artifacts/native/moon.yml +++ b/src/extensions/artifacts/native/moon.yml @@ -29,6 +29,9 @@ tasks: inputs: - "/src/extensions/**/*" - "/src/shared/extension-runtime-contract/**/*" + - "/tools/release/release_graph_query.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/tools/xtask/**/*" - "/Cargo.lock" - "/Cargo.toml" diff --git a/src/extensions/artifacts/wasix/moon.yml b/src/extensions/artifacts/wasix/moon.yml index 47d6d890..6cc5e94b 100644 --- a/src/extensions/artifacts/wasix/moon.yml +++ b/src/extensions/artifacts/wasix/moon.yml @@ -29,6 +29,9 @@ tasks: inputs: - "/src/extensions/**/*" - "/src/shared/extension-runtime-contract/**/*" + - "/tools/release/release_graph_query.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/tools/xtask/**/*" - "/Cargo.lock" - "/Cargo.toml" diff --git a/src/extensions/model/moon.yml b/src/extensions/model/moon.yml index 118d34c7..0a7c6881 100644 --- a/src/extensions/model/moon.yml +++ b/src/extensions/model/moon.yml @@ -18,6 +18,9 @@ tasks: inputs: - "/src/extensions/**/*" - "/src/shared/extension-runtime-contract/**/*" + - "/tools/release/release_graph_query.mjs" + - "/tools/release/release-artifact-targets.mjs" + - "/tools/release/release-graph.mjs" - "/tools/xtask/**/*" - "/Cargo.lock" - "/Cargo.toml" diff --git a/src/extensions/tools/check-extension-model.py b/src/extensions/tools/check-extension-model.py index 35a787e3..778771b5 100755 --- a/src/extensions/tools/check-extension-model.py +++ b/src/extensions/tools/check-extension-model.py @@ -7,15 +7,12 @@ import re import shutil import subprocess -import sys import tomllib +from functools import lru_cache from pathlib import Path from tempfile import TemporaryDirectory ROOT = Path(__file__).resolve().parents[3] -sys.path.insert(0, str(ROOT / "tools/release")) - -import product_metadata # noqa: E402 PROMOTED = ROOT / "src/extensions/catalog/extensions.promoted.toml" SMOKE = ROOT / "src/extensions/catalog/extensions.smoke.toml" @@ -159,6 +156,69 @@ def format_typescript_source(source: str, path: Path) -> str: fail(f"failed to format generated TypeScript extension metadata with Biome {BIOME_VERSION}: {error}") +@lru_cache(maxsize=None) +def release_graph_rows(command: str) -> tuple[dict, ...]: + try: + output = subprocess.check_output( + ["tools/dev/bun.sh", "tools/release/release_graph_query.mjs", command], + cwd=ROOT, + text=True, + stderr=subprocess.PIPE, + ) + except (FileNotFoundError, subprocess.CalledProcessError) as error: + detail = getattr(error, "stderr", "") or str(error) + fail(f"failed to query release graph {command}: {detail.strip()}") + try: + rows = json.loads(output) + except json.JSONDecodeError as error: + fail(f"release graph {command} query did not return valid JSON: {error}") + if not isinstance(rows, list) or not all(isinstance(row, dict) for row in rows): + fail(f"release graph {command} query must return a JSON object list") + return tuple(rows) + + +def validate_extension_metadata_row(row: dict) -> None: + product = row.get("product") + if not isinstance(product, str) or not product.startswith("oliphaunt-extension-"): + fail(f"release graph extension-metadata row must declare an exact-extension product: {product!r}") + for key in ["sqlName", "class", "versioning", "sourcePath"]: + value = row.get(key) + if not isinstance(value, str) or not value: + fail(f"release graph extension-metadata {product}.{key} must be a non-empty string") + compatibility = row.get("compatibility") + if not isinstance(compatibility, dict): + fail(f"release graph extension-metadata {product}.compatibility must be an object") + for key in [ + "postgresMajor", + "extensionRuntimeContract", + "nativeRuntimeProduct", + "nativeRuntimeVersion", + "wasixRuntimeProduct", + "wasixRuntimeVersion", + ]: + value = compatibility.get(key) + if not isinstance(value, str) or not value: + fail(f"release graph extension-metadata {product}.compatibility.{key} must be a non-empty string") + source_identity = row.get("sourceIdentity") + if not isinstance(source_identity, dict) or not source_identity: + fail(f"release graph extension-metadata {product}.sourceIdentity must be an object") + + +@lru_cache(maxsize=1) +def extension_metadata_rows() -> tuple[dict, ...]: + rows = release_graph_rows("extension-metadata") + seen: set[str] = set() + for row in rows: + validate_extension_metadata_row(row) + product = str(row["product"]) + if product in seen: + fail(f"release graph extension-metadata query returned duplicate product {product}") + seen.add(product) + if not rows: + fail("release graph extension-metadata query returned no products") + return rows + + def rel(path: Path) -> str: try: return path.relative_to(ROOT).as_posix() @@ -527,9 +587,7 @@ def validate_external_source_pins(build_by_sql_name: dict[str, dict], source_nam def validate_extension_release_metadata() -> None: - for product in product_metadata.extension_product_ids(): - product_metadata.extension_source_identity(product) - product_metadata.validate_extension_metadata(product) + extension_metadata_rows() def extension_family(source_kind: object) -> str: diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 197baa9f..a528860c 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -230,6 +230,10 @@ def validate_graph_files() -> None: check_staged_artifacts = read_text("tools/release/check-staged-artifacts.mjs") check_artifact_targets = read_text("tools/release/check_artifact_targets.py") check_consumer_shape = read_text("tools/release/check_consumer_shape.py") + extension_model = read_text("src/extensions/tools/check-extension-model.py") + extension_model_moon = read_text("src/extensions/model/moon.yml") + extension_artifacts_native_moon = read_text("src/extensions/artifacts/native/moon.yml") + extension_artifacts_wasix_moon = read_text("src/extensions/artifacts/wasix/moon.yml") release_policy = read_text("tools/policy/check-release-policy.py") check_release_metadata_source = read_text("tools/release/check_release_metadata.py") if ( @@ -254,6 +258,21 @@ def validate_graph_files() -> None: or "import product_metadata" in release_policy or "import product_metadata" in check_artifact_targets or "import product_metadata" in check_consumer_shape + or "import product_metadata" in extension_model + or 'release_graph_rows("extension-metadata")' not in extension_model + or any( + required not in moon_source + for moon_source in [ + extension_model_moon, + extension_artifacts_native_moon, + extension_artifacts_wasix_moon, + ] + for required in [ + "/tools/release/release_graph_query.mjs", + "/tools/release/release-artifact-targets.mjs", + "/tools/release/release-graph.mjs", + ] + ) or "function extensionMetadata(" in build_extension_ci_artifacts or "function extensionSourceIdentity(" in build_extension_ci_artifacts or "function extensionMetadata(" in check_staged_artifacts From 3a878609ffe09f35bee47d2951ad2e8fd10f57fe Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 19:44:52 +0000 Subject: [PATCH 220/308] chore: query local registry metadata via bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 22 +++++ tools/release/check_release_metadata.py | 7 +- tools/release/local_registry_publish.py | 81 +++++++++++++++---- 3 files changed, 91 insertions(+), 19 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 0fbb11b7..a8dc8944 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,28 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Removed `local_registry_publish.py`'s import of the Python + `product_metadata.py` compatibility module. The local registry publisher now + reads the local-publish artifact preset and native runtime/tools release + asset target names through cached wrappers over `release_graph_query.mjs`. + `check_release_metadata.py` rejects reintroducing the import and requires the + local registry publisher to use the shared Bun `local-publish-artifacts` and + `artifact-targets` queries. Fresh checks passed: `python3 -m py_compile` for + touched Python helpers, direct module smoke for `local_publish_artifacts`, + `local_publish_aggregate_artifacts`, and Linux x64 native runtime/tools asset + name resolution, `tools/release/local_registry_publish.py download --preset + local-publish --dry-run` against GitHub Actions run `28049923289`, + `tools/release/local_registry_publish.py publish --surface cargo --strict + --dry-run`, `tools/release/local_registry_publish.py publish --surface npm + --strict --dry-run`, `python3 tools/release/check_release_metadata.py`, and a + grep proving `local_registry_publish.py` no longer imports or calls + `product_metadata`, `bash tools/policy/check-policy-tools.sh`, `bash + tools/policy/check-tooling-stack.sh`, `bash tools/policy/check-docs.sh`, + `tools/release/release.py check`, and `git diff --check`. + The Python entrypoint inventory still reports 9 entrypoints because this + slice removes one compatibility import rather than deleting an entrypoint. A + subagent review was attempted for this slice, but the current session remained + at the agent thread limit, so the pass used local repository evidence. - 2026-06-27: Removed `check-extension-model.py`'s import of the Python `product_metadata.py` compatibility module. The extension model checker now validates exact-extension release metadata shape directly from the canonical diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index a528860c..6bf7223e 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -584,8 +584,11 @@ def validate_local_registry_publisher() -> None: 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_local_publish_artifact_names(aggregate_only=True)" not in publisher - or "ci_local_publish_artifact_names()" not in publisher + or 'release_graph_rows("local-publish-artifacts"' not in publisher + or "local_publish_artifact_names(aggregate_only=True)" not in publisher + or "local_publish_artifact_names()" not in publisher + or 'release_graph_rows(\n "artifact-targets"' not in publisher + or "import product_metadata" in publisher or "ci_aggregate_release_asset_artifact_name(\"liboliphaunt-native\")" in publisher or "ci_wasix_runtime_artifact_names()" in publisher or "ci_wasix_aot_runtime_artifact_names()" in publisher diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index d48db498..032fcdc4 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -31,11 +31,10 @@ import urllib.error import urllib.request from dataclasses import dataclass, field +from functools import lru_cache from pathlib import Path from typing import Any, Iterable -import product_metadata - ROOT = Path(__file__).resolve().parents[2] DEFAULT_RUN_ID = "28049923289" @@ -59,18 +58,63 @@ "oliphaunt-perf-", ) + +def release_graph_json(command: str, args: tuple[str, ...] = ()) -> Any: + try: + completed = run( + ["tools/dev/bun.sh", "tools/release/release_graph_query.mjs", command, *args], + capture=True, + ) + except subprocess.CalledProcessError as error: + detail = (error.stderr or "").strip() + if detail: + raise RuntimeError(f"release graph {command} query failed: {detail}") from error + raise RuntimeError(f"release graph {command} query failed with exit code {error.returncode}") from error + try: + return json.loads(completed.stdout) + except json.JSONDecodeError as error: + raise RuntimeError(f"release graph {command} query did not return valid JSON: {error}") from error + + +@lru_cache(maxsize=None) +def release_graph_rows(command: str, args: tuple[str, ...] = ()) -> tuple[dict[str, Any], ...]: + rows = release_graph_json(command, args) + if not isinstance(rows, list) or not all(isinstance(row, dict) for row in rows): + raise RuntimeError(f"release graph {command} query must return a JSON object list") + return tuple(rows) + + def local_publish_aggregate_artifacts() -> list[str]: - return product_metadata.ci_local_publish_artifact_names(aggregate_only=True) + return local_publish_artifact_names(aggregate_only=True) def local_publish_artifacts() -> list[str]: - artifacts = product_metadata.ci_local_publish_artifact_names() + artifacts = local_publish_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 local_publish_artifact_names(*, aggregate_only: bool = False) -> list[str]: + args = ("--aggregate-only",) if aggregate_only else () + names: list[str] = [] + for row in release_graph_rows("local-publish-artifacts", args): + artifact_name = row.get("artifactName") + aggregate = row.get("aggregate") + if not isinstance(artifact_name, str) or not artifact_name: + raise RuntimeError("release graph local-publish-artifacts rows must declare a non-empty artifactName") + if not isinstance(aggregate, bool): + raise RuntimeError(f"release graph local-publish-artifacts {artifact_name}.aggregate must be true or false") + names.append(artifact_name) + if not names: + raise RuntimeError("release graph returned no local-publish artifacts") + duplicates = sorted({name for name in names if names.count(name) > 1}) + if duplicates: + raise RuntimeError("release graph returned duplicate local-publish artifacts: " + ", ".join(duplicates)) + return sorted(names) + + def rel(path: Path) -> str: try: return str(path.relative_to(ROOT)) @@ -348,19 +392,22 @@ def release_asset_dir_selected(roots: list[Path], asset_dir: Path) -> bool: def native_release_asset_name(version: str, target: str, kind: str) -> str: - matches = [ - artifact.asset_name(version) - for artifact in product_metadata.artifact_targets( - product="liboliphaunt-native", - kind=kind, - published_only=True, - ) - if artifact.target == target - and ( - "rust-native-direct" in artifact.surfaces - or "typescript-native-direct" in artifact.surfaces - ) - ] + matches: list[str] = [] + for artifact in release_graph_rows( + "artifact-targets", + ("--product", "liboliphaunt-native", "--kind", kind, "--published-only"), + ): + if artifact.get("target") != target: + continue + surfaces = artifact.get("surfaces") + if not isinstance(surfaces, list) or not all(isinstance(surface, str) for surface in surfaces): + raise RuntimeError(f"release graph artifact target {target}/{kind} surfaces must be a string list") + if "rust-native-direct" not in surfaces and "typescript-native-direct" not in surfaces: + continue + asset = artifact.get("asset") + if not isinstance(asset, str) or not asset: + raise RuntimeError(f"release graph artifact target {target}/{kind} asset must be a non-empty string") + matches.append(asset.format(version=version)) if len(matches) != 1: raise RuntimeError( f"expected exactly one published liboliphaunt-native {kind} asset for {target}, got {matches}" From 0c8e20cc251660e364aa2d84c4c7dd6adb6a9be5 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 20:02:09 +0000 Subject: [PATCH 221/308] chore: query wasix cargo packager metadata via bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 27 +++ .../fixtures/consumer-shape/products.json | 2 +- tools/release/check_consumer_shape.py | 9 +- tools/release/check_release_metadata.py | 17 +- ...kage_liboliphaunt_wasix_cargo_artifacts.py | 226 ++++++++++++++++-- 5 files changed, 241 insertions(+), 40 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index a8dc8944..ee195fb8 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,33 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Removed `package_liboliphaunt_wasix_cargo_artifacts.py`'s + import of the Python `product_metadata.py` compatibility module. The WASIX + Cargo artifact packager now reads the portable runtime/tools/ICU/AOT + contract, bulk WASIX extension package names, and the + `liboliphaunt-wasix` version through cached Bun + `release_graph_query.mjs` calls. `check_release_metadata.py`, + `check_consumer_shape.py`, and the consumer-shape fixture now reject + reintroducing the Python adapter path and require the Bun + `wasix-cargo-artifact-contract`, `wasix-extension-package-names`, and + `product-versions` queries. Fresh checks passed: grep proving the WASIX + packager no longer imports or calls `product_metadata`, `python3 -m + py_compile` for touched Python helpers, direct packager module smoke for + runtime/tools/ICU/AOT package names and split tool lists, Bun + `wasix-cargo-artifact-contract` and single-extension + `wasix-extension-package-names` query smokes, focused + `check_consumer_shape.py --product liboliphaunt-wasix` and + `--product liboliphaunt-native`, `check_release_metadata.py`, strict local + Cargo registry dry-run, `bash tools/policy/check-policy-tools.sh`, `bash + tools/policy/check-tooling-stack.sh`, `bash tools/policy/check-docs.sh`, + full WASIX Cargo packager smoke into + `target/oliphaunt-wasix/cargo-artifacts-smoke`, and + `tools/release/release.py check`. The packager smoke generated zero crates + over the 10 MiB crates.io cap; the largest crate was + `oliphaunt-extension-postgis-wasix-aot-aarch64-unknown-linux-gnu-part-001` + at 10,212,312 bytes, leaving 273,448 bytes of headroom. The split tools + crates stayed small: `oliphaunt-wasix-tools` was 1,206,842 bytes and the + largest `oliphaunt-wasix-tools-aot-*` crate was 1,804,340 bytes. - 2026-06-27: Removed `local_registry_publish.py`'s import of the Python `product_metadata.py` compatibility module. The local registry publisher now reads the local-publish artifact preset and native runtime/tools release diff --git a/src/shared/fixtures/consumer-shape/products.json b/src/shared/fixtures/consumer-shape/products.json index 4c6652fe..5d41e667 100644 --- a/src/shared/fixtures/consumer-shape/products.json +++ b/src/shared/fixtures/consumer-shape/products.json @@ -60,7 +60,7 @@ "tools/release/package_liboliphaunt_wasix_cargo_artifacts.py": [ "CRATES_IO_MAX_BYTES", "validate_crate_size", - "product_metadata.wasix_cargo_artifact_schema()" + "release_graph_json(\"wasix-cargo-artifact-contract\")" ], "tools/release/wasix-cargo-artifact-contract.mjs": [ "oliphaunt-liboliphaunt-wasix-cargo-artifacts-v2" diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index bacc9622..9b27dec6 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -2364,10 +2364,11 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: 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 "product_metadata.wasix_core_runtime_archive_files()" in wasix_packager_source - and "product_metadata.wasix_tools_payload_files()" in wasix_packager_source - and "product_metadata.wasix_forbidden_runtime_archive_tool_files()" in wasix_packager_source - and "product_metadata.wasix_tools_aot_artifacts()" in wasix_packager_source, + and ("import " + "product_metadata") not in wasix_packager_source + and "product_metadata." not in wasix_packager_source + and 'release_graph_json("wasix-cargo-artifact-contract")' in wasix_packager_source + and 'release_graph_rows("wasix-extension-package-names")' in wasix_packager_source + and 'release_graph_rows("product-versions", ("--product", product))' 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 6bf7223e..2f70d209 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1677,17 +1677,16 @@ 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 "product_metadata.wasix_core_runtime_archive_files()" not in wasix_packager_source - or "product_metadata.wasix_tools_payload_files()" not in wasix_packager_source - or "product_metadata.wasix_forbidden_runtime_archive_tool_files()" not in wasix_packager_source - or "product_metadata.wasix_tools_aot_artifacts()" not in wasix_packager_source - or "product_metadata.wasix_extension_package_name(" not in wasix_packager_source - or "product_metadata.wasix_extension_aot_package_name(" not in wasix_packager_source - or "def wasix_extension_package_name(product" in wasix_packager_source - or "def wasix_extension_aot_package_name(product" in wasix_packager_source + or "import product_metadata" in wasix_packager_source + or "product_metadata." in wasix_packager_source + or 'release_graph_json("wasix-cargo-artifact-contract")' not in wasix_packager_source + or 'release_graph_rows("wasix-extension-package-names")' not in wasix_packager_source + or 'release_graph_rows("product-versions", ("--product", product))' not in wasix_packager_source + or "def wasix_extension_package_name(product" not in wasix_packager_source + or "def wasix_extension_aot_package_name(product" 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 publishable tools crates while keeping only postgres/initdb in root runtime crates") + fail("WASIX Cargo artifact packager must read the Bun WASIX artifact contract, split pg_dump/psql into publishable tools crates, and keep only postgres/initdb in root runtime crates") wasix_dependency_invariant_source = read_text("tools/policy/check-wasix-release-dependency-invariants.mjs") if ( "SOURCE_TEMPLATE_TOOLS_MANIFEST" not in wasix_dependency_invariant_source diff --git a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py index 9de39c01..f182006c 100644 --- a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py +++ b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py @@ -13,46 +13,220 @@ import sys import tarfile from dataclasses import dataclass +from functools import lru_cache from pathlib import Path, PurePosixPath -from typing import NoReturn - -import product_metadata +from typing import Any, NoReturn ROOT = Path(__file__).resolve().parents[2] PRODUCT = "liboliphaunt-wasix" -SCHEMA = product_metadata.wasix_cargo_artifact_schema() -CRATES_IO_MAX_BYTES = 10 * 1024 * 1024 -EXTENSION_AOT_SPLIT_THRESHOLD_BYTES = 9 * 1024 * 1024 -RUNTIME_PACKAGE = product_metadata.wasix_runtime_package_name() -TOOLS_PACKAGE = product_metadata.wasix_tools_package_name() -ICU_PACKAGE = product_metadata.wasix_icu_package_name() -ICU_PAYLOAD_ARCHIVE = product_metadata.wasix_icu_payload_archive_name() -TOOLS_PAYLOAD_FILES = product_metadata.wasix_tools_payload_files() -CORE_RUNTIME_ARCHIVE_FILES = product_metadata.wasix_core_runtime_archive_files() -FORBIDDEN_RUNTIME_ARCHIVE_TOOL_FILES = product_metadata.wasix_forbidden_runtime_archive_tool_files() -TOOLS_AOT_ARTIFACTS = product_metadata.wasix_tools_aot_artifacts() -AOT_PACKAGES = product_metadata.wasix_aot_packages() -TOOLS_AOT_PACKAGES = product_metadata.wasix_tools_aot_packages() -AOT_TARGET_TRIPLES = product_metadata.wasix_aot_target_triples() -AOT_TARGET_CFGS = product_metadata.wasix_aot_target_cfgs() -EXPECTED_EXTENSION_AOT_TARGETS = frozenset(product_metadata.wasix_expected_extension_aot_targets()) + + +def release_graph_json(command: str, args: tuple[str, ...] = ()) -> Any: + try: + output = subprocess.check_output( + ["tools/dev/bun.sh", "tools/release/release_graph_query.mjs", command, *args], + cwd=ROOT, + text=True, + stderr=subprocess.PIPE, + ) + except subprocess.CalledProcessError as error: + detail = (error.stderr or "").strip() + if detail: + raise RuntimeError(f"release graph {command} query failed: {detail}") from error + raise RuntimeError(f"release graph {command} query failed with exit code {error.returncode}") from error + try: + return json.loads(output) + except json.JSONDecodeError as error: + raise RuntimeError(f"release graph {command} query did not return valid JSON: {error}") from error + + +@lru_cache(maxsize=None) +def release_graph_rows(command: str, args: tuple[str, ...] = ()) -> tuple[dict[str, Any], ...]: + rows = release_graph_json(command, args) + if not isinstance(rows, list) or not all(isinstance(row, dict) for row in rows): + raise RuntimeError(f"release graph {command} query must return a JSON object list") + return tuple(rows) + + +@lru_cache(maxsize=1) +def wasix_cargo_artifact_contract() -> dict[str, Any]: + contract = release_graph_json("wasix-cargo-artifact-contract") + if not isinstance(contract, dict): + raise RuntimeError("release graph wasix-cargo-artifact-contract query must return a JSON object") + return contract + + +def wasix_contract_string(key: str) -> str: + value = wasix_cargo_artifact_contract().get(key) + if not isinstance(value, str) or not value: + raise RuntimeError(f"WASIX Cargo artifact contract {key} must be a non-empty string") + return value + + +def wasix_contract_string_list(key: str) -> tuple[str, ...]: + value = wasix_cargo_artifact_contract().get(key) + if not isinstance(value, list) or not all(isinstance(item, str) and item for item in value): + raise RuntimeError(f"WASIX Cargo artifact contract {key} must be a string list") + return tuple(value) + + +def wasix_contract_string_map(key: str) -> dict[str, str]: + value = wasix_cargo_artifact_contract().get(key) + if not isinstance(value, dict) or not all( + isinstance(item_key, str) + and item_key + and isinstance(item_value, str) + and item_value + for item_key, item_value in value.items() + ): + raise RuntimeError(f"WASIX Cargo artifact contract {key} must be a string map") + return dict(value) + + +def wasix_cargo_artifact_schema() -> str: + return wasix_contract_string("schema") + + +def wasix_runtime_package_name() -> str: + return wasix_contract_string("runtimePackage") + + +def wasix_tools_package_name() -> str: + return wasix_contract_string("toolsPackage") + + +def wasix_icu_package_name() -> str: + return wasix_contract_string("icuPackage") + + +def wasix_icu_payload_archive_name() -> str: + return wasix_contract_string("icuPayloadArchive") + + +def wasix_tools_payload_files() -> tuple[str, ...]: + return wasix_contract_string_list("toolsPayloadFiles") + + +def wasix_core_runtime_archive_files() -> tuple[str, ...]: + return wasix_contract_string_list("coreRuntimeArchiveFiles") + + +def wasix_forbidden_runtime_archive_tool_files() -> tuple[str, ...]: + return wasix_contract_string_list("forbiddenRuntimeArchiveToolFiles") + + +def wasix_tools_aot_artifacts() -> set[str]: + return set(wasix_contract_string_list("toolsAotArtifacts")) + + +def wasix_aot_packages() -> dict[str, str]: + return wasix_contract_string_map("aotPackages") + + +def wasix_tools_aot_packages() -> dict[str, str]: + return wasix_contract_string_map("toolsAotPackages") + + +def wasix_aot_target_triples() -> dict[str, str]: + return wasix_contract_string_map("aotTargetTriples") + + +def wasix_aot_target_cfgs() -> dict[str, str]: + return wasix_contract_string_map("aotTargetCfgs") + + +def wasix_expected_extension_aot_targets() -> tuple[str, ...]: + return wasix_contract_string_list("expectedExtensionAotTargets") def public_cargo_package_names() -> tuple[str, ...]: - return product_metadata.wasix_public_cargo_package_names() + return wasix_contract_string_list("publicCargoPackageNames") def public_aot_cargo_dependencies() -> dict[str, str]: - return product_metadata.wasix_public_aot_cargo_dependencies() + return wasix_contract_string_map("publicAotCargoDependencies") def public_tools_aot_cargo_dependencies() -> dict[str, str]: - return product_metadata.wasix_public_tools_aot_cargo_dependencies() + return wasix_contract_string_map("publicToolsAotCargoDependencies") def public_tools_feature_dependencies() -> set[str]: - return product_metadata.wasix_public_tools_feature_dependencies() + return set(wasix_contract_string_list("publicToolsFeatureDependencies")) + + +@lru_cache(maxsize=1) +def wasix_extension_package_rows() -> tuple[dict[str, Any], ...]: + rows = release_graph_rows("wasix-extension-package-names") + seen: set[str] = set() + for row in rows: + product = row.get("product") + package_name = row.get("packageName") + aot_packages = row.get("aotPackages") + if not isinstance(product, str) or not product: + raise RuntimeError("release graph wasix-extension-package-names rows must declare a non-empty product") + if product in seen: + raise RuntimeError(f"release graph wasix-extension-package-names returned duplicate product {product}") + seen.add(product) + if not isinstance(package_name, str) or not package_name: + raise RuntimeError(f"release graph wasix-extension-package-names {product}.packageName must be non-empty") + if not isinstance(aot_packages, list) or not all(isinstance(item, dict) for item in aot_packages): + raise RuntimeError(f"release graph wasix-extension-package-names {product}.aotPackages must be an object list") + if not rows: + raise RuntimeError("release graph returned no WASIX extension package names") + return rows + + +def wasix_extension_package_contract(product: str) -> dict[str, Any]: + matches = [row for row in wasix_extension_package_rows() if row.get("product") == product] + if len(matches) != 1: + raise RuntimeError(f"release graph wasix-extension-package-names returned {len(matches)} rows for {product}") + return dict(matches[0]) + + +def wasix_extension_package_name(product: str) -> str: + return str(wasix_extension_package_contract(product).get("packageName")) + + +def wasix_extension_aot_package_name(product: str, target: str) -> str: + rows = wasix_extension_package_contract(product).get("aotPackages") + assert isinstance(rows, list) + matches = [row for row in rows if row.get("target") == target] + if len(matches) != 1: + raise RuntimeError(f"release graph returned {len(matches)} WASIX extension AOT package names for {product}/{target}") + package_name = matches[0].get("packageName") + if not isinstance(package_name, str) or not package_name: + raise RuntimeError(f"release graph wasix-extension-package-names {product}/{target}.packageName must be non-empty") + return package_name + + +def read_current_version(product: str) -> str: + rows = release_graph_rows("product-versions", ("--product", product)) + if len(rows) != 1: + raise RuntimeError(f"release graph product-versions query returned {len(rows)} rows for {product}") + version = rows[0].get("version") + if not isinstance(version, str) or not version: + raise RuntimeError(f"release graph product-versions {product}.version must be a non-empty string") + return version + + +SCHEMA = wasix_cargo_artifact_schema() +CRATES_IO_MAX_BYTES = 10 * 1024 * 1024 +EXTENSION_AOT_SPLIT_THRESHOLD_BYTES = 9 * 1024 * 1024 +RUNTIME_PACKAGE = wasix_runtime_package_name() +TOOLS_PACKAGE = wasix_tools_package_name() +ICU_PACKAGE = wasix_icu_package_name() +ICU_PAYLOAD_ARCHIVE = wasix_icu_payload_archive_name() +TOOLS_PAYLOAD_FILES = wasix_tools_payload_files() +CORE_RUNTIME_ARCHIVE_FILES = wasix_core_runtime_archive_files() +FORBIDDEN_RUNTIME_ARCHIVE_TOOL_FILES = wasix_forbidden_runtime_archive_tool_files() +TOOLS_AOT_ARTIFACTS = wasix_tools_aot_artifacts() +AOT_PACKAGES = wasix_aot_packages() +TOOLS_AOT_PACKAGES = wasix_tools_aot_packages() +AOT_TARGET_TRIPLES = wasix_aot_target_triples() +AOT_TARGET_CFGS = wasix_aot_target_cfgs() +EXPECTED_EXTENSION_AOT_TARGETS = frozenset(wasix_expected_extension_aot_targets()) @dataclass(frozen=True) @@ -821,7 +995,7 @@ def extension_aot_specs(extension_dir: Path, *, product: str, version: str, sql_ seen_targets.add(target) specs.append( ExtensionAotCargoSpec( - name=product_metadata.wasix_extension_aot_package_name(product, target), + name=wasix_extension_aot_package_name(product, target), version=version, sql_name=sql_name, target=target, @@ -846,7 +1020,7 @@ def extension_cargo_specs(extension_roots: list[Path]) -> list[ExtensionCargoSpe continue specs.append( ExtensionCargoSpec( - name=product_metadata.wasix_extension_package_name(str(product)), + name=wasix_extension_package_name(str(product)), product=str(product), version=str(version), sql_name=str(sql_name), @@ -1311,7 +1485,7 @@ def parse_args(argv: list[str]) -> argparse.Namespace: default="target/oliphaunt-wasix/cargo-artifacts", help="directory where generated .crate files are written", ) - parser.add_argument("--version", default=product_metadata.read_current_version(PRODUCT)) + parser.add_argument("--version", default=read_current_version(PRODUCT)) parser.add_argument( "--extension-artifact-root", action="append", From 8ea498d8ac5bf4510374117da0680726f4f785c7 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 20:30:04 +0000 Subject: [PATCH 222/308] chore: query release metadata checks via bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 19 + tools/policy/python-entrypoints.allowlist | 2 +- tools/release/check_release_metadata.py | 470 ++++++++++++++++-- 3 files changed, 451 insertions(+), 40 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index ee195fb8..f8191ab9 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,25 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Removed `check_release_metadata.py`'s import of the Python + `product_metadata.py` compatibility module. The release metadata checker now + reads product configs, version files, current versions, artifact targets, + publish-step target coverage, exact-extension metadata/targets, TypeScript + optional runtime package versions, and WASIX Cargo artifact contract data + through cached local wrappers over `release_graph_query.mjs`. The checker + now self-guards against reintroducing a direct `import product_metadata`, and + the Python entrypoint inventory rationale now records that this remaining + Python entrypoint consumes Bun release graph rows rather than the Python + compatibility API. Fresh checks passed: `python3 -m py_compile` for + `check_release_metadata.py`, AST smoke proving no `product_metadata` import + or executable attribute calls remain, direct helper smoke for + `liboliphaunt-wasix` product/version/WASIX package metadata and native + artifact targets, `tools/dev/bun.sh + tools/policy/check-python-entrypoints.mjs --json`, full `python3 + tools/release/check_release_metadata.py`, `bash + tools/policy/check-policy-tools.sh`, `bash + tools/policy/check-tooling-stack.sh`, `bash tools/policy/check-docs.sh`, and + `tools/release/release.py check`. - 2026-06-27: Removed `package_liboliphaunt_wasix_cargo_artifacts.py`'s import of the Python `product_metadata.py` compatibility module. The WASIX Cargo artifact packager now reads the portable runtime/tools/ICU/AOT diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index 68ba769f..f0aab234 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -5,7 +5,7 @@ src/extensions/tools/check-extension-model.py extensions defer-extension-model-p tools/policy/check-release-policy.py release-policy defer-release-graph-port guards CI and release policy against product metadata and the Bun CI planner during release-graph migration tools/release/check_artifact_targets.py release-metadata defer-release-graph-port validates release target coverage across workflow producers, product metadata, and package artifact handlers tools/release/check_consumer_shape.py release-consumer-shape defer-release-graph-port validates cross-SDK package/runtime/install shape from generated release fixtures and source invariants -tools/release/check_release_metadata.py release-metadata defer-release-graph-port validates release metadata and publish-step wiring against the Python release graph while it remains canonical +tools/release/check_release_metadata.py release-metadata defer-release-graph-port validates release metadata and publish-step wiring through cached Bun release graph query rows tools/release/local_registry_publish.py local-registry defer-local-registry-port publishes local Cargo, npm, Maven, and Swift registries from current release artifacts for e2e example validation tools/release/package_liboliphaunt_wasix_cargo_artifacts.py wasix-cargo-artifacts defer-wasix-packager-port generates split WASIX runtime, tools, ICU, and extension Cargo artifact crates with size-limit enforcement tools/release/product_metadata.py release-metadata defer-release-graph-port owns the Python compatibility product metadata API consumed by release tools while direct version reads use Bun diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 2f70d209..7b22c60f 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -5,13 +5,14 @@ import json import re +import subprocess import sys import tempfile import tomllib +from functools import lru_cache from pathlib import Path -from typing import NoReturn - -import product_metadata +from types import SimpleNamespace +from typing import Any, NoReturn ROOT = Path(__file__).resolve().parents[2] @@ -47,6 +48,395 @@ def read_text(path: str) -> str: return (ROOT / path).read_text(encoding="utf-8") +def release_graph_json(command: str, args: tuple[str, ...] = ()) -> Any: + try: + output = subprocess.check_output( + ["tools/dev/bun.sh", "tools/release/release_graph_query.mjs", command, *args], + cwd=ROOT, + text=True, + stderr=subprocess.PIPE, + ) + except subprocess.CalledProcessError as error: + detail = (error.stderr or "").strip() + if detail: + fail(f"release graph {command} query failed: {detail}") + fail(f"release graph {command} query failed with exit code {error.returncode}") + try: + return json.loads(output) + except json.JSONDecodeError as error: + fail(f"release graph {command} query did not return valid JSON: {error}") + + +@lru_cache(maxsize=None) +def release_graph_rows(command: str, args: tuple[str, ...] = ()) -> tuple[dict[str, Any], ...]: + rows = release_graph_json(command, args) + if not isinstance(rows, list) or not all(isinstance(row, dict) for row in rows): + fail(f"release graph {command} query must return a JSON object list") + return tuple(rows) + + +def string_list(config: dict[str, Any], key: str, product: str) -> list[str]: + value = config.get(key, []) + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + fail(f"{product}.{key} must be a string list") + return value + + +@lru_cache(maxsize=1) +def product_config_rows() -> tuple[dict[str, Any], ...]: + rows = release_graph_rows("product-configs") + seen: set[str] = set() + for row in rows: + product = row.get("product") + if not isinstance(product, str) or not product: + fail("release graph product-configs rows must declare a non-empty product") + if product in seen: + fail(f"release graph product-configs query returned duplicate product {product}") + seen.add(product) + if not rows: + fail("release graph product-configs query returned no products") + return rows + + +def product_config(product: str) -> dict[str, Any]: + matches = [row for row in product_config_rows() if row.get("product") == product] + if len(matches) != 1: + fail(f"release graph product-configs query returned {len(matches)} rows for {product}") + config = dict(matches[0]) + config.pop("product", None) + return config + + +def graph_products() -> dict[str, dict[str, Any]]: + return { + str(row["product"]): product_config(str(row["product"])) + for row in product_config_rows() + } + + +def product_ids() -> list[str]: + return [str(row["product"]) for row in product_config_rows()] + + +def version_files(product: str) -> list[str]: + files = string_list(product_config(product), "version_files", product) + for path in files: + if not (ROOT / path).is_file(): + fail(f"{product} version file does not exist: {path}") + return files + + +def derived_version_files(product: str) -> list[str]: + return string_list(product_config(product), "derived_version_files", product) + + +@lru_cache(maxsize=1) +def product_version_rows() -> tuple[dict[str, Any], ...]: + rows = release_graph_rows("product-versions") + seen: set[str] = set() + for row in rows: + product = row.get("product") + version = row.get("version") + if not isinstance(product, str) or not product: + fail("release graph product-versions rows must declare a non-empty product") + if not isinstance(version, str) or not version: + fail(f"release graph product-versions {product}.version must be a non-empty string") + if product in seen: + fail(f"release graph product-versions query returned duplicate product {product}") + seen.add(product) + if not rows: + fail("release graph product-versions query returned no products") + return rows + + +def read_current_version(product: str) -> str: + matches = [row for row in product_version_rows() if row.get("product") == product] + if len(matches) != 1: + fail(f"release graph product-versions query returned {len(matches)} rows for {product}") + version = matches[0].get("version") + if not isinstance(version, str) or not version: + fail(f"release graph product-versions {product}.version must be a non-empty string") + return version + + +def artifact_target_args( + *, + product: str | None = None, + kind: str | None = None, + surface: str | None = None, + published_only: bool = False, +) -> tuple[str, ...]: + args: list[str] = [] + if product is not None: + args.extend(["--product", product]) + if kind is not None: + args.extend(["--kind", kind]) + if surface is not None: + args.extend(["--surface", surface]) + if published_only: + args.append("--published-only") + return tuple(args) + + +def artifact_targets( + *, + product: str | None = None, + kind: str | None = None, + surface: str | None = None, + published_only: bool = False, +) -> tuple[SimpleNamespace, ...]: + optional_defaults = { + "triple": None, + "runner": None, + "library_relative_path": None, + "executable_relative_path": None, + "npm_package": None, + "npm_os": None, + "npm_cpu": None, + "npm_libc": None, + "llvm_url": None, + "extension_artifacts": True, + } + return tuple( + SimpleNamespace(**{**optional_defaults, **row}) + for row in release_graph_rows( + "artifact-targets", + artifact_target_args( + product=product, + kind=kind, + surface=surface, + published_only=published_only, + ), + ) + ) + + +def publish_step_target_coverage(product: str) -> dict[str, set[str]]: + coverage: dict[str, set[str]] = {} + for row in release_graph_rows("publish-step-target-coverage", ("--product", product)): + product_id = row.get("product") + step = row.get("step") + publish_targets = row.get("publishTargets") + if product_id != product: + fail(f"release graph publish-step-target-coverage returned row for {product_id!r}, expected {product!r}") + if not isinstance(step, str) or not step: + fail(f"release graph publish-step-target-coverage {product}.step must be a non-empty string") + if not isinstance(publish_targets, list) or not publish_targets or not all( + isinstance(item, str) and item for item in publish_targets + ): + fail(f"release graph publish-step-target-coverage {product}.{step}.publishTargets must be a non-empty string list") + coverage[step] = set(publish_targets) + return coverage + + +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 is_extension_product(product: str) -> bool: + rows = release_graph_rows("publish-step-target-coverage", ("--product", product)) + if not rows: + return product.startswith("oliphaunt-extension-") + return bool(rows[0].get("extension")) + + +@lru_cache(maxsize=1) +def extension_metadata_rows() -> tuple[dict[str, Any], ...]: + rows = release_graph_rows("extension-metadata") + seen: set[str] = set() + for row in rows: + product = row.get("product") + if not isinstance(product, str) or not product: + fail("release graph extension-metadata rows must declare a non-empty product") + if product in seen: + fail(f"release graph extension-metadata query returned duplicate product {product}") + seen.add(product) + if not rows: + fail("release graph extension-metadata query returned no products") + return rows + + +def extension_product_ids() -> list[str]: + return sorted(str(row["product"]) for row in extension_metadata_rows()) + + +def validate_all_extension_metadata() -> None: + for row in extension_metadata_rows(): + product = str(row["product"]) + for key in ["sqlName", "class", "versioning", "sourcePath"]: + value = row.get(key) + if not isinstance(value, str) or not value: + fail(f"release graph extension-metadata {product}.{key} must be a non-empty string") + compatibility = row.get("compatibility") + if not isinstance(compatibility, dict): + fail(f"release graph extension-metadata {product}.compatibility must be an object") + source_identity = row.get("sourceIdentity") + if not isinstance(source_identity, dict): + fail(f"release graph extension-metadata {product}.sourceIdentity must be an object") + + +def extension_artifact_targets( + *, + product: str | None = None, + family: str | None = None, + published_only: bool = False, +) -> tuple[SimpleNamespace, ...]: + args: list[str] = [] + if product is not None: + args.extend(["--product", product]) + if family is not None: + args.extend(["--family", family]) + if published_only: + args.append("--published-only") + return tuple(SimpleNamespace(**row) for row in release_graph_rows("extension-targets", tuple(args))) + + +def published_android_maven_targets(product: str) -> tuple[SimpleNamespace, ...]: + return tuple( + sorted( + ( + target + for target in extension_artifact_targets( + product=product, + family="native", + published_only=True, + ) + if target.kind == "native-static-registry" and target.target.startswith("android-") + ), + key=lambda target: target.target, + ) + ) + + +def typescript_optional_runtime_package_versions() -> dict[str, str]: + versions: dict[str, str] = {} + for row in release_graph_rows("typescript-optional-runtime-package-versions"): + package_name = row.get("packageName") + version = row.get("version") + if not isinstance(package_name, str) or not package_name: + fail("typescript-optional-runtime-package-versions rows must declare a non-empty packageName") + if not isinstance(version, str) or not version: + fail(f"typescript-optional-runtime-package-versions {package_name}.version must be non-empty") + if package_name in versions: + fail(f"duplicate TypeScript optional runtime package target {package_name}") + versions[package_name] = version + if not versions: + fail("release graph returned no TypeScript optional runtime package versions") + return versions + + +@lru_cache(maxsize=1) +def wasix_cargo_artifact_contract() -> dict[str, Any]: + contract = release_graph_json("wasix-cargo-artifact-contract") + if not isinstance(contract, dict): + fail("release graph wasix-cargo-artifact-contract query must return a JSON object") + return contract + + +def wasix_contract_string_list(key: str) -> tuple[str, ...]: + return tuple(string_list(wasix_cargo_artifact_contract(), key, f"WASIX Cargo artifact contract {key}")) + + +def wasix_contract_string_map(key: str) -> dict[str, str]: + value = wasix_cargo_artifact_contract().get(key) + if not isinstance(value, dict) or not all( + isinstance(item_key, str) + and item_key + and isinstance(item_value, str) + and item_value + for item_key, item_value in value.items() + ): + fail(f"WASIX Cargo artifact contract {key} must be a string map") + return dict(value) + + +def wasix_public_cargo_package_names() -> tuple[str, ...]: + return wasix_contract_string_list("publicCargoPackageNames") + + +def wasix_public_aot_cargo_dependencies() -> dict[str, str]: + return wasix_contract_string_map("publicAotCargoDependencies") + + +def wasix_public_tools_aot_cargo_dependencies() -> dict[str, str]: + return wasix_contract_string_map("publicToolsAotCargoDependencies") + + +def wasix_public_tools_feature_dependencies() -> set[str]: + return set(wasix_contract_string_list("publicToolsFeatureDependencies")) + + +def wasix_core_runtime_archive_files() -> tuple[str, ...]: + return wasix_contract_string_list("coreRuntimeArchiveFiles") + + +def wasix_tools_payload_files() -> tuple[str, ...]: + return wasix_contract_string_list("toolsPayloadFiles") + + +def wasix_forbidden_runtime_archive_tool_files() -> tuple[str, ...]: + return wasix_contract_string_list("forbiddenRuntimeArchiveToolFiles") + + +def wasix_tools_aot_artifacts() -> set[str]: + return set(wasix_contract_string_list("toolsAotArtifacts")) + + +def wasix_expected_extension_aot_targets() -> tuple[str, ...]: + return wasix_contract_string_list("expectedExtensionAotTargets") + + +@lru_cache(maxsize=1) +def wasix_extension_package_rows() -> tuple[dict[str, Any], ...]: + rows = release_graph_rows("wasix-extension-package-names") + seen: set[str] = set() + for row in rows: + product = row.get("product") + package_name = row.get("packageName") + aot_packages = row.get("aotPackages") + if not isinstance(product, str) or not product: + fail("release graph wasix-extension-package-names rows must declare a non-empty product") + if product in seen: + fail(f"release graph wasix-extension-package-names returned duplicate product {product}") + seen.add(product) + if not isinstance(package_name, str) or not package_name: + fail(f"release graph wasix-extension-package-names {product}.packageName must be non-empty") + if not isinstance(aot_packages, list) or not all(isinstance(item, dict) for item in aot_packages): + fail(f"release graph wasix-extension-package-names {product}.aotPackages must be an object list") + if not rows: + fail("release graph returned no WASIX extension package names") + return rows + + +def wasix_extension_package_contract(product: str) -> dict[str, Any]: + matches = [row for row in wasix_extension_package_rows() if row.get("product") == product] + if len(matches) != 1: + fail(f"release graph wasix-extension-package-names returned {len(matches)} rows for {product}") + return dict(matches[0]) + + +def wasix_extension_package_name(product: str) -> str: + package_name = wasix_extension_package_contract(product).get("packageName") + if not isinstance(package_name, str) or not package_name: + fail(f"release graph wasix-extension-package-names {product}.packageName must be non-empty") + return package_name + + +def wasix_extension_aot_package_name(product: str, target: str) -> str: + rows = wasix_extension_package_contract(product).get("aotPackages") + assert isinstance(rows, list) + matches = [row for row in rows if row.get("target") == target] + if len(matches) != 1: + fail(f"release graph returned {len(matches)} WASIX extension AOT package names for {product}/{target}") + package_name = matches[0].get("packageName") + if not isinstance(package_name, str) or not package_name: + fail(f"release graph wasix-extension-package-names {product}/{target}.packageName must be non-empty") + return package_name + + def require_text(path: str, needle: str, message: str) -> None: if needle not in read_text(path): fail(message) @@ -114,7 +504,7 @@ def validate_platform_npm_packages( package_dirs = npm_package_dirs_under(package_root) targets = [ target - for target in product_metadata.artifact_targets(product=product, kind=kind, surface=surface, published_only=True) + for target in artifact_targets(product=product, kind=kind, surface=surface, published_only=True) if target.npm_package is not None ] expected_packages = sorted(target.npm_package for target in targets if target.npm_package is not None) @@ -211,15 +601,15 @@ def cargo_manifest_version(path: str) -> str: def validate_graph_files() -> None: - products = product_metadata.graph_products() + products = graph_products() for product in products: for path in [ - *product_metadata.version_files(product), - *product_metadata.derived_version_files(product), + *version_files(product), + *derived_version_files(product), ]: if not (ROOT / path).is_file(): fail(f"{product} release metadata path does not exist: {path}") - product_metadata.validate_all_extension_metadata() + validate_all_extension_metadata() product_metadata_source = read_text("tools/release/product_metadata.py") release_graph_query = read_text("tools/release/release_graph_query.mjs") release_graph_source = read_text("tools/release/release-graph.mjs") @@ -236,6 +626,8 @@ def validate_graph_files() -> None: extension_artifacts_wasix_moon = read_text("src/extensions/artifacts/wasix/moon.yml") release_policy = read_text("tools/policy/check-release-policy.py") check_release_metadata_source = read_text("tools/release/check_release_metadata.py") + if re.search(r"(?m)^import product_metadata$", check_release_metadata_source): + fail("check_release_metadata.py must consume Bun release graph rows instead of importing product_metadata.py") if ( "_release_metadata(product).get(\"compatibility_versions\"" in product_metadata_source or "_compatibility_version_entries(" in product_metadata_source @@ -421,14 +813,14 @@ def validate_graph_files() -> None: def validate_exact_extension_registry_shape() -> None: - for product in product_metadata.extension_product_ids(): - config = product_metadata.product_config(product) + for product in extension_product_ids(): + config = product_config(product) 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)) + publish_targets = set(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) + registry_packages = 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( @@ -437,7 +829,7 @@ def validate_exact_extension_registry_shape() -> None: ) expected_registry_packages = { f"maven:dev.oliphaunt.extensions:{product}-{target.target}" - for target in product_metadata.published_android_maven_targets(product) + for target in published_android_maven_targets(product) } if set(registry_packages) != expected_registry_packages: fail( @@ -446,20 +838,20 @@ def validate_exact_extension_registry_shape() -> None: ) android_targets = { target.target - for target in product_metadata.published_android_maven_targets(product) + for target in published_android_maven_targets(product) } if android_targets != {"android-arm64-v8a", "android-x86_64"}: fail(f"{product} derived Android Maven targets are wrong: {sorted(android_targets)}") - for target in product_metadata.extension_artifact_targets(product=product, published_only=True): + for target in extension_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 = product_metadata.wasix_extension_package_name(product) + wasix_package = 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 product_metadata.wasix_expected_extension_aot_targets(): - package = product_metadata.wasix_extension_aot_package_name(product, target) + for target in wasix_expected_extension_aot_targets(): + package = 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}") @@ -474,16 +866,16 @@ def validate_publish_target_coverage() -> None: 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().items(): - declared = set(product_metadata.string_list(config, "publish_targets", product)) - supported = product_metadata.supported_publish_targets(product) + for product, config in graph_products().items(): + declared = set(string_list(config, "publish_targets", product)) + supported = 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 = product_metadata.publish_step_target_coverage(product) - if product_metadata.is_extension_product(product): + step_coverage = publish_step_target_coverage(product) + if is_extension_product(product): saw_extension = True continue for step in step_coverage: @@ -1277,7 +1669,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 = product_metadata.typescript_optional_runtime_package_versions() + expected_optional = 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") @@ -1579,11 +1971,11 @@ def version_file_value(path: str) -> str: def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None: - runtime_version_files = product_metadata.version_files("liboliphaunt-wasix") + runtime_version_files = version_files("liboliphaunt-wasix") for path in runtime_version_files: if version_file_value(path) != wasix_runtime_version: fail(f"{path} must use liboliphaunt-wasix runtime version {wasix_runtime_version}") - binding_version_files = product_metadata.version_files("oliphaunt-wasix-rust") + binding_version_files = version_files("oliphaunt-wasix-rust") for path in binding_version_files: if version_file_value(path) != wasm_binding_version: fail(f"{path} must use oliphaunt-wasix binding version {wasm_binding_version}") @@ -1609,10 +2001,10 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None ): fail("oliphaunt-wasix source must optionally depend on the local oliphaunt-icu path crate version") expected_aot_dependencies = ( - product_metadata.wasix_public_aot_cargo_dependencies() + wasix_public_aot_cargo_dependencies() ) expected_tools_aot_dependencies = ( - product_metadata.wasix_public_tools_aot_cargo_dependencies() + wasix_public_tools_aot_cargo_dependencies() ) target_tables = manifest.get("target", {}) for cfg, crate in expected_aot_dependencies.items(): @@ -1632,7 +2024,7 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None ): fail(f"oliphaunt-wasix must optionally depend on {crate} at the exact liboliphaunt-wasix runtime version behind {cfg}") expected_tools_feature = ( - product_metadata.wasix_public_tools_feature_dependencies() + wasix_public_tools_feature_dependencies() ) tools_feature = set(manifest.get("features", {}).get("tools", [])) if tools_feature != expected_tools_feature: @@ -1667,13 +2059,13 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None 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 ( - product_metadata.wasix_core_runtime_archive_files() + wasix_core_runtime_archive_files() != ("oliphaunt/bin/initdb", "oliphaunt/bin/postgres") - or product_metadata.wasix_tools_payload_files() + or wasix_tools_payload_files() != ("bin/pg_dump.wasix.wasm", "bin/psql.wasix.wasm") - or product_metadata.wasix_forbidden_runtime_archive_tool_files() + or wasix_forbidden_runtime_archive_tool_files() != ("oliphaunt/bin/pg_ctl", "oliphaunt/bin/pg_dump", "oliphaunt/bin/psql") - or product_metadata.wasix_tools_aot_artifacts() + or wasix_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 @@ -1766,14 +2158,14 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None or "cargo::metadata=" not in build_source ): fail("oliphaunt-wasix must relay WASIX Cargo artifact manifests through a Cargo links build script") - runtime_config = product_metadata.product_config("liboliphaunt-wasix") - publish_targets = product_metadata.string_list(runtime_config, "publish_targets", "liboliphaunt-wasix") + runtime_config = product_config("liboliphaunt-wasix") + publish_targets = string_list(runtime_config, "publish_targets", "liboliphaunt-wasix") if publish_targets != ["github-release-assets", "crates-io"]: 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")) + registry_packages = set(string_list(runtime_config, "registry_packages", "liboliphaunt-wasix")) expected_registry_packages = { f"crates:{name}" - for name in product_metadata.wasix_public_cargo_package_names() + for name in wasix_public_cargo_package_names() } if registry_packages != expected_registry_packages: fail( @@ -1812,8 +2204,8 @@ def main() -> int: validate_local_registry_publisher() versions = { - product: product_metadata.read_current_version(product) - for product in product_metadata.product_ids() + product: read_current_version(product) + for product in product_ids() } for product, version in versions.items(): stable_version(version, product) From d4c5677018acf083124f912cbee820e08bb65228 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 20:45:39 +0000 Subject: [PATCH 223/308] chore: query release cli metadata via bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 24 + tools/policy/check-release-policy.py | 2 +- tools/release/check_release_metadata.py | 9 +- tools/release/release.py | 627 ++++++++++++++++-- 4 files changed, 608 insertions(+), 54 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index f8191ab9..e07d857b 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,30 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Removed `release.py`'s import of the Python + `product_metadata.py` compatibility module. The release orchestrator now + reads product configs, current versions, publish-step target coverage, + artifact targets, registry package names, expected release assets, CI + artifact names, SDK package products, extension metadata/targets, and the + WASIX Cargo artifact contract through cached local wrappers over + `release_graph_query.mjs`. `check_release_metadata.py` now rejects + reintroducing the compatibility import in `release.py`, and + `check-release-policy.py` now requires staged WASIX asset validation to use + `expected_assets(...)` from the release graph adapter. Fresh checks passed: + grep proving `release.py` has no `import product_metadata` or + `product_metadata.*` calls, `python3 -m py_compile` for the touched Python + helpers, `release.py ci-products --family sdk-package`, `release.py + ci-artifacts` smokes for `liboliphaunt-native` release assets, + `oliphaunt-node-direct` npm packages, `oliphaunt-rust` SDK packages, and + `oliphaunt-broker` release assets, clean adapter failure reporting for an + invalid npm-package query, `check_release_metadata.py`, + `check-release-policy.py`, `tools/dev/bun.sh + tools/policy/check-python-entrypoints.mjs --json`, and full + `tools/release/release.py check`. The Python entrypoint inventory remains at + 9 entries; + `release.py` is now 3,894 lines and 158,160 bytes, while + `product_metadata.py` remains a compatibility adapter at 970 lines and + 37,950 bytes. - 2026-06-27: Removed `check_release_metadata.py`'s import of the Python `product_metadata.py` compatibility module. The release metadata checker now reads product configs, version files, current versions, artifact targets, diff --git a/tools/policy/check-release-policy.py b/tools/policy/check-release-policy.py index 7544d416..17d21813 100644 --- a/tools/policy/check-release-policy.py +++ b/tools/policy/check-release-policy.py @@ -1105,7 +1105,7 @@ def check_release_workflow_policy() -> None: ) for snippet in ( "validate_wasix_release_assets", - "product_metadata.expected_assets(product, version, surface=\"github-release\")", + "expected_assets(product, version, surface=\"github-release\")", "parse_local_checksum_manifest", "target/oliphaunt-wasix/release-assets", "validate_wasix_release_asset_contents", diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 7b22c60f..554724d6 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -698,12 +698,13 @@ def validate_graph_files() -> None: if ( "publish-step-target-coverage [--product PRODUCT]" not in release_graph_query or "export function publishStepTargetCoverageRows(" not in release_graph_source - or "product_metadata.publish_step_target_coverage(product)" not in release_source - or "product_metadata.supported_publish_targets(product)" not in release_source + or 'release_graph_rows("publish-step-target-coverage", args)' not in release_source + or "def publish_step_target_coverage(product: str)" not in release_source + or "import product_metadata" in release_source or '"liboliphaunt-native": {' in release_source or 'return {"github-release-assets": {"github-release-assets"}' in release_source ): - fail("publish target coverage must be shared through the Bun release graph query instead of duplicated in release.py") + fail("release.py publish target coverage must be adapted through the Bun release graph query") if ( '"moon-release-metadata"' not in product_metadata_source or "moon-release-metadata [--product PRODUCT]" not in release_graph_query @@ -1379,7 +1380,7 @@ def validate_kotlin(kotlin_version: str, liboliphaunt_version: str) -> None: ) require_text( "tools/release/release.py", - 'product_metadata.registry_package_names("oliphaunt-kotlin", "maven")', + 'registry_package_names("oliphaunt-kotlin", "maven")', "Kotlin Maven release idempotency probes must derive package coordinates from release metadata", ) reject_text( diff --git a/tools/release/release.py b/tools/release/release.py index d1c8d26f..9dc132ef 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -14,10 +14,11 @@ import tarfile import time import zipfile +from dataclasses import dataclass +from functools import lru_cache from pathlib import Path, PurePosixPath -from typing import NoReturn - -import product_metadata +from types import SimpleNamespace +from typing import Any, Iterable, NoReturn ROOT = Path(__file__).resolve().parents[2] @@ -35,6 +36,30 @@ NATIVE_PACKAGED_TOOL_STEMS = (*NATIVE_RUNTIME_TOOL_STEMS, *NATIVE_TOOLS_TOOL_STEMS) LIBOLIPHAUNT_NATIVE_CARGO_PRODUCT = "liboliphaunt-native" LIBOLIPHAUNT_TOOLS_PRODUCT = "oliphaunt-tools" +PUBLIC_EXTENSION_RELEASE_MANIFEST_KEYS = { + "schema", + "product", + "version", + "sqlName", + "extensionClass", + "versioning", + "sourceIdentity", + "compatibility", + "dependencies", + "nativeModuleStem", + "sharedPreloadLibraries", + "mobileReleaseReady", + "desktopReleaseReady", + "assets", +} +PUBLIC_EXTENSION_RELEASE_ASSET_KEYS = { + "name", + "family", + "target", + "kind", + "sha256", + "bytes", +} def liboliphaunt_cargo_package_name(target_id: str, package_base: str = LIBOLIPHAUNT_NATIVE_CARGO_PRODUCT) -> str: @@ -58,6 +83,500 @@ def bun_json(args: list[str]) -> object: return json.loads(output) +@dataclass(frozen=True) +class ArtifactTarget: + id: str + product: str + kind: str + target: str + asset: str + published: bool + surfaces: tuple[str, ...] + triple: str | None = None + runner: str | None = None + library_relative_path: str | None = None + executable_relative_path: str | None = None + npm_package: str | None = None + npm_os: str | None = None + npm_cpu: str | None = None + npm_libc: str | None = None + llvm_url: str | None = None + extension_artifacts: bool = True + + def asset_name(self, version: str) -> str: + return self.asset.format(version=version) + + +@lru_cache(maxsize=None) +def release_graph_json(command: str, args: tuple[str, ...] = ()) -> Any: + try: + output = subprocess.check_output( + ["tools/dev/bun.sh", "tools/release/release_graph_query.mjs", command, *args], + cwd=ROOT, + text=True, + stderr=subprocess.PIPE, + ) + except subprocess.CalledProcessError as error: + detail = (error.stderr or "").strip() + if detail: + fail(f"release graph {command} query failed: {detail}") + fail(f"release graph {command} query failed with exit code {error.returncode}") + return json.loads(output) + + +@lru_cache(maxsize=None) +def release_graph_rows(command: str, args: tuple[str, ...] = ()) -> tuple[dict[str, Any], ...]: + rows = release_graph_json(command, args) + if not isinstance(rows, list) or not all(isinstance(row, dict) for row in rows): + fail(f"release graph {command} query must return a JSON object list") + return tuple(rows) + + +@lru_cache(maxsize=None) +def product_config_rows(product: str | None = None) -> tuple[dict[str, Any], ...]: + args = () if product is None else ("--product", product) + rows = release_graph_rows("product-configs", args) + if product is not None and len(rows) != 1: + fail(f"release graph product-configs query must return one row for {product}, got {len(rows)}") + seen: set[str] = set() + parsed: list[dict[str, Any]] = [] + for row in rows: + product_id = row.get("product") + config_id = row.get("id") + if not isinstance(product_id, str) or not product_id: + fail("release graph product-configs rows must declare a non-empty product") + if product_id in seen: + fail(f"release graph product-configs query returned duplicate product {product_id}") + seen.add(product_id) + if config_id != product_id: + fail(f"release graph product-configs {product_id}.id must match the product id") + for key in ["kind", "owner", "path", "changelog_path", "tag_prefix"]: + value = row.get(key) + if not isinstance(value, str) or not value: + fail(f"release graph product-configs {product_id}.{key} must be a non-empty string") + for key in ["publish_targets", "release_artifacts", "version_files"]: + value = row.get(key) + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + fail(f"release graph product-configs {product_id}.{key} must be a string list") + if not value: + fail(f"release graph product-configs {product_id}.{key} must not be empty") + for key in ["registry_packages", "derived_version_files"]: + value = row.get(key) + if value is None: + row[key] = [] + continue + if not isinstance(value, list) or not all(isinstance(item, str) for item in value): + fail(f"release graph product-configs {product_id}.{key} must be a string list") + parsed.append(dict(row)) + if not parsed: + fail("release graph returned no product config rows") + return tuple(parsed) + + +def product_config(product: str) -> dict[str, Any]: + row = dict(product_config_rows(product)[0]) + row.pop("product", None) + return row + + +def product_ids() -> list[str]: + return [str(row["product"]) for row in product_config_rows()] + + +def package_path(product: str) -> str: + value = product_config(product).get("path") + if not isinstance(value, str) or not value: + fail(f"release graph product {product!r} must declare a package path") + return value + + +def tag_prefix(product: str) -> str: + value = product_config(product).get("tag_prefix") + if not isinstance(value, str) or not value: + fail(f"release graph product {product}.tag_prefix must be a non-empty string") + return value + + +@lru_cache(maxsize=1) +def product_version_rows() -> tuple[dict[str, Any], ...]: + return release_graph_rows("product-versions") + + +def read_current_version(product: str) -> str: + matches = [row for row in product_version_rows() if row.get("product") == product] + if len(matches) != 1: + fail(f"release graph product-versions query must return one row for {product}, got {len(matches)}") + version = matches[0].get("version") + if not isinstance(version, str) or not version: + fail(f"release graph product-versions {product}.version must be a non-empty string") + return version + + +@lru_cache(maxsize=None) +def publish_step_target_coverage_rows(product: str | None = None) -> tuple[dict[str, Any], ...]: + args = () if product is None else ("--product", product) + rows = release_graph_rows("publish-step-target-coverage", args) + seen: set[tuple[str, str]] = set() + parsed: list[dict[str, Any]] = [] + for row in rows: + product_id = row.get("product") + step = row.get("step") + publish_targets = row.get("publishTargets") + extension = row.get("extension") + if not isinstance(product_id, str) or not product_id: + fail("release graph publish-step-target-coverage rows must declare a non-empty product") + if product is not None and product_id != product: + fail(f"release graph publish-step-target-coverage returned row for {product_id}, expected {product}") + if not isinstance(step, str) or not step: + fail(f"release graph publish-step-target-coverage {product_id}.step must be a non-empty string") + if not isinstance(publish_targets, list) or not publish_targets or not all( + isinstance(item, str) and item for item in publish_targets + ): + fail( + f"release graph publish-step-target-coverage {product_id}.{step}.publishTargets " + "must be a non-empty string list" + ) + if not isinstance(extension, bool): + fail(f"release graph publish-step-target-coverage {product_id}.{step}.extension must be true or false") + key = (product_id, step) + if key in seen: + fail(f"release graph publish-step-target-coverage returned duplicate row for {product_id}.{step}") + seen.add(key) + parsed.append(dict(row)) + return tuple(parsed) + + +def _target_string(row: dict[str, Any], key: str, target_id: str, *, required: bool = True) -> str | None: + value = row.get(key) + if isinstance(value, str) and value: + return value + if required: + fail(f"artifact target {target_id}.{key} must be a non-empty string") + if value is not None: + fail(f"artifact target {target_id}.{key} must be a string") + return None + + +def _target_bool(row: dict[str, Any], key: str, target_id: str, *, default: bool | None = None) -> bool: + value = row.get(key) + if isinstance(value, bool): + return value + if value is None and default is not None: + return default + fail(f"artifact target {target_id}.{key} must be true or false") + + +def _target_surfaces(row: dict[str, Any], target_id: str) -> tuple[str, ...]: + value = row.get("surfaces") + if not isinstance(value, list) or not value or not all(isinstance(item, str) and item for item in value): + fail(f"artifact target {target_id}.surfaces must be a non-empty string list") + return tuple(value) + + +def artifact_target_from_row(row: dict[str, Any]) -> ArtifactTarget: + target_id = _target_string(row, "id", "") + assert target_id is not None + return ArtifactTarget( + id=target_id, + product=_target_string(row, "product", target_id) or "", + kind=_target_string(row, "kind", target_id) or "", + target=_target_string(row, "target", target_id) or "", + asset=_target_string(row, "asset", target_id) or "", + published=_target_bool(row, "published", target_id), + surfaces=_target_surfaces(row, target_id), + triple=_target_string(row, "triple", target_id, required=False), + runner=_target_string(row, "runner", target_id, required=False), + library_relative_path=_target_string(row, "library_relative_path", target_id, required=False), + executable_relative_path=_target_string(row, "executable_relative_path", target_id, required=False), + npm_package=_target_string(row, "npm_package", target_id, required=False), + npm_os=_target_string(row, "npm_os", target_id, required=False), + npm_cpu=_target_string(row, "npm_cpu", target_id, required=False), + npm_libc=_target_string(row, "npm_libc", target_id, required=False), + llvm_url=_target_string(row, "llvm_url", target_id, required=False), + extension_artifacts=_target_bool(row, "extension_artifacts", target_id, default=True), + ) + + +def artifact_targets( + *, + product: str | None = None, + kind: str | None = None, + surface: str | None = None, + published_only: bool = False, +) -> list[ArtifactTarget]: + args: list[str] = [] + if product is not None: + args.extend(["--product", product]) + if kind is not None: + args.extend(["--kind", kind]) + if surface is not None: + args.extend(["--surface", surface]) + if published_only: + args.append("--published-only") + return [artifact_target_from_row(row) for row in release_graph_rows("artifact-targets", tuple(args))] + + +@lru_cache(maxsize=1) +def wasix_cargo_artifact_contract() -> dict[str, Any]: + value = release_graph_json("wasix-cargo-artifact-contract") + if not isinstance(value, dict): + fail("release graph wasix-cargo-artifact-contract query must return a JSON object") + return value + + +def wasix_contract_string(key: str) -> str: + value = wasix_cargo_artifact_contract().get(key) + if not isinstance(value, str) or not value: + fail(f"WASIX Cargo artifact contract {key} must be a non-empty string") + return value + + +def wasix_contract_string_list(key: str) -> tuple[str, ...]: + value = wasix_cargo_artifact_contract().get(key) + if not isinstance(value, list) or not all(isinstance(item, str) and item for item in value): + fail(f"WASIX Cargo artifact contract {key} must be a string list") + return tuple(value) + + +def wasix_cargo_artifact_schema() -> str: + return wasix_contract_string("schema") + + +def wasix_public_cargo_package_names() -> tuple[str, ...]: + return wasix_contract_string_list("publicCargoPackageNames") + + +@lru_cache(maxsize=None) +def expected_asset_rows( + product: str, + version: str, + surface: str, + published_only: bool, + kinds: tuple[str, ...] | None, +) -> tuple[dict[str, Any], ...]: + args: list[str] = ["--product", product, "--version", version, "--surface", surface] + if not published_only: + args.append("--include-unpublished") + if kinds is not None: + for kind in kinds: + args.extend(["--kind", kind]) + return release_graph_rows("expected-assets", tuple(args)) + + +def expected_assets( + product: str, + version: str, + *, + surface: str = "github-release", + published_only: bool = True, + kinds: Iterable[str] | None = None, +) -> list[str]: + kind_tuple = None if kinds is None else tuple(sorted(set(kinds))) + names: list[str] = [] + for row in expected_asset_rows(product, version, surface, published_only, kind_tuple): + asset_name = row.get("assetName") + artifact_target = row.get("artifactTarget") + row_product = row.get("product") + kind = row.get("kind") + if not isinstance(asset_name, str) or not asset_name: + fail(f"release graph expected-assets {product}/{surface} assetName must be a non-empty string") + if not isinstance(artifact_target, str) or not artifact_target: + fail(f"release graph expected-assets {asset_name}.artifactTarget must be a non-empty string") + if not isinstance(row_product, str) or not row_product: + fail(f"release graph expected-assets {asset_name}.product must be a non-empty string") + if not isinstance(kind, str) or not kind: + fail(f"release graph expected-assets {asset_name}.kind must be a non-empty string") + names.append(asset_name) + if len(names) != len(set(names)): + fail(f"release graph expected-assets returned duplicate names for {product}/{surface}") + if not names: + fail(f"release graph returned no expected assets for {product}/{surface}") + return sorted(names) + + +@lru_cache(maxsize=None) +def ci_artifact_name_rows(family: str, product: str, kind: str) -> tuple[dict[str, Any], ...]: + return release_graph_rows( + "ci-artifact-names", + ("--family", family, "--product", product, "--kind", kind), + ) + + +def ci_artifact_names(family: str, product: str, kind: str) -> list[str]: + names: list[str] = [] + for row in ci_artifact_name_rows(family, product, kind): + artifact_name = row.get("artifactName") + artifact_target = row.get("artifactTarget") + if row.get("family") != family or row.get("product") != product or row.get("kind") != kind: + fail(f"release graph ci-artifact-names returned an unexpected row for {family}/{product}/{kind}") + if not isinstance(artifact_name, str) or not artifact_name: + fail(f"release graph ci-artifact-names {family}/{product}/{kind} artifactName must be a non-empty string") + if not isinstance(artifact_target, str) or not artifact_target: + fail(f"release graph ci-artifact-names {family}/{product}/{kind} artifactTarget must be a non-empty string") + names.append(artifact_name) + if len(names) != len(set(names)): + fail(f"release graph ci-artifact-names returned duplicate artifacts for {family}/{product}/{kind}") + if not names: + fail(f"release graph returned no CI artifact names for {family}/{product}/{kind}") + return sorted(names) + + +def ci_release_asset_artifact_names(product: str, kind: str) -> list[str]: + return ci_artifact_names("release-assets", product, kind) + + +def ci_npm_package_artifact_names(product: str, kind: str) -> list[str]: + return ci_artifact_names("npm-package", product, kind) + + +@lru_cache(maxsize=1) +def sdk_package_product_rows() -> tuple[dict[str, Any], ...]: + return release_graph_rows("sdk-package-products") + + +def sdk_package_product_row(product: str) -> dict[str, Any]: + matches = [row for row in sdk_package_product_rows() if row.get("product") == product] + if len(matches) != 1: + fail(f"release graph sdk-package-products query must return one row for SDK product {product}, got {len(matches)}") + return dict(matches[0]) + + +def sdk_package_product_string(row: dict[str, Any], key: str, product: str) -> str: + value = row.get(key) + if not isinstance(value, str) or not value: + fail(f"release graph sdk-package-products {product}.{key} must be a non-empty string") + return value + + +def ci_sdk_package_artifact_name(product: str) -> str: + return sdk_package_product_string(sdk_package_product_row(product), "artifactName", product) + + +def sdk_package_products() -> tuple[str, ...]: + products = tuple(sdk_package_product_string(row, "product", "") for row in sdk_package_product_rows()) + if len(products) != len(set(products)): + fail("release graph sdk-package-products query returned duplicate SDK products") + if not products: + fail("release graph returned no SDK package products") + return products + + +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()] + + +@lru_cache(maxsize=None) +def registry_package_rows(product: str, package_kind: str | None = None) -> tuple[dict[str, Any], ...]: + args = ["--product", product] + if package_kind is not None: + args.extend(["--kind", package_kind]) + return release_graph_rows("registry-packages", tuple(args)) + + +def registry_package_names(product: str, package_kind: str) -> list[str]: + names: list[str] = [] + for row in registry_package_rows(product, package_kind): + row_product = row.get("product") + kind = row.get("packageKind") + name = row.get("packageName") + if row_product != product: + fail(f"release graph registry-packages returned row for {row_product!r}, expected {product!r}") + if kind != package_kind: + fail(f"release graph registry-packages returned {product}.{kind!r}, expected {package_kind!r}") + if not isinstance(name, str) or not name: + fail(f"release graph registry-packages {product}.{package_kind} packageName must be a non-empty string") + 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 + + +@lru_cache(maxsize=1) +def extension_metadata_rows() -> tuple[dict[str, Any], ...]: + return release_graph_rows("extension-metadata") + + +def extension_metadata_row(product: str) -> dict[str, Any]: + matches = [row for row in extension_metadata_rows() if row.get("product") == product] + if len(matches) != 1: + fail(f"release graph extension-metadata query must return one row for {product}, got {len(matches)}") + return dict(matches[0]) + + +def extension_metadata_string(row: dict[str, Any], key: str, product: str) -> str: + value = row.get(key) + if not isinstance(value, str) or not value: + fail(f"extension-metadata {product}.{key} must be a non-empty string") + return value + + +def extension_metadata_object(row: dict[str, Any], key: str, product: str) -> dict[str, Any]: + value = row.get(key) + if not isinstance(value, dict): + fail(f"extension-metadata {product}.{key} must be an object") + return dict(value) + + +def release_extension_metadata(product: str) -> dict[str, Any]: + row = extension_metadata_row(product) + compatibility = extension_metadata_object(row, "compatibility", product) + for key in [ + "postgresMajor", + "extensionRuntimeContract", + "nativeRuntimeProduct", + "nativeRuntimeVersion", + "wasixRuntimeProduct", + "wasixRuntimeVersion", + ]: + if not isinstance(compatibility.get(key), str) or not compatibility[key]: + fail(f"extension-metadata {product}.compatibility.{key} must be a non-empty string") + return { + "sqlName": extension_metadata_string(row, "sqlName", product), + "class": extension_metadata_string(row, "class", product), + "versioning": extension_metadata_string(row, "versioning", product), + "sourcePath": extension_metadata_string(row, "sourcePath", product), + "compatibility": compatibility, + } + + +def release_extension_source_identity(product: str) -> dict[str, Any]: + return extension_metadata_object(extension_metadata_row(product), "sourceIdentity", product) + + +def extension_product_ids() -> list[str]: + products: list[str] = [] + for row in extension_metadata_rows(): + product = row.get("product") + if not isinstance(product, str) or not product: + fail("release graph extension-metadata rows must declare a non-empty product") + products.append(product) + if len(products) != len(set(products)): + fail("release graph extension-metadata query returned duplicate extension products") + if not products: + fail("release graph returned no extension products") + return sorted(products) + + +@lru_cache(maxsize=None) +def extension_artifact_targets( + *, + product: str | None = None, + family: str | None = None, + published_only: bool = False, +) -> tuple[SimpleNamespace, ...]: + args: list[str] = [] + if product is not None: + args.extend(["--product", product]) + if family is not None: + args.extend(["--family", family]) + if published_only: + args.append("--published-only") + return tuple(SimpleNamespace(**row) for row in release_graph_rows("extension-targets", tuple(args))) + + 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 @@ -349,7 +868,7 @@ def json_contains_workspace_protocol(value: object) -> bool: def validate_staged_npm_package_tarball(product: str, tarball: Path) -> None: - package_dir = ROOT / product_metadata.package_path(product) + package_dir = ROOT / package_path(product) package_json = package_dir / "package.json" if not package_json.is_file(): fail(f"{product} has no package.json at {package_json.relative_to(ROOT)}") @@ -451,7 +970,7 @@ def selected_products_from_passthrough(args: list[str]) -> list[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") - known = set(product_metadata.product_ids()) + known = set(product_ids()) unknown = sorted(set(value) - known) if unknown: fail(f"unknown release products: {', '.join(unknown)}") @@ -469,7 +988,7 @@ def selected_products_from_passthrough(args: list[str]) -> list[str]: def product_tag(product: str) -> str: - return f"{product_metadata.tag_prefix(product)}{product_metadata.read_current_version(product)}" + return f"{tag_prefix(product)}{read_current_version(product)}" def is_extension_product(product: str) -> bool: @@ -481,15 +1000,25 @@ def selected_extension_products(products: list[str]) -> list[str]: def publish_step_target_coverage(product: str) -> dict[str, set[str]]: - return product_metadata.publish_step_target_coverage(product) + coverage: dict[str, set[str]] = {} + for row in publish_step_target_coverage_rows(product): + step = row["step"] + publish_targets = row["publishTargets"] + assert isinstance(step, str) + assert isinstance(publish_targets, list) + coverage[step] = set(publish_targets) + return coverage def supported_publish_targets(product: str) -> set[str]: - return product_metadata.supported_publish_targets(product) + 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) + config = product_config(product) value = config.get("extension_sql_name") if not isinstance(value, str) or not value: fail(f"{product} release metadata must declare extension_sql_name") @@ -501,7 +1030,7 @@ def broker_cargo_package_name(target_id: str) -> str: def current_product_version(product: str) -> str: - return product_metadata.read_current_version(product) + return read_current_version(product) def verify_release_tag(product: str, head_ref: str) -> None: @@ -650,7 +1179,7 @@ def cargo_publish_manifest(package: str, version: str, manifest_path: Path, *, a def cargo_registry_packages(product: str) -> list[str]: - return sorted(product_metadata.registry_package_names(product, "crates")) + return sorted(registry_package_names(product, "crates")) def maven_pom_url(coordinate: str, version: str) -> str: @@ -664,7 +1193,7 @@ def maven_pom_url(coordinate: str, version: str) -> str: ) -def rust_artifact_cargo_target_cfg(target: product_metadata.ArtifactTarget) -> str: +def rust_artifact_cargo_target_cfg(target: 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": @@ -693,7 +1222,7 @@ def render_oliphaunt_release_cargo_toml(source: str, native_version: str, broker "# artifacts are published and indexed.", ] target_dependencies: dict[str, list[str]] = {} - for target in product_metadata.artifact_targets( + for target in artifact_targets( product="liboliphaunt-native", kind="native-runtime", surface="rust-native-direct", @@ -704,7 +1233,7 @@ def render_oliphaunt_release_cargo_toml(source: str, native_version: str, broker cfg = rust_artifact_cargo_target_cfg(target) target_dependencies.setdefault(cfg, []).append(f'{crate} = {{ version = "={native_version}" }}') target_dependencies.setdefault(cfg, []).append(f'{tools_facade} = {{ version = "={native_version}" }}') - for target in product_metadata.artifact_targets( + for target in artifact_targets( product="oliphaunt-broker", kind="broker-helper", surface="rust-broker", @@ -735,7 +1264,7 @@ def validate_generated_oliphaunt_release_artifact_coverage(manifest_path: Path) ) native_version = current_product_version("liboliphaunt-native") - native_targets = product_metadata.artifact_targets( + native_targets = artifact_targets( product="liboliphaunt-native", kind="native-runtime", surface="rust-native-direct", @@ -787,7 +1316,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 = set(product_metadata.wasix_public_cargo_package_names()) + artifact_crates = set(wasix_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) @@ -803,7 +1332,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 = set(product_metadata.wasix_public_cargo_package_names()) + required_crates = set(wasix_public_cargo_package_names()) missing = [ crate for crate in sorted(required_crates) @@ -861,7 +1390,7 @@ def prepare_oliphaunt_release_source(version: str) -> Path: package = rendered.split("[package]", 1)[1].split("[", 1)[0] if f'version = "{version}"' not in package: fail(f"generated oliphaunt release source must keep SDK version {version}") - for target in product_metadata.artifact_targets( + for target in artifact_targets( product="liboliphaunt-native", kind="native-runtime", surface="rust-native-direct", @@ -873,7 +1402,7 @@ def prepare_oliphaunt_release_source(version: str) -> Path: tools_facade = LIBOLIPHAUNT_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 product_metadata.artifact_targets( + for target in artifact_targets( product="oliphaunt-broker", kind="broker-helper", surface="rust-broker", @@ -920,7 +1449,7 @@ def validate_wasix_release_assets() -> None: "target/oliphaunt-wasix/release-assets; download the CI workflow " "liboliphaunt-wasix-release-assets artifact before release validation or publishing" ) - expected = set(product_metadata.expected_assets(product, version, surface="github-release")) + expected = set(expected_assets(product, version, surface="github-release")) actual = {path.name for path in asset_dir.iterdir() if path.is_file()} missing = sorted(expected - actual) if missing: @@ -1399,7 +1928,7 @@ def validate_checksum_manifest(checksum_manifest: Path, asset_dir: Path) -> None def public_extension_asset(asset: dict[str, object]) -> dict[str, object]: return { key: asset[key] - for key in product_metadata.PUBLIC_EXTENSION_RELEASE_ASSET_KEYS + for key in PUBLIC_EXTENSION_RELEASE_ASSET_KEYS if key in asset } @@ -1447,20 +1976,20 @@ def validate_extension_release_package(product: str) -> None: if release_data.get(key) != value: fail(f"{release_manifest.relative_to(ROOT)} has {key}={release_data.get(key)!r}, expected {value!r}") actual_release_keys = set(release_data) - expected_release_keys = product_metadata.PUBLIC_EXTENSION_RELEASE_MANIFEST_KEYS + expected_release_keys = PUBLIC_EXTENSION_RELEASE_MANIFEST_KEYS if actual_release_keys != expected_release_keys: fail( f"{release_manifest.relative_to(ROOT)} public manifest keys must be " f"{sorted(expected_release_keys)}, got {sorted(actual_release_keys)}" ) - extension_metadata = product_metadata.extension_metadata(product) - if release_data.get("extensionClass") != extension_metadata["class"]: + extension_meta = release_extension_metadata(product) + if release_data.get("extensionClass") != extension_meta["class"]: fail(f"{release_manifest.relative_to(ROOT)} has stale extensionClass") - if release_data.get("versioning") != extension_metadata["versioning"]: + if release_data.get("versioning") != extension_meta["versioning"]: fail(f"{release_manifest.relative_to(ROOT)} has stale versioning") - if release_data.get("sourceIdentity") != product_metadata.extension_source_identity(product): + if release_data.get("sourceIdentity") != release_extension_source_identity(product): fail(f"{release_manifest.relative_to(ROOT)} has stale sourceIdentity") - if release_data.get("compatibility") != extension_metadata["compatibility"]: + if release_data.get("compatibility") != extension_meta["compatibility"]: fail(f"{release_manifest.relative_to(ROOT)} has stale compatibility") assets = data.get("assets") @@ -1474,7 +2003,7 @@ def validate_extension_release_package(product: str) -> None: if not isinstance(asset, dict): fail(f"{release_manifest.relative_to(ROOT)} public assets must contain object rows") actual_asset_keys = set(asset) - expected_asset_keys = product_metadata.PUBLIC_EXTENSION_RELEASE_ASSET_KEYS + expected_asset_keys = PUBLIC_EXTENSION_RELEASE_ASSET_KEYS if actual_asset_keys != expected_asset_keys: fail( f"{release_manifest.relative_to(ROOT)} public asset {asset.get('name')!r} keys must be " @@ -1483,7 +2012,7 @@ def validate_extension_release_package(product: str) -> None: declared_native_targets = { target.target - for target in product_metadata.extension_artifact_targets( + for target in extension_artifact_targets( product=product, family="native", published_only=True, @@ -1491,7 +2020,7 @@ def validate_extension_release_package(product: str) -> None: } declared_wasix_targets = { target.target - for target in product_metadata.extension_artifact_targets( + for target in extension_artifact_targets( product=product, family="wasix", published_only=True, @@ -1802,15 +2331,15 @@ def command_ci_artifacts(args: list[str]) -> None: if parsed.family == "release-assets": if parsed.kind is None: fail("ci-artifacts --family release-assets requires --kind") - names = product_metadata.ci_release_asset_artifact_names(parsed.product, parsed.kind) + names = ci_release_asset_artifact_names(parsed.product, parsed.kind) elif parsed.family == "npm-package": if parsed.kind is None: fail("ci-artifacts --family npm-package requires --kind") - names = product_metadata.ci_npm_package_artifact_names(parsed.product, parsed.kind) + names = 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 = product_metadata.ci_sdk_package_artifact_names(parsed.product) + names = ci_sdk_package_artifact_names(parsed.product) for name in names: print(name) @@ -1820,9 +2349,9 @@ def command_ci_products(args: list[str]) -> None: parser.add_argument("--family", choices=["sdk-package"], required=True) parser.add_argument("--products-json") parsed = parser.parse_args(args) - sdk_products = set(product_metadata.sdk_package_products()) + sdk_products = set(sdk_package_products()) if parsed.products_json is None: - products = list(product_metadata.sdk_package_products()) + products = list(sdk_package_products()) else: products = selected_products_from_passthrough(["--products-json", parsed.products_json]) for product in products: @@ -1885,7 +2414,7 @@ def publish_swift_release(head_ref: str) -> None: def kotlin_artifacts_published(version: str) -> bool: return all( url_exists(maven_pom_url(coordinate, version)) - for coordinate in product_metadata.registry_package_names("oliphaunt-kotlin", "maven") + for coordinate in registry_package_names("oliphaunt-kotlin", "maven") ) @@ -2057,10 +2586,10 @@ def publish_node_direct_release_assets(head_ref: str) -> None: upload_github_release_assets("oliphaunt-node-direct", assets=assets) -def node_direct_optional_package_targets(version: str) -> list[tuple[str, Path, product_metadata.ArtifactTarget]]: +def node_direct_optional_package_targets(version: str) -> list[tuple[str, Path, ArtifactTarget]]: package_dirs = npm_package_dirs_under(NODE_DIRECT_PACKAGE_ROOT) - packages: list[tuple[str, Path, product_metadata.ArtifactTarget]] = [] - for target in product_metadata.artifact_targets( + packages: list[tuple[str, Path, ArtifactTarget]] = [] + for target in artifact_targets( product="oliphaunt-node-direct", kind="node-direct-addon", surface="npm-optional", @@ -2107,10 +2636,10 @@ def artifact_npm_package_targets( kind: str, surface: str, package_root: Path, -) -> list[tuple[str, Path, product_metadata.ArtifactTarget]]: +) -> list[tuple[str, Path, ArtifactTarget]]: package_dirs = npm_package_dirs_under(package_root) - packages: list[tuple[str, Path, product_metadata.ArtifactTarget]] = [] - for target in product_metadata.artifact_targets(product=product, kind=kind, surface=surface, published_only=True): + packages: list[tuple[str, Path, ArtifactTarget]] = [] + for target in artifact_targets(product=product, kind=kind, surface=surface, published_only=True): package_name = target.npm_package if package_name is None: fail(f"{target.id} must declare npm_package for npm artifact package publication") @@ -2735,7 +3264,7 @@ def broker_cargo_artifact_crates(version: str) -> list[tuple[str, Path, Path]]: source_root = ROOT / "target" / "oliphaunt-broker" / "cargo-package-sources" expected_crates = { broker_cargo_package_name(target.target) - for target in product_metadata.artifact_targets( + for target in artifact_targets( product="oliphaunt-broker", kind="broker-helper", surface="rust-broker", @@ -2790,7 +3319,7 @@ 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 = product_metadata.artifact_targets( + native_targets = artifact_targets( product="liboliphaunt-native", kind="native-runtime", surface="rust-native-direct", @@ -2888,11 +3417,11 @@ def liboliphaunt_wasix_cargo_artifact_crates(version: str) -> list[tuple[str, Pa fail(f"missing generated liboliphaunt-wasix Cargo artifact manifest: {manifest_path.relative_to(ROOT)}") data = json.loads(manifest_path.read_text(encoding="utf-8")) packages_data = data.get("packages") - if data.get("schema") != product_metadata.wasix_cargo_artifact_schema() or not isinstance(packages_data, list): + if data.get("schema") != wasix_cargo_artifact_schema() or not isinstance(packages_data, list): fail(f"{manifest_path.relative_to(ROOT)} has an invalid schema") expected_base_crates = set( - product_metadata.wasix_public_cargo_package_names() + wasix_public_cargo_package_names() ) configured_crates = set(cratesio_product_crates("liboliphaunt-wasix")) if configured_crates != expected_base_crates: @@ -2917,10 +3446,10 @@ def liboliphaunt_wasix_cargo_artifact_crates(version: str) -> list[tuple[str, Pa 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 any(name == f"{product}-wasix" for product in product_metadata.extension_product_ids()) + and any(name == f"{product}-wasix" for product in extension_product_ids()) ) and not ( kind == "wasix-extension-aot" - and any(name.startswith(f"{product}-wasix-aot-") for product in product_metadata.extension_product_ids()) + and any(name.startswith(f"{product}-wasix-aot-") for product in extension_product_ids()) ): fail(f"unexpected liboliphaunt-wasix Cargo artifact crate {name}") if kind not in {"wasix-runtime", "wasix-tools", "wasix-aot", "wasix-tools-aot", "icu-data", "wasix-extension", "wasix-extension-aot"}: @@ -3225,7 +3754,7 @@ def command_publish_product_step(args: argparse.Namespace) -> None: head_ref = args.head_ref if product is None or step is None: fail("publish product step requires --product and --step") - known = set(product_metadata.product_ids()) + known = set(product_ids()) if product not in known: fail(f"unknown release product: {product}") From 889e6a0871eb06d12c8bd51a1a4aa705158e131c Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 20:47:56 +0000 Subject: [PATCH 224/308] chore: remove stale release metadata inputs --- docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 10 ++++++++++ src/runtimes/liboliphaunt/native/moon.yml | 1 - src/runtimes/node-direct/moon.yml | 2 -- tools/policy/python-entrypoints.allowlist | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index e07d857b..257ce1f1 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,16 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Removed stale `tools/release/product_metadata.py` Moon task + inputs from Node-direct `check`/`release-assets` and native + `release-assets` tasks after those paths moved to Bun release graph queries. + The Python tooling inventory rationale now describes `product_metadata.py` as + a transitional compatibility adapter rather than an actively imported release + dependency. Fresh checks passed: `rg product_metadata.py` over the touched + Moon files and inventory, `tools/dev/bun.sh + tools/policy/check-python-entrypoints.mjs --json`, `tools/dev/bun.sh + tools/policy/check-moon-product-graph.mjs`, and `bash + tools/policy/check-policy-tools.sh`. - 2026-06-27: Removed `release.py`'s import of the Python `product_metadata.py` compatibility module. The release orchestrator now reads product configs, current versions, publish-step target coverage, diff --git a/src/runtimes/liboliphaunt/native/moon.yml b/src/runtimes/liboliphaunt/native/moon.yml index d2e7dac5..65651754 100644 --- a/src/runtimes/liboliphaunt/native/moon.yml +++ b/src/runtimes/liboliphaunt/native/moon.yml @@ -171,7 +171,6 @@ tasks: - "/src/runtimes/liboliphaunt/native/moon.yml" - "/tools/release/check-liboliphaunt-release-assets.mjs" - "/tools/release/package-liboliphaunt-aggregate-assets.sh" - - "/tools/release/product_metadata.py" - "/tools/release/release-artifact-targets.mjs" - "/tools/release/release-graph.mjs" - "/tools/release/release_graph_query.mjs" diff --git a/src/runtimes/node-direct/moon.yml b/src/runtimes/node-direct/moon.yml index aaedf308..2300053e 100644 --- a/src/runtimes/node-direct/moon.yml +++ b/src/runtimes/node-direct/moon.yml @@ -40,7 +40,6 @@ tasks: - "/src/runtimes/node-direct/**/*" - "/tools/release/check_artifact_targets.py" - "/tools/release/check-node-direct-release-assets.mjs" - - "/tools/release/product_metadata.py" - "/tools/release/release-asset-validation.mjs" - "/tools/release/release-artifact-targets.mjs" - "/tools/release/release_graph_query.mjs" @@ -82,7 +81,6 @@ tasks: - "/src/runtimes/node-direct/**/*" - "/tools/release/artifact_target_matrix.mjs" - "/tools/release/check-node-direct-release-assets.mjs" - - "/tools/release/product_metadata.py" - "/tools/release/release-asset-validation.mjs" - "/tools/release/release-artifact-targets.mjs" - "/tools/release/release_graph_query.mjs" diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index f0aab234..9314fdb0 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -8,5 +8,5 @@ tools/release/check_consumer_shape.py release-consumer-shape defer-release-graph tools/release/check_release_metadata.py release-metadata defer-release-graph-port validates release metadata and publish-step wiring through cached Bun release graph query rows tools/release/local_registry_publish.py local-registry defer-local-registry-port publishes local Cargo, npm, Maven, and Swift registries from current release artifacts for e2e example validation tools/release/package_liboliphaunt_wasix_cargo_artifacts.py wasix-cargo-artifacts defer-wasix-packager-port generates split WASIX runtime, tools, ICU, and extension Cargo artifact crates with size-limit enforcement -tools/release/product_metadata.py release-metadata defer-release-graph-port owns the Python compatibility product metadata API consumed by release tools while direct version reads use Bun +tools/release/product_metadata.py release-metadata defer-release-graph-port keeps a transitional compatibility adapter and fail-fast legacy CLI path while release graph ownership moves to Bun tools/release/release.py release-orchestrator defer-release-graph-port owns protected release planning, validation, registry checks, publish dry-runs, and publish dispatch From 575eba74e78d464d3d054b0e81a90683578ccfa6 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 20:56:32 +0000 Subject: [PATCH 225/308] chore: remove release metadata compatibility module --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 28 +- docs/internal/IMPLEMENTATION_CHECKLIST.md | 5 +- .../examples-ci-release-validation.md | 37 +- tools/policy/python-entrypoints.allowlist | 1 - tools/release/check_release_metadata.py | 73 +- tools/release/product_metadata.py | 970 ------------------ 6 files changed, 57 insertions(+), 1057 deletions(-) delete mode 100644 tools/release/product_metadata.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 257ce1f1..d8e23658 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,14 +78,25 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Deleted the unused `tools/release/product_metadata.py` + compatibility module now that executable release consumers query + `release_graph_query.mjs` directly. `check_release_metadata.py` now fails if + the compatibility file reappears and keeps the direct Bun graph/query guards + for product configs, versions, artifact targets, registry packages, expected + assets, extension metadata, WASIX package names, and local-publish presets. + The Python tooling inventory dropped from 9 to 8 tracked files. Fresh checks + passed: `rg` proving no executable `import product_metadata` or + `product_metadata.*` calls remain, `tools/dev/bun.sh + tools/policy/check-python-entrypoints.mjs --json`, `python3 -m py_compile` + for touched Python release/policy helpers, `check_release_metadata.py`, and + `release.py ci-products --family sdk-package`. - 2026-06-27: Removed stale `tools/release/product_metadata.py` Moon task inputs from Node-direct `check`/`release-assets` and native `release-assets` tasks after those paths moved to Bun release graph queries. - The Python tooling inventory rationale now describes `product_metadata.py` as - a transitional compatibility adapter rather than an actively imported release - dependency. Fresh checks passed: `rg product_metadata.py` over the touched - Moon files and inventory, `tools/dev/bun.sh - tools/policy/check-python-entrypoints.mjs --json`, `tools/dev/bun.sh + This was the temporary state before the compatibility file was deleted. + Fresh checks passed: `rg product_metadata.py` over the touched Moon files and + inventory, `tools/dev/bun.sh tools/policy/check-python-entrypoints.mjs + --json`, `tools/dev/bun.sh tools/policy/check-moon-product-graph.mjs`, and `bash tools/policy/check-policy-tools.sh`. - 2026-06-27: Removed `release.py`'s import of the Python @@ -107,11 +118,8 @@ until the current-state gates here are checked with fresh local evidence. invalid npm-package query, `check_release_metadata.py`, `check-release-policy.py`, `tools/dev/bun.sh tools/policy/check-python-entrypoints.mjs --json`, and full - `tools/release/release.py check`. The Python entrypoint inventory remains at - 9 entries; - `release.py` is now 3,894 lines and 158,160 bytes, while - `product_metadata.py` remains a compatibility adapter at 970 lines and - 37,950 bytes. + `tools/release/release.py check`. The Python entrypoint inventory remained at + 9 entries before the follow-up deletion of `product_metadata.py`. - 2026-06-27: Removed `check_release_metadata.py`'s import of the Python `product_metadata.py` compatibility module. The release metadata checker now reads product configs, version files, current versions, artifact targets, diff --git a/docs/internal/IMPLEMENTATION_CHECKLIST.md b/docs/internal/IMPLEMENTATION_CHECKLIST.md index dfb4a6f1..049f1695 100644 --- a/docs/internal/IMPLEMENTATION_CHECKLIST.md +++ b/docs/internal/IMPLEMENTATION_CHECKLIST.md @@ -311,8 +311,9 @@ or CI/build output proves the contract. changelogs, and tags. Evidence: `release-please-config.json` and `.release-please-manifest.json`. - [x] Product-local `release.toml` files own registry/package metadata. - Evidence: `tools/release/product_metadata.py` validates Moon release products - against release-please components. + Evidence: `tools/release/release_graph_query.mjs product-configs` and + `registry-packages` expose product-local package metadata from the canonical + Bun release graph. - [x] There is no active `release-graph.toml`, `release-inputs.toml`, or `tools/graph/jobs.toml` release brain. - [x] `tools/release/release.py plan` uses Moon project ownership and dependency diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index 583db295..abb13254 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -170,33 +170,38 @@ the release/tooling surface after the runtime tool crate split. `coverage/baseline.toml` are intentional policy inputs. - On 2026-06-27, the remaining Python and Rust helper inventories were rechecked. `tools/dev/bun.sh tools/policy/check-python-entrypoints.mjs --list` - verified the nine remaining Python tooling files, all deferred release, + verified the nine remaining Python tooling files at that point, all deferred release, local-registry, WASIX-packager, or extension-modeling ports rather than low-risk wrappers. `tools/dev/bun.sh tools/policy/check-rust-helper-crates.mjs --list` verified the only Rust helper crates are `tools/perf/runner` and `tools/xtask`, both retained as domain tools. -- On 2026-06-27, the Python release compatibility layer was narrowed further. +- On 2026-06-27, the unused Python release metadata compatibility module was + deleted after the remaining executable consumers moved to the Bun release + graph query. `check_release_metadata.py` now fails if + `tools/release/product_metadata.py` reappears, and the Python tooling + inventory is down to eight tracked files. +- Earlier on 2026-06-27, the Python release compatibility layer was narrowed + further before the module was deleted. `tools/release/product_metadata.py` no longer parses `release-please-config.json` for version files, changelog paths, or tag prefixes, and its extension-target lookup now uses the same cached Bun - `release_graph_query.mjs` helper as other artifact target reads. The tracked - Python inventory remains nine files, with `product_metadata.py` reduced to - 987 lines. Fresh checks passed for Python compile, release graph output, - targeted product metadata reads, release metadata, artifact targets, focused - consumer-shape checks, release policy, tooling-stack policy, + `release_graph_query.mjs` helper as other artifact target reads. At that + checkpoint the tracked Python inventory still had nine files, with + `product_metadata.py` reduced to 987 lines. Fresh checks passed for Python + compile, release graph output, targeted product metadata reads, release + metadata, artifact targets, focused consumer-shape checks, release policy, + tooling-stack policy, `tools/release/release.py check`, strict local Cargo publication, strict local npm publication, docs policy, and `git diff --check`. - On 2026-06-27, the stale direct `tools/release/product_metadata.py version` - CLI was retired. Product version reads remain on the Bun helper - `tools/release/product-version.mjs`, and direct execution of - `product_metadata.py` now fails with module-only guidance instead of exposing - a second version-read path. Fresh validation passed for the Bun version - helper, the expected failing Python guidance path, Python compile, tooling - inventory, policy tooling, docs, `tools/release/release.py check`, and strict - local Cargo/npm publication. A sweep of 836 generated `.crate` files found no - crate above the 10 MiB crates.io limit; the largest observed crate was - 10,212,312 bytes. + CLI was retired before the compatibility module was deleted. Product version + reads remain on the Bun helper `tools/release/product-version.mjs`. Fresh + validation passed for the Bun version helper, the expected failing Python + guidance path, Python compile, tooling inventory, policy tooling, docs, + `tools/release/release.py check`, and strict local Cargo/npm publication. A + sweep of 836 generated `.crate` files found no crate above the 10 MiB + crates.io limit; the largest observed crate was 10,212,312 bytes. - On 2026-06-27, strict local Cargo and npm publication were rerun against the current split runtime/tools package surface with `tools/release/local_registry_publish.py publish --surface cargo --strict` diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index 9314fdb0..951789b9 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -8,5 +8,4 @@ tools/release/check_consumer_shape.py release-consumer-shape defer-release-graph tools/release/check_release_metadata.py release-metadata defer-release-graph-port validates release metadata and publish-step wiring through cached Bun release graph query rows tools/release/local_registry_publish.py local-registry defer-local-registry-port publishes local Cargo, npm, Maven, and Swift registries from current release artifacts for e2e example validation tools/release/package_liboliphaunt_wasix_cargo_artifacts.py wasix-cargo-artifacts defer-wasix-packager-port generates split WASIX runtime, tools, ICU, and extension Cargo artifact crates with size-limit enforcement -tools/release/product_metadata.py release-metadata defer-release-graph-port keeps a transitional compatibility adapter and fail-fast legacy CLI path while release graph ownership moves to Bun tools/release/release.py release-orchestrator defer-release-graph-port owns protected release planning, validation, registry checks, publish dry-runs, and publish dispatch diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 554724d6..9339c246 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -610,7 +610,8 @@ def validate_graph_files() -> None: if not (ROOT / path).is_file(): fail(f"{product} release metadata path does not exist: {path}") validate_all_extension_metadata() - product_metadata_source = read_text("tools/release/product_metadata.py") + if (ROOT / "tools/release/product_metadata.py").exists(): + fail("tools/release/product_metadata.py must stay deleted; release metadata consumers should query Bun directly") release_graph_query = read_text("tools/release/release_graph_query.mjs") release_graph_source = read_text("tools/release/release-graph.mjs") release_artifact_targets = read_text("tools/release/release-artifact-targets.mjs") @@ -629,21 +630,15 @@ def validate_graph_files() -> None: if re.search(r"(?m)^import product_metadata$", check_release_metadata_source): fail("check_release_metadata.py must consume Bun release graph rows instead of importing product_metadata.py") if ( - "_release_metadata(product).get(\"compatibility_versions\"" in product_metadata_source - or "_compatibility_version_entries(" in product_metadata_source - or "compatibility_version_specs(" in product_metadata_source - or "compatibility_version_links(" in product_metadata_source - or "compatibility-version-entries [--require-source-product]" not in release_graph_query + "compatibility-version-entries [--require-source-product]" not in release_graph_query or "compatibilityVersionEntries(graphProducts()" not in sync_release_pr ): fail("compatibility version metadata must be collected through the canonical Bun release graph query") if ( - '"extension-metadata"' not in product_metadata_source - or "extension-metadata [--product PRODUCT]" not in release_graph_query + "extension-metadata [--product PRODUCT]" not in release_graph_query or "export function extensionMetadata(" not in release_artifact_targets or "export function extensionSourceIdentity(" not in release_artifact_targets or "exactExtensionProducts(TOOL)" not in release_graph_query - or 'config.get("kind") == "exact-extension-artifact"' in product_metadata_source or "extension_products = extension_product_ids()" not in check_artifact_targets or "return set(extension_product_ids())" not in check_consumer_shape or "modeled_extension_products = set(extension_product_ids())" not in release_policy @@ -672,26 +667,15 @@ def validate_graph_files() -> None: ): fail("extension metadata and source identity must be shared through release-artifact-targets and the Bun release graph query") if ( - '"product-versions"' not in product_metadata_source - or "product-versions [--product PRODUCT]" not in release_graph_query + "product-versions [--product PRODUCT]" not in release_graph_query or "currentProductVersionSync(" not in release_graph_query - or "parse_version_text(" in product_metadata_source - or "parse_toml_path(" in product_metadata_source - or "parser_for_version_file(" in product_metadata_source - or "canonical_version_spec(" in product_metadata_source - or "product_version_specs(" in product_metadata_source - or "release_owned_version_specs(" in product_metadata_source - or "import tomllib" in product_metadata_source or 'property.trim() === "VERSION_NAME"' not in release_artifact_targets ): fail("current product version values must be read through the Bun release graph product-versions query") if ( - '"product-configs"' not in product_metadata_source - or "product-configs [--product PRODUCT]" not in release_graph_query + "product-configs [--product PRODUCT]" not in release_graph_query or "productConfigRows({ product }, TOOL)" not in release_graph_query or "export function productConfigRows(" not in release_graph_source - or "source = load_graph() if graph is None else graph" in product_metadata_source - or 'products = source.get("products")' in product_metadata_source ): fail("product config metadata must be adapted through the Bun release graph product-configs query") release_source = read_text("tools/release/release.py") @@ -706,12 +690,9 @@ def validate_graph_files() -> None: ): fail("release.py publish target coverage must be adapted through the Bun release graph query") if ( - '"moon-release-metadata"' not in product_metadata_source - or "moon-release-metadata [--product PRODUCT]" not in release_graph_query + "moon-release-metadata [--product PRODUCT]" not in release_graph_query or "moonReleaseMetadataRows({ product }, TOOL)" not in release_graph_query or "export function moonReleaseMetadataRows(" not in release_graph_source - or 'load_graph().get("moon_projects")' in product_metadata_source - or 'project.get("project")' in product_metadata_source ): fail("Moon release metadata must be adapted through the Bun release graph moon-release-metadata query") if ( @@ -725,8 +706,7 @@ def validate_graph_files() -> None: ): fail("release policy must consume normalized Bun Moon project rows and product-config metadata") if ( - '"legacy-central-artifact-targets"' not in product_metadata_source - or "legacy-central-artifact-targets" not in release_graph_query + "legacy-central-artifact-targets" not in release_graph_query or 'release_graph_rows("legacy-central-artifact-targets")' not in check_artifact_targets or ("product_metadata." + "load_graph()") in check_artifact_targets or ("def " + "load_graph()") in check_release_metadata_source @@ -740,9 +720,7 @@ def validate_graph_files() -> None: ): fail("release PR coverage must call the Bun release planner directly") if ( - "typescript_optional_runtime_package_products(" in product_metadata_source - or "typescript-broker" in product_metadata_source - or "function typescriptOptionalRuntimePackageProducts(" in sync_release_pr + "function typescriptOptionalRuntimePackageProducts(" in sync_release_pr or "export function typescriptOptionalRuntimePackageProducts(" not in release_artifact_targets or "typescriptOptionalRuntimePackageProducts(PREFIX)" not in sync_release_pr or "typescript-optional-runtime-package-versions" not in release_graph_query @@ -750,21 +728,14 @@ def validate_graph_files() -> None: ): fail("TypeScript optional runtime package selection must come from the shared Bun artifact target helper") if ( - '"sdk-package-products"' not in product_metadata_source - or "config.get(\"kind\") == \"sdk\"" in product_metadata_source - or "config.get(\"kind\") != \"sdk\"" in product_metadata_source - or "oliphaunt-wasix-rust" in product_metadata_source - or "export function sdkPackageProducts(" not in release_artifact_targets + "export function sdkPackageProducts(" not in release_artifact_targets or "sdk-package-products [--product PRODUCT]" not in release_graph_query or "ci-products --family sdk-package" not in release_graph_query or "sdkPackageProducts(TOOL)" not in release_graph_query ): fail("SDK package product and CI artifact-name selection must come from the shared Bun release graph query") if ( - '"ci-artifact-names"' not in product_metadata_source - or "f\"{product}-release-assets-{target.target}\"" in product_metadata_source - or "f\"{product}-npm-package-{target.target}\"" in product_metadata_source - or "export function ciReleaseAssetArtifactRows(" not in release_artifact_targets + "export function ciReleaseAssetArtifactRows(" not in release_artifact_targets or "export function ciNpmPackageArtifactRows(" not in release_artifact_targets or "ci-artifact-names --family release-assets|npm-package|sdk-package --product PRODUCT" not in release_graph_query or "ciReleaseAssetArtifactRows(product, kind, TOOL)" not in release_graph_query @@ -772,43 +743,29 @@ def validate_graph_files() -> None: ): fail("CI release asset and npm package artifact names must come from the shared Bun artifact target helper") if ( - '"expected-assets"' not in product_metadata_source - or "export function expectedAssetRows(" not in release_artifact_targets + "export function expectedAssetRows(" not in release_artifact_targets or "expected-assets --product PRODUCT --version VERSION" not in release_graph_query or "expectedAssetRows({" not in release_graph_query - or "target.asset_name(version)" in product_metadata_source - or "allowed_kinds = set(kinds)" in product_metadata_source ): fail("expected release asset names must come from the shared Bun release graph query") if ( - '"registry-packages"' not in product_metadata_source - or "export function registryPackageRows(" not in release_artifact_targets + "export function registryPackageRows(" not in release_artifact_targets or "registry-packages --product PRODUCT [--kind KIND]" not in release_graph_query or "registryPackageRows({ product, packageKind }, TOOL)" not in release_graph_query - or 'for raw in string_list(product_config(product), "registry_packages", product)' in product_metadata_source - or 'raw.partition(":")' in product_metadata_source ): fail("registry package name selection must come from the shared Bun release graph query") if ( - '"wasix-extension-package-names"' not in product_metadata_source - or "wasix-extension-package-names [--product PRODUCT [--target TARGET...]]" not in release_graph_query + "wasix-extension-package-names [--product PRODUCT [--target TARGET...]]" not in release_graph_query or "exactExtensionProducts(TOOL).map" not in release_graph_query or 'release_graph_rows("wasix-extension-package-names")' not in check_consumer_shape or "wasixExtensionPackageName(product)" not in release_graph_query or "wasixExtensionAotPackageName(product, target)" not in release_graph_query - or 'return f"{product}-wasix"' in product_metadata_source - or 'return f"{product}-wasix-aot-{target}"' in product_metadata_source ): fail("WASIX extension package names must come from the shared Bun WASIX Cargo artifact contract query") if ( - '"local-publish-artifacts"' not in product_metadata_source - or "export function localPublishArtifactRows(" not in release_artifact_targets + "export function localPublishArtifactRows(" not in release_artifact_targets or "local-publish-artifacts [--aggregate-only]" not in release_graph_query or "localPublishArtifactRows({ aggregateOnly }, TOOL)" not in release_graph_query - or "liboliphaunt-wasix-runtime-aot-{target.target}" in product_metadata_source - or "liboliphaunt-wasix-runtime-{target.target}" in product_metadata_source - or "liboliphaunt-wasix-extension-artifacts-{target_id}" in product_metadata_source - or "oliphaunt-extension-package-artifacts" in product_metadata_source ): fail("local-registry publish artifact preset must come from the shared Bun release graph query") diff --git a/tools/release/product_metadata.py b/tools/release/product_metadata.py deleted file mode 100644 index 6c1b593c..00000000 --- a/tools/release/product_metadata.py +++ /dev/null @@ -1,970 +0,0 @@ -"""Shared release product metadata. - -Release identity comes from release-please manifest-mode config. Product-local -``release.toml`` files hold package and artifact metadata that release-please -does not own. -""" - -from __future__ import annotations - -import json -import subprocess -import sys -from dataclasses import dataclass -from functools import lru_cache -from pathlib import Path -from types import SimpleNamespace -from typing import Any, Iterable, NoReturn - - -ROOT = Path(__file__).resolve().parents[2] -PUBLIC_EXTENSION_RELEASE_MANIFEST_KEYS = { - "schema", - "product", - "version", - "sqlName", - "extensionClass", - "versioning", - "sourceIdentity", - "compatibility", - "dependencies", - "nativeModuleStem", - "sharedPreloadLibraries", - "mobileReleaseReady", - "desktopReleaseReady", - "assets", -} -PUBLIC_EXTENSION_RELEASE_ASSET_KEYS = { - "name", - "family", - "target", - "kind", - "sha256", - "bytes", -} - - -def fail(message: str) -> NoReturn: - print(f"product_metadata.py: {message}", file=sys.stderr) - raise SystemExit(2) - - -def package_path(product: str) -> str: - value = product_config(product).get("path") - if not isinstance(value, str) or not value: - fail(f"release graph product {product!r} must declare a package path") - return value - - -@lru_cache(maxsize=None) -def _moon_release_metadata_rows(product: str | None = None) -> tuple[dict[str, Any], ...]: - args = () if product is None else ("--product", product) - rows = _release_graph_query_rows("moon-release-metadata", args) - if product is not None and len(rows) != 1: - fail(f"release graph moon-release-metadata query must return one row for {product}, got {len(rows)}") - seen: set[str] = set() - parsed: list[dict[str, Any]] = [] - for row in rows: - product_id = row.get("product") - component = row.get("component") - package_path = row.get("packagePath") - if not isinstance(product_id, str) or not product_id: - fail("release graph moon-release-metadata rows must declare a non-empty product") - if product_id in seen: - fail(f"release graph moon-release-metadata returned duplicate product {product_id}") - seen.add(product_id) - if component != product_id: - fail(f"release graph moon-release-metadata {product_id}.component must match the product id") - if not isinstance(package_path, str) or not package_path: - fail(f"release graph moon-release-metadata {product_id}.packagePath must be a non-empty string") - parsed.append(dict(row)) - if not parsed: - fail("release graph returned no Moon release metadata rows") - return tuple(parsed) - - -def moon_release_metadata(product: str) -> dict[str, Any]: - row = dict(_moon_release_metadata_rows(product)[0]) - row.pop("product", None) - return row - - -@lru_cache(maxsize=None) -def _publish_step_target_coverage_rows(product: str | None = None) -> tuple[dict[str, Any], ...]: - args = () if product is None else ("--product", product) - rows = _release_graph_query_rows("publish-step-target-coverage", args) - seen: set[tuple[str, str]] = set() - parsed: list[dict[str, Any]] = [] - for row in rows: - product_id = row.get("product") - step = row.get("step") - publish_targets = row.get("publishTargets") - extension = row.get("extension") - if not isinstance(product_id, str) or not product_id: - fail("release graph publish-step-target-coverage rows must declare a non-empty product") - if product is not None and product_id != product: - fail(f"release graph publish-step-target-coverage returned row for {product_id}, expected {product}") - if not isinstance(step, str) or not step: - fail(f"release graph publish-step-target-coverage {product_id}.step must be a non-empty string") - if not isinstance(publish_targets, list) or not publish_targets or not all( - isinstance(item, str) and item for item in publish_targets - ): - fail(f"release graph publish-step-target-coverage {product_id}.{step}.publishTargets must be a non-empty string list") - if not isinstance(extension, bool): - fail(f"release graph publish-step-target-coverage {product_id}.{step}.extension must be true or false") - key = (product_id, step) - if key in seen: - fail(f"release graph publish-step-target-coverage returned duplicate row for {product_id}.{step}") - seen.add(key) - parsed.append(dict(row)) - return tuple(parsed) - - -def publish_step_target_coverage(product: str) -> dict[str, set[str]]: - coverage: dict[str, set[str]] = {} - for row in _publish_step_target_coverage_rows(product): - step = row["step"] - publish_targets = row["publishTargets"] - assert isinstance(step, str) - assert isinstance(publish_targets, list) - coverage[step] = set(publish_targets) - return coverage - - -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 is_extension_product(product: str) -> bool: - rows = _publish_step_target_coverage_rows(product) - if not rows: - return product.startswith("oliphaunt-extension-") - return bool(rows[0].get("extension")) - - -def load_graph() -> dict[str, Any]: - """Compatibility return value for callers that still accept a graph arg.""" - - return _release_graph() - - -@dataclass(frozen=True) -class ArtifactTarget: - id: str - product: str - kind: str - target: str - asset: str - published: bool - surfaces: tuple[str, ...] - triple: str | None = None - runner: str | None = None - library_relative_path: str | None = None - executable_relative_path: str | None = None - npm_package: str | None = None - npm_os: str | None = None - npm_cpu: str | None = None - npm_libc: str | None = None - llvm_url: str | None = None - extension_artifacts: bool = True - - def asset_name(self, version: str) -> str: - return self.asset.format(version=version) - - -@lru_cache(maxsize=None) -def _release_graph_query_json(command: str, args: tuple[str, ...] = ()) -> Any: - try: - output = subprocess.check_output( - ["tools/dev/bun.sh", "tools/release/release_graph_query.mjs", command, *args], - cwd=ROOT, - text=True, - stderr=subprocess.PIPE, - ) - except subprocess.CalledProcessError as error: - detail = (error.stderr or "").strip() - if detail: - fail(f"release graph {command} query failed: {detail}") - fail(f"release graph {command} query failed with exit code {error.returncode}") - return json.loads(output) - - -@lru_cache(maxsize=None) -def _release_graph_query_rows(command: str, args: tuple[str, ...] = ()) -> tuple[dict[str, Any], ...]: - rows = _release_graph_query_json(command, args) - if not isinstance(rows, list) or not all(isinstance(row, dict) for row in rows): - fail(f"release graph {command} query must return a JSON object list") - return tuple(rows) - - -@lru_cache(maxsize=1) -def _release_graph() -> dict[str, Any]: - value = _release_graph_query_json("graph") - if not isinstance(value, dict): - fail("release graph query must return a JSON object") - products = value.get("products") - if not isinstance(products, dict) or not products: - fail("release graph query must return a non-empty products object") - return value - - -def _target_string(row: dict[str, Any], key: str, target_id: str, *, required: bool = True) -> str | None: - value = row.get(key) - if isinstance(value, str) and value: - return value - if required: - fail(f"artifact target {target_id}.{key} must be a non-empty string") - if value is not None: - fail(f"artifact target {target_id}.{key} must be a string") - return None - - -def _target_bool(row: dict[str, Any], key: str, target_id: str, *, default: bool | None = None) -> bool: - value = row.get(key) - if isinstance(value, bool): - return value - if value is None and default is not None: - return default - fail(f"artifact target {target_id}.{key} must be true or false") - - -def _target_surfaces(row: dict[str, Any], target_id: str) -> tuple[str, ...]: - value = row.get("surfaces") - if not isinstance(value, list) or not value or not all(isinstance(item, str) and item for item in value): - fail(f"artifact target {target_id}.surfaces must be a non-empty string list") - return tuple(value) - - -def _artifact_target_from_row(row: dict[str, Any]) -> ArtifactTarget: - target_id = _target_string(row, "id", "") - assert target_id is not None - return ArtifactTarget( - id=target_id, - product=_target_string(row, "product", target_id) or "", - kind=_target_string(row, "kind", target_id) or "", - target=_target_string(row, "target", target_id) or "", - asset=_target_string(row, "asset", target_id) or "", - published=_target_bool(row, "published", target_id), - surfaces=_target_surfaces(row, target_id), - triple=_target_string(row, "triple", target_id, required=False), - runner=_target_string(row, "runner", target_id, required=False), - library_relative_path=_target_string(row, "library_relative_path", target_id, required=False), - executable_relative_path=_target_string(row, "executable_relative_path", target_id, required=False), - npm_package=_target_string(row, "npm_package", target_id, required=False), - npm_os=_target_string(row, "npm_os", target_id, required=False), - npm_cpu=_target_string(row, "npm_cpu", target_id, required=False), - npm_libc=_target_string(row, "npm_libc", target_id, required=False), - llvm_url=_target_string(row, "llvm_url", target_id, required=False), - extension_artifacts=_target_bool(row, "extension_artifacts", target_id, default=True), - ) - - -def _artifact_target_args( - *, - product: str | None = None, - kind: str | None = None, - surface: str | None = None, - published_only: bool = False, -) -> tuple[str, ...]: - args: list[str] = [] - if product is not None: - args.extend(["--product", product]) - if kind is not None: - args.extend(["--kind", kind]) - if surface is not None: - args.extend(["--surface", surface]) - if published_only: - args.append("--published-only") - return tuple(args) - - -def raw_artifact_target_tables(graph: dict | None = None) -> list[dict[str, Any]]: - """Return raw artifact target rows from the canonical Bun release graph.""" - - return [ - dict(row) - for row in _release_graph_query_rows("raw-artifact-targets") - ] - - -def legacy_central_artifact_target_rows() -> tuple[dict[str, Any], ...]: - return _release_graph_query_rows("legacy-central-artifact-targets") - - -def artifact_targets( - graph: dict | None = None, - *, - product: str | None = None, - kind: str | None = None, - surface: str | None = None, - published_only: bool = False, -) -> list[ArtifactTarget]: - rows = _release_graph_query_rows( - "artifact-targets", - _artifact_target_args( - product=product, - kind=kind, - surface=surface, - published_only=published_only, - ), - ) - return [_artifact_target_from_row(row) for row in rows] - - -@lru_cache(maxsize=1) -def _wasix_cargo_artifact_contract() -> dict[str, Any]: - value = _release_graph_query_json("wasix-cargo-artifact-contract") - if not isinstance(value, dict): - fail("release graph wasix-cargo-artifact-contract query must return a JSON object") - return value - - -def _wasix_contract_string(key: str) -> str: - value = _wasix_cargo_artifact_contract().get(key) - if not isinstance(value, str) or not value: - fail(f"WASIX Cargo artifact contract {key} must be a non-empty string") - return value - - -def _wasix_contract_string_list(key: str) -> tuple[str, ...]: - value = _wasix_cargo_artifact_contract().get(key) - if not isinstance(value, list) or not all(isinstance(item, str) and item for item in value): - fail(f"WASIX Cargo artifact contract {key} must be a string list") - return tuple(value) - - -def _wasix_contract_string_map(key: str) -> dict[str, str]: - value = _wasix_cargo_artifact_contract().get(key) - if not isinstance(value, dict) or not all( - isinstance(item_key, str) and item_key and isinstance(item_value, str) and item_value - for item_key, item_value in value.items() - ): - fail(f"WASIX Cargo artifact contract {key} must be a string map") - return dict(value) - - -def wasix_cargo_artifact_schema() -> str: - return _wasix_contract_string("schema") - - -def wasix_runtime_package_name() -> str: - return _wasix_contract_string("runtimePackage") - - -def wasix_tools_package_name() -> str: - return _wasix_contract_string("toolsPackage") - - -def wasix_icu_package_name() -> str: - return _wasix_contract_string("icuPackage") - - -def wasix_icu_payload_archive_name() -> str: - return _wasix_contract_string("icuPayloadArchive") - - -def wasix_aot_packages() -> dict[str, str]: - return _wasix_contract_string_map("aotPackages") - - -def wasix_tools_aot_packages() -> dict[str, str]: - return _wasix_contract_string_map("toolsAotPackages") - - -def wasix_aot_target_triples() -> dict[str, str]: - return _wasix_contract_string_map("aotTargetTriples") - - -def wasix_aot_target_cfgs() -> dict[str, str]: - return _wasix_contract_string_map("aotTargetCfgs") - - -def wasix_public_cargo_package_names() -> tuple[str, ...]: - return _wasix_contract_string_list("publicCargoPackageNames") - - -def wasix_public_aot_cargo_dependencies() -> dict[str, str]: - return _wasix_contract_string_map("publicAotCargoDependencies") - - -def wasix_public_tools_aot_cargo_dependencies() -> dict[str, str]: - return _wasix_contract_string_map("publicToolsAotCargoDependencies") - - -def wasix_public_tools_feature_dependencies() -> set[str]: - return set(_wasix_contract_string_list("publicToolsFeatureDependencies")) - - -def wasix_core_runtime_archive_files() -> tuple[str, ...]: - return _wasix_contract_string_list("coreRuntimeArchiveFiles") - - -def wasix_tools_payload_files() -> tuple[str, ...]: - return _wasix_contract_string_list("toolsPayloadFiles") - - -def wasix_forbidden_runtime_archive_tool_files() -> tuple[str, ...]: - return _wasix_contract_string_list("forbiddenRuntimeArchiveToolFiles") - - -def wasix_tools_aot_artifacts() -> set[str]: - return set(_wasix_contract_string_list("toolsAotArtifacts")) - - -def wasix_expected_extension_aot_targets() -> tuple[str, ...]: - return _wasix_contract_string_list("expectedExtensionAotTargets") - - -@lru_cache(maxsize=None) -def _wasix_extension_package_names(product: str, targets: tuple[str, ...] = ()) -> dict[str, Any]: - args: list[str] = ["--product", product] - for target in targets: - args.extend(["--target", target]) - value = _release_graph_query_json("wasix-extension-package-names", tuple(args)) - if not isinstance(value, dict): - fail("release graph wasix-extension-package-names query must return a JSON object") - if value.get("product") != product: - fail(f"release graph wasix-extension-package-names returned product {value.get('product')!r}, expected {product!r}") - package_name = value.get("packageName") - if not isinstance(package_name, str) or not package_name: - fail(f"release graph wasix-extension-package-names {product}.packageName must be a non-empty string") - aot_packages = value.get("aotPackages") - if not isinstance(aot_packages, list) or not all(isinstance(row, dict) for row in aot_packages): - fail(f"release graph wasix-extension-package-names {product}.aotPackages must be an object list") - return value - - -def wasix_extension_package_name(product: str) -> str: - return str(_wasix_extension_package_names(product).get("packageName")) - - -def wasix_extension_aot_package_name(product: str, target: str) -> str: - rows = _wasix_extension_package_names(product, (target,)).get("aotPackages") - assert isinstance(rows, list) - matches = [row for row in rows if row.get("target") == target] - if len(matches) != 1: - fail(f"release graph returned {len(matches)} WASIX extension AOT package names for {product}/{target}") - package_name = matches[0].get("packageName") - if not isinstance(package_name, str) or not package_name: - fail(f"release graph wasix-extension-package-names {product}/{target}.packageName must be a non-empty string") - return package_name - - -@lru_cache(maxsize=None) -def _expected_asset_rows( - product: str, - version: str, - surface: str, - published_only: bool, - kinds: tuple[str, ...] | None, -) -> tuple[dict[str, Any], ...]: - args: list[str] = ["--product", product, "--version", version, "--surface", surface] - if not published_only: - args.append("--include-unpublished") - if kinds is not None: - for kind in kinds: - args.extend(["--kind", kind]) - return _release_graph_query_rows("expected-assets", tuple(args)) - - -def _expected_asset_names(rows: Iterable[dict[str, Any]], *, context: str) -> list[str]: - names: list[str] = [] - for row in rows: - asset_name = row.get("assetName") - artifact_target = row.get("artifactTarget") - product = row.get("product") - kind = row.get("kind") - if not isinstance(asset_name, str) or not asset_name: - fail(f"release graph expected-assets {context} assetName must be a non-empty string") - if not isinstance(artifact_target, str) or not artifact_target: - fail(f"release graph expected-assets {asset_name}.artifactTarget must be a non-empty string") - if not isinstance(product, str) or not product: - fail(f"release graph expected-assets {asset_name}.product must be a non-empty string") - if not isinstance(kind, str) or not kind: - fail(f"release graph expected-assets {asset_name}.kind must be a non-empty string") - names.append(asset_name) - if len(names) != len(set(names)): - fail(f"release graph expected-assets returned duplicate names for {context}") - if not names: - fail(f"release graph returned no expected assets for {context}") - return sorted(names) - - -def expected_assets( - product: str, - version: str, - *, - surface: str = "github-release", - published_only: bool = True, - kinds: Iterable[str] | None = None, -) -> list[str]: - kind_tuple = None if kinds is None else tuple(sorted(set(kinds))) - return _expected_asset_names( - _expected_asset_rows(product, version, surface, published_only, kind_tuple), - context=f"{product}/{surface}", - ) - - -@lru_cache(maxsize=None) -def _ci_artifact_name_rows(family: str, product: str, kind: str) -> tuple[dict[str, Any], ...]: - return _release_graph_query_rows( - "ci-artifact-names", - ("--family", family, "--product", product, "--kind", kind), - ) - - -def _ci_artifact_names(family: str, product: str, kind: str) -> list[str]: - names: list[str] = [] - for row in _ci_artifact_name_rows(family, product, kind): - artifact_name = row.get("artifactName") - artifact_target = row.get("artifactTarget") - if row.get("family") != family or row.get("product") != product or row.get("kind") != kind: - fail(f"release graph ci-artifact-names returned an unexpected row for {family}/{product}/{kind}") - if not isinstance(artifact_name, str) or not artifact_name: - fail(f"release graph ci-artifact-names {family}/{product}/{kind} artifactName must be a non-empty string") - if not isinstance(artifact_target, str) or not artifact_target: - fail(f"release graph ci-artifact-names {family}/{product}/{kind} artifactTarget must be a non-empty string") - names.append(artifact_name) - if len(names) != len(set(names)): - fail(f"release graph ci-artifact-names returned duplicate artifacts for {family}/{product}/{kind}") - if not names: - fail(f"release graph returned no CI artifact names for {family}/{product}/{kind}") - return sorted(names) - - -def ci_release_asset_artifact_names(product: str, kind: str) -> list[str]: - return _ci_artifact_names("release-assets", product, kind) - - -def ci_npm_package_artifact_names(product: str, kind: str) -> list[str]: - return _ci_artifact_names("npm-package", product, kind) - - -@lru_cache(maxsize=None) -def _local_publish_artifact_rows(aggregate_only: bool = False) -> tuple[dict[str, Any], ...]: - args = ("--aggregate-only",) if aggregate_only else () - return _release_graph_query_rows("local-publish-artifacts", args) - - -def _local_publish_row_names(rows: Iterable[dict[str, Any]], *, context: str) -> list[str]: - names: list[str] = [] - for row in rows: - artifact_name = row.get("artifactName") - aggregate = row.get("aggregate") - family = row.get("family") - if not isinstance(artifact_name, str) or not artifact_name: - fail(f"release graph local-publish-artifacts {context} artifactName must be a non-empty string") - if not isinstance(aggregate, bool): - fail(f"release graph local-publish-artifacts {artifact_name}.aggregate must be true or false") - if not isinstance(family, str) or not family: - fail(f"release graph local-publish-artifacts {artifact_name}.family must be a non-empty string") - names.append(artifact_name) - if len(names) != len(set(names)): - fail(f"release graph local-publish-artifacts returned duplicate names for {context}") - if not names: - fail(f"release graph returned no local-publish artifacts for {context}") - return sorted(names) - - -def ci_local_publish_artifact_names(*, aggregate_only: bool = False) -> list[str]: - return _local_publish_row_names( - _local_publish_artifact_rows(aggregate_only), - context="aggregate-only" if aggregate_only else "full preset", - ) - - -def _local_publish_artifact_names_by_family(family: str, *, aggregate_only: bool = False) -> list[str]: - return _local_publish_row_names( - ( - row - for row in _local_publish_artifact_rows(aggregate_only) - if row.get("family") == family - ), - context=family, - ) - - -def ci_wasix_aot_runtime_artifact_names() -> list[str]: - return _local_publish_artifact_names_by_family("wasix-aot-runtime") - - -def ci_aggregate_release_asset_artifact_name(product: str) -> str: - names = _local_publish_row_names( - ( - row - for row in _local_publish_artifact_rows(aggregate_only=True) - if row.get("family") == "aggregate-release-assets" and row.get("product") == product - ), - context=f"aggregate release assets for {product}", - ) - if len(names) != 1: - fail(f"release graph returned {len(names)} aggregate release asset rows for {product}") - return names[0] - - -def ci_wasix_runtime_artifact_names() -> list[str]: - return _local_publish_artifact_names_by_family("wasix-runtime", aggregate_only=True) - - -@lru_cache(maxsize=1) -def _sdk_package_product_rows() -> tuple[dict[str, Any], ...]: - return _release_graph_query_rows("sdk-package-products") - - -def _sdk_package_product_row(product: str) -> dict[str, Any]: - matches = [row for row in _sdk_package_product_rows() if row.get("product") == product] - if len(matches) != 1: - fail(f"release graph sdk-package-products query must return one row for SDK product {product}, got {len(matches)}") - return dict(matches[0]) - - -def _sdk_row_string(row: dict[str, Any], key: str, product: str) -> str: - value = row.get(key) - if not isinstance(value, str) or not value: - fail(f"release graph sdk-package-products {product}.{key} must be a non-empty string") - return value - - -def ci_sdk_package_artifact_name(product: str) -> str: - return _sdk_row_string(_sdk_package_product_row(product), "artifactName", product) - - -def sdk_package_products() -> tuple[str, ...]: - products = tuple(_sdk_row_string(row, "product", "") for row in _sdk_package_product_rows()) - if len(products) != len(set(products)): - fail("release graph sdk-package-products query returned duplicate SDK products") - if not products: - fail("release graph returned no SDK package products") - return products - - -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()] - - -@lru_cache(maxsize=1) -def _typescript_optional_runtime_package_version_rows() -> tuple[dict[str, Any], ...]: - return _release_graph_query_rows("typescript-optional-runtime-package-versions") - - -def typescript_optional_runtime_package_versions() -> dict[str, str]: - versions: dict[str, str] = {} - for row in _typescript_optional_runtime_package_version_rows(): - package_name = row.get("packageName") - product = row.get("product") - version = row.get("version") - artifact_target = row.get("artifactTarget") - if not isinstance(package_name, str) or not package_name: - fail("typescript-optional-runtime-package-versions rows must declare a non-empty packageName") - if not isinstance(product, str) or not product: - fail(f"typescript-optional-runtime-package-versions {package_name}.product must be a non-empty string") - if not isinstance(version, str) or not version: - fail(f"typescript-optional-runtime-package-versions {package_name}.version must be a non-empty string") - if not isinstance(artifact_target, str) or not artifact_target: - fail( - f"typescript-optional-runtime-package-versions {package_name}.artifactTarget " - "must be a non-empty string" - ) - if package_name in versions: - fail(f"duplicate TypeScript optional runtime package target {package_name}") - versions[package_name] = version - if not versions: - fail("release graph returned no TypeScript optional runtime package versions") - return versions - - -@lru_cache(maxsize=None) -def _product_config_rows(product: str | None = None) -> tuple[dict[str, Any], ...]: - args = () if product is None else ("--product", product) - rows = _release_graph_query_rows("product-configs", args) - if product is not None and len(rows) != 1: - fail(f"release graph product-configs query must return one row for {product}, got {len(rows)}") - seen: set[str] = set() - parsed: list[dict[str, Any]] = [] - for row in rows: - product_id = row.get("product") - config_id = row.get("id") - if not isinstance(product_id, str) or not product_id: - fail("release graph product-configs rows must declare a non-empty product") - if product_id in seen: - fail(f"release graph product-configs query returned duplicate product {product_id}") - seen.add(product_id) - if config_id != product_id: - fail(f"release graph product-configs {product_id}.id must match the product id") - for key in ["kind", "owner", "path", "changelog_path", "tag_prefix"]: - value = row.get(key) - if not isinstance(value, str) or not value: - fail(f"release graph product-configs {product_id}.{key} must be a non-empty string") - for key in ["publish_targets", "release_artifacts", "version_files"]: - value = row.get(key) - if not isinstance(value, list) or not all(isinstance(item, str) for item in value): - fail(f"release graph product-configs {product_id}.{key} must be a string list") - if not value: - fail(f"release graph product-configs {product_id}.{key} must not be empty") - for key in ["registry_packages", "derived_version_files"]: - value = row.get(key) - if value is None: - row[key] = [] - continue - if not isinstance(value, list) or not all(isinstance(item, str) for item in value): - fail(f"release graph product-configs {product_id}.{key} must be a string list") - parsed.append(dict(row)) - if not parsed: - fail("release graph returned no product config rows") - return tuple(parsed) - - -def _product_config_row(product: str) -> dict[str, Any]: - row = dict(_product_config_rows(product)[0]) - row.pop("product", None) - return row - - -def graph_products(graph: dict | None = None) -> dict[str, dict[str, Any]]: - return { - str(row["product"]): _product_config_row(str(row["product"])) - for row in _product_config_rows() - } - - -def product_config(product: str, graph: dict | None = None) -> dict[str, Any]: - return _product_config_row(product) - - -def product_ids(graph: dict | None = None) -> list[str]: - return [str(row["product"]) for row in _product_config_rows()] - - -def extension_product_ids(graph: dict | None = None) -> list[str]: - products: list[str] = [] - for row in _extension_metadata_rows(): - product = row.get("product") - if not isinstance(product, str) or not product: - fail("release graph extension-metadata rows must declare a non-empty product") - products.append(product) - if len(products) != len(set(products)): - fail("release graph extension-metadata query returned duplicate extension products") - if not products: - fail("release graph returned no extension products") - return sorted(products) - - -@lru_cache(maxsize=None) -def extension_artifact_targets( - *, - product: str | None = None, - family: str | None = None, - published_only: bool = False, -) -> tuple[SimpleNamespace, ...]: - args: list[str] = [] - if product is not None: - args.extend(["--product", product]) - if family is not None: - args.extend(["--family", family]) - if published_only: - args.append("--published-only") - rows = _release_graph_query_rows("extension-targets", tuple(args)) - return tuple(SimpleNamespace(**row) for row in rows) - - -def published_android_maven_targets(product: str) -> tuple[SimpleNamespace, ...]: - return tuple( - sorted( - ( - target - for target in extension_artifact_targets( - product=product, - family="native", - published_only=True, - ) - if target.kind == "native-static-registry" and target.target.startswith("android-") - ), - key=lambda target: target.target, - ) - ) - - -def published_extension_target_ids(*, family: str) -> list[str]: - return sorted( - { - target.target - for target in extension_artifact_targets(family=family, published_only=True) - } - ) - - -def ci_wasix_extension_artifact_names() -> list[str]: - return _local_publish_artifact_names_by_family("wasix-extension-artifacts", aggregate_only=True) - - -def ci_extension_package_artifact_names() -> list[str]: - return _local_publish_artifact_names_by_family("extension-package-artifacts", aggregate_only=True) - - -def string_list(config: dict, key: str, product: str) -> list[str]: - value = config.get(key, []) - if not isinstance(value, list) or not all(isinstance(item, str) for item in value): - fail(f"{product}.{key} must be a string list") - return value - - -@lru_cache(maxsize=None) -def _registry_package_rows(product: str, package_kind: str | None = None) -> tuple[dict[str, Any], ...]: - args = ["--product", product] - if package_kind is not None: - args.extend(["--kind", package_kind]) - return _release_graph_query_rows("registry-packages", tuple(args)) - - -def registry_package_names(product: str, package_kind: str) -> list[str]: - names: list[str] = [] - for row in _registry_package_rows(product, package_kind): - row_product = row.get("product") - kind = row.get("packageKind") - name = row.get("packageName") - if row_product != product: - fail(f"release graph registry-packages returned row for {row_product!r}, expected {product!r}") - if kind != package_kind: - fail(f"release graph registry-packages returned {product}.{kind!r}, expected {package_kind!r}") - if not isinstance(name, str) or not name: - fail(f"release graph registry-packages {product}.{package_kind} packageName must be a non-empty string") - 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 - - -@lru_cache(maxsize=1) -def _extension_metadata_rows() -> tuple[dict[str, Any], ...]: - return _release_graph_query_rows("extension-metadata") - - -def _extension_metadata_row(product: str) -> dict[str, Any]: - matches = [row for row in _extension_metadata_rows() if row.get("product") == product] - if len(matches) != 1: - fail(f"release graph extension-metadata query must return one row for {product}, got {len(matches)}") - return dict(matches[0]) - - -def _metadata_string(row: dict[str, Any], key: str, product: str) -> str: - value = row.get(key) - if not isinstance(value, str) or not value: - fail(f"extension-metadata {product}.{key} must be a non-empty string") - return value - - -def _metadata_object(row: dict[str, Any], key: str, product: str) -> dict[str, Any]: - value = row.get(key) - if not isinstance(value, dict): - fail(f"extension-metadata {product}.{key} must be an object") - return dict(value) - - -def extension_metadata(product: str, graph: dict | None = None) -> dict[str, Any]: - row = _extension_metadata_row(product) - compatibility = _metadata_object(row, "compatibility", product) - for key in [ - "postgresMajor", - "extensionRuntimeContract", - "nativeRuntimeProduct", - "nativeRuntimeVersion", - "wasixRuntimeProduct", - "wasixRuntimeVersion", - ]: - if not isinstance(compatibility.get(key), str) or not compatibility[key]: - fail(f"extension-metadata {product}.compatibility.{key} must be a non-empty string") - return { - "sqlName": _metadata_string(row, "sqlName", product), - "class": _metadata_string(row, "class", product), - "versioning": _metadata_string(row, "versioning", product), - "sourcePath": _metadata_string(row, "sourcePath", product), - "compatibility": compatibility, - } - - -def extension_source_identity(product: str, graph: dict | None = None) -> dict[str, Any]: - return _metadata_object(_extension_metadata_row(product), "sourceIdentity", product) - - -def validate_extension_metadata(product: str, graph: dict | None = None) -> None: - extension_metadata(product, graph) - - -def validate_all_extension_metadata(graph: dict | None = None) -> None: - for product in extension_product_ids(): - validate_extension_metadata(product, graph) - - -def _graph_string(config: dict[str, Any], key: str, product: str) -> str: - value = config.get(key) - if not isinstance(value, str) or not value: - fail(f"release graph product {product}.{key} must be a non-empty string") - return value - - -def _graph_string_list(config: dict[str, Any], key: str, product: str) -> list[str]: - value = config.get(key) - if not isinstance(value, list) or not value or not all(isinstance(item, str) and item for item in value): - fail(f"release graph product {product}.{key} must be a non-empty string list") - return list(value) - - -def version_files(product: str, graph: dict | None = None) -> list[str]: - files = _graph_string_list(product_config(product, graph), "version_files", product) - for path in files: - if not (ROOT / path).is_file(): - fail(f"{product} version file does not exist: {path}") - return files - - -def derived_version_files(product: str, graph: dict | None = None) -> list[str]: - value = product_config(product, graph).get("derived_version_files", []) - if not isinstance(value, list) or not all(isinstance(item, str) for item in value): - fail(f"release graph product {product}.derived_version_files must be a string list") - return list(value) - - -def changelog_path(product: str, graph: dict | None = None) -> str: - path = _graph_string(product_config(product, graph), "changelog_path", product) - if not (ROOT / path).is_file(): - fail(f"{product} changelog does not exist: {path}") - return path - - -def tag_prefix(product: str, graph: dict | None = None) -> str: - return _graph_string(product_config(product, graph), "tag_prefix", product) - - -@lru_cache(maxsize=1) -def _product_version_rows() -> tuple[dict[str, Any], ...]: - return _release_graph_query_rows("product-versions") - - -def _product_version_row(product: str) -> dict[str, Any]: - matches = [row for row in _product_version_rows() if row.get("product") == product] - if len(matches) != 1: - fail(f"release graph product-versions query must return one row for {product}, got {len(matches)}") - return dict(matches[0]) - - -def read_current_version(product: str, graph: dict | None = None) -> str: - version = _product_version_row(product).get("version") - if not isinstance(version, str) or not version: - fail(f"release graph product-versions {product}.version must be a non-empty string") - return version - - -if __name__ == "__main__": - fail( - "tools/release/product_metadata.py is a Python compatibility module; " - "use tools/dev/bun.sh tools/release/product-version.mjs version for version reads" - ) From af614dde5048b39babae73810598b229eb191aab Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 21:04:15 +0000 Subject: [PATCH 226/308] chore: remove release ci compatibility commands --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 12 +- tools/release/check_release_metadata.py | 4 + tools/release/release.py | 116 ------------------ 3 files changed, 15 insertions(+), 117 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index d8e23658..394b7a4b 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,16 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Removed the obsolete `release.py ci-products` and + `release.py ci-artifacts` compatibility commands after the release workflow + and CI assertions moved to direct Bun `release_graph_query.mjs` calls. The + release CLI no longer carries the CI artifact-name helper adapters, and + `check_release_metadata.py` now rejects reintroducing those subcommands. + Fresh checks passed: `rg` proving no active `release.py ci-*` command surface + remains outside historical notes, direct Bun `ci-products` and + `ci-artifact-names` smokes for SDK products, native release assets, + Node-direct npm packages, and broker release assets, `python3 -m py_compile` + for touched release helpers, and `check_release_metadata.py`. - 2026-06-27: Deleted the unused `tools/release/product_metadata.py` compatibility module now that executable release consumers query `release_graph_query.mjs` directly. `check_release_metadata.py` now fails if @@ -89,7 +99,7 @@ until the current-state gates here are checked with fresh local evidence. `product_metadata.*` calls remain, `tools/dev/bun.sh tools/policy/check-python-entrypoints.mjs --json`, `python3 -m py_compile` for touched Python release/policy helpers, `check_release_metadata.py`, and - `release.py ci-products --family sdk-package`. + direct Bun `release_graph_query.mjs ci-products --family sdk-package`. - 2026-06-27: Removed stale `tools/release/product_metadata.py` Moon task inputs from Node-direct `check`/`release-assets` and native `release-assets` tasks after those paths moved to Bun release graph queries. diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 9339c246..a0c0dd1a 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -732,6 +732,8 @@ def validate_graph_files() -> None: or "sdk-package-products [--product PRODUCT]" not in release_graph_query or "ci-products --family sdk-package" not in release_graph_query or "sdkPackageProducts(TOOL)" not in release_graph_query + or "def command_ci_products(" in release_source + or '"ci-products"' in release_source ): fail("SDK package product and CI artifact-name selection must come from the shared Bun release graph query") if ( @@ -740,6 +742,8 @@ def validate_graph_files() -> None: or "ci-artifact-names --family release-assets|npm-package|sdk-package --product PRODUCT" not in release_graph_query or "ciReleaseAssetArtifactRows(product, kind, TOOL)" not in release_graph_query or "ciNpmPackageArtifactRows(product, kind, TOOL)" not in release_graph_query + or "def command_ci_artifacts(" in release_source + or '"ci-artifacts"' in release_source ): fail("CI release asset and npm package artifact names must come from the shared Bun artifact target helper") if ( diff --git a/tools/release/release.py b/tools/release/release.py index 9dc132ef..d2837057 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -394,79 +394,6 @@ def expected_assets( return sorted(names) -@lru_cache(maxsize=None) -def ci_artifact_name_rows(family: str, product: str, kind: str) -> tuple[dict[str, Any], ...]: - return release_graph_rows( - "ci-artifact-names", - ("--family", family, "--product", product, "--kind", kind), - ) - - -def ci_artifact_names(family: str, product: str, kind: str) -> list[str]: - names: list[str] = [] - for row in ci_artifact_name_rows(family, product, kind): - artifact_name = row.get("artifactName") - artifact_target = row.get("artifactTarget") - if row.get("family") != family or row.get("product") != product or row.get("kind") != kind: - fail(f"release graph ci-artifact-names returned an unexpected row for {family}/{product}/{kind}") - if not isinstance(artifact_name, str) or not artifact_name: - fail(f"release graph ci-artifact-names {family}/{product}/{kind} artifactName must be a non-empty string") - if not isinstance(artifact_target, str) or not artifact_target: - fail(f"release graph ci-artifact-names {family}/{product}/{kind} artifactTarget must be a non-empty string") - names.append(artifact_name) - if len(names) != len(set(names)): - fail(f"release graph ci-artifact-names returned duplicate artifacts for {family}/{product}/{kind}") - if not names: - fail(f"release graph returned no CI artifact names for {family}/{product}/{kind}") - return sorted(names) - - -def ci_release_asset_artifact_names(product: str, kind: str) -> list[str]: - return ci_artifact_names("release-assets", product, kind) - - -def ci_npm_package_artifact_names(product: str, kind: str) -> list[str]: - return ci_artifact_names("npm-package", product, kind) - - -@lru_cache(maxsize=1) -def sdk_package_product_rows() -> tuple[dict[str, Any], ...]: - return release_graph_rows("sdk-package-products") - - -def sdk_package_product_row(product: str) -> dict[str, Any]: - matches = [row for row in sdk_package_product_rows() if row.get("product") == product] - if len(matches) != 1: - fail(f"release graph sdk-package-products query must return one row for SDK product {product}, got {len(matches)}") - return dict(matches[0]) - - -def sdk_package_product_string(row: dict[str, Any], key: str, product: str) -> str: - value = row.get(key) - if not isinstance(value, str) or not value: - fail(f"release graph sdk-package-products {product}.{key} must be a non-empty string") - return value - - -def ci_sdk_package_artifact_name(product: str) -> str: - return sdk_package_product_string(sdk_package_product_row(product), "artifactName", product) - - -def sdk_package_products() -> tuple[str, ...]: - products = tuple(sdk_package_product_string(row, "product", "") for row in sdk_package_product_rows()) - if len(products) != len(set(products)): - fail("release graph sdk-package-products query returned duplicate SDK products") - if not products: - fail("release graph returned no SDK package products") - return products - - -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()] - - @lru_cache(maxsize=None) def registry_package_rows(product: str, package_kind: str | None = None) -> tuple[dict[str, Any], ...]: args = ["--product", product] @@ -2322,43 +2249,6 @@ 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") - 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 = ci_release_asset_artifact_names(parsed.product, parsed.kind) - elif parsed.family == "npm-package": - if parsed.kind is None: - fail("ci-artifacts --family npm-package requires --kind") - names = 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 = ci_sdk_package_artifact_names(parsed.product) - for name in names: - 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(sdk_package_products()) - if parsed.products_json is None: - products = list(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 @@ -3844,8 +3734,6 @@ def main(argv: list[str]) -> int: "check", "check-registries", "consumer-shape", - "ci-artifacts", - "ci-products", "prepare-rust-release-source", "verify-release", ]: @@ -3873,10 +3761,6 @@ 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 == "ci-products": - command_ci_products(passthrough) elif command == "prepare-rust-release-source": command_prepare_rust_release_source(passthrough) elif command == "verify-release": From dcbfc8ee24ba75ff0f407a573f77fe2783058092 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 21:13:42 +0000 Subject: [PATCH 227/308] chore: remove release plan compatibility command --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 43 +++++++++++++------ docs/internal/IMPLEMENTATION_CHECKLIST.md | 6 +-- tools/release/check_release_metadata.py | 5 ++- tools/release/release.py | 14 +----- 4 files changed, 38 insertions(+), 30 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 394b7a4b..62927f15 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -271,19 +271,33 @@ until the current-state gates here are checked with fresh local evidence. evidence still shows `actions/upload-artifact@v7` `mime_type` incompatibility, so artifact-dependent downstream CI jobs remain not fully provable with local `act` on this host. +- 2026-06-27: Removed the final `tools/release/release.py plan` compatibility + command. Release planning now uses only `tools/dev/bun.sh + tools/release/release_plan.mjs`; `check_release_metadata.py` rejects + reintroducing the Python planner command surface in `release.py` or in release + PR coverage checks. Fresh checks passed: `tools/dev/bun.sh + tools/release/release_plan.mjs --format json`, `python3 -m py_compile + tools/release/release.py tools/release/check_release_metadata.py`, `python3 + tools/release/check_release_metadata.py`, `python3 + tools/policy/check-release-policy.py`, `bash tools/policy/check-docs.sh`, + `tools/release/release.py check`, `tools/dev/bun.sh + tools/policy/check-python-entrypoints.mjs --json`, `git diff --check`, and a + source-tree scan for stray `__pycache__` or `.pyc` files. The Python + entrypoint inventory now reports 8 entries; `release.py` is 3,766 lines and + 152,680 bytes. - 2026-06-27: Switched `check_release_pr_coverage.mjs` from the Python `release.py plan` compatibility wrapper to the Bun `tools/release/release_plan.mjs` entrypoint. The release PR coverage checker remains a Bun checker end to end: it now reads release-please manifest diffs and Moon-selected release products from the same canonical Bun planner used - by the release workflow and release-intent check, while `release.py plan` - remains only a compatibility shim. `check_release_metadata.py` now rejects + by the release workflow and release-intent check. `check_release_metadata.py` + now rejects reintroducing the Python planner wrapper in the release PR coverage checker. Fresh checks passed: `tools/dev/bun.sh - tools/release/check_release_pr_coverage.mjs`, direct parity diff between + tools/release/check_release_pr_coverage.mjs`, direct parity diff at the time between `tools/dev/bun.sh tools/release/release_plan.mjs --base-ref origin/main - --head-ref HEAD --format json` and `tools/release/release.py plan --base-ref - origin/main --head-ref HEAD --format json`, active-file grep proving + --head-ref HEAD --format json` and the then-existing `tools/release/release.py + plan --base-ref origin/main --head-ref HEAD --format json`, active-file grep proving `check_release_pr_coverage.mjs` no longer calls `release.py`, `python3 -m py_compile tools/release/check_release_metadata.py`, `python3 tools/release/check_release_metadata.py`, `tools/release/release.py check`, @@ -328,14 +342,16 @@ until the current-state gates here are checked with fresh local evidence. `release.py plan` compatibility wrapper to the Bun `tools/release/release_plan.mjs` entrypoint. The release workflow, release-intent checker, CI summary action, maintainer release docs, and - architecture release-model docs now point at the Bun planner, while - `release.py plan` remains as a compatibility shim that delegates to the same - script. `assert-ci-workflows.mjs` now rejects the Python planner wrapper in + architecture release-model docs now point at the Bun planner. At the time, + `release.py plan` remained as a compatibility shim that delegated to the same + script; a later follow-up removed that command. `assert-ci-workflows.mjs` now + rejects the Python planner wrapper in active workflow surfaces and requires the Bun planner command. Fresh checks passed: `bash -n .github/scripts/check-release-intent.sh`, `python3 -m py_compile tools/policy/check-release-policy.py`, `tools/dev/bun.sh - tools/release/release_plan.mjs --format json`, `tools/release/release.py plan - --format json`, direct JSON parity diff between those two planners, + tools/release/release_plan.mjs --format json`, then-existing + `tools/release/release.py plan --format json`, direct JSON parity diff between + those two planners, `tools/dev/bun.sh tools/policy/assertions/assert-ci-workflows.mjs`, `python3 tools/policy/check-release-policy.py`, `python3 tools/release/check_release_metadata.py`, `bash tools/policy/check-docs.sh`, @@ -2478,9 +2494,10 @@ until the current-state gates here are checked with fresh local evidence. - 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 + `release_plan.mjs` replaced the old Python planner; before the later + compatibility-command removal, it also backed `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. - 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 diff --git a/docs/internal/IMPLEMENTATION_CHECKLIST.md b/docs/internal/IMPLEMENTATION_CHECKLIST.md index 049f1695..d08c6d70 100644 --- a/docs/internal/IMPLEMENTATION_CHECKLIST.md +++ b/docs/internal/IMPLEMENTATION_CHECKLIST.md @@ -54,7 +54,7 @@ or CI/build output proves the contract. `android-x86_64` extension artifacts while iOS mobile builds request only `ios-xcframework`. - [x] Moon dependency scopes encode release-affecting versus build-only edges. - Evidence: `tools/release/release.py plan --changed-file ... --format json` + Evidence: `tools/dev/bun.sh tools/release/release_plan.mjs --changed-file ... --format json` probes prove extension catalog changes run affected CI without releases, exact extension target changes release only that extension product, native runtime patches release native plus production downstream products, and @@ -316,7 +316,7 @@ or CI/build output proves the contract. Bun release graph. - [x] There is no active `release-graph.toml`, `release-inputs.toml`, or `tools/graph/jobs.toml` release brain. -- [x] `tools/release/release.py plan` uses Moon project ownership and dependency +- [x] `tools/dev/bun.sh tools/release/release_plan.mjs` uses Moon project ownership and dependency scopes for release closure. Evidence: direct release-plan probes for extension catalog, PostGIS target metadata, native runtime patch, and WASIX runtime patch paths. @@ -585,7 +585,7 @@ Run before claiming this architecture complete: `wasix-rust-package`, SDK packages, extension packages, or mobile builders. The emitted AOT matrix contains the single friendly target id `linux-x64-gnu`. -- [x] `tools/release/release.py plan` +- [x] `tools/dev/bun.sh tools/release/release_plan.mjs` - [x] `tools/release/release.py check` - [x] `tools/release/release.py consumer-shape --format json --require-ready --products-json '["oliphaunt-swift"]'` diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index a0c0dd1a..41432706 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -717,8 +717,11 @@ def validate_graph_files() -> None: "tools/release/release_plan.mjs" not in release_pr_coverage or "tools/release/release.py', [\n 'plan'" in release_pr_coverage or 'tools/release/release.py", [\n "plan"' in release_pr_coverage + or "def command_plan(" in release_source + or 'if command == "plan":' in release_source + or 'for name in [\n "plan",' in release_source ): - fail("release PR coverage must call the Bun release planner directly") + fail("release planning must use the Bun release planner directly") if ( "function typescriptOptionalRuntimePackageProducts(" in sync_release_pr or "export function typescriptOptionalRuntimePackageProducts(" not in release_artifact_targets diff --git a/tools/release/release.py b/tools/release/release.py index d2837057..3c5268a0 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -2192,15 +2192,6 @@ def run_product_publish_dry_runs(products: list[str], *, allow_dirty: bool, head fail(f"no publish dry-run handler for {product}") -def command_plan(args: list[str]) -> None: - 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: run(["python3", "tools/policy/check-release-policy.py"]) run(["tools/release/check_release_please_config.mjs"]) @@ -3730,7 +3721,6 @@ def main(argv: list[str]) -> int: subparsers = parser.add_subparsers(dest="command", required=True) for name in [ - "plan", "check", "check-registries", "consumer-shape", @@ -3753,9 +3743,7 @@ def main(argv: list[str]) -> int: args, passthrough = parser.parse_known_args(argv) command = args.command - if command == "plan": - command_plan(passthrough) - elif command == "check": + if command == "check": command_check(passthrough) elif command == "check-registries": command_check_registries(passthrough) From 5fa3b8dcccff4cf37a738a368f7eee8e5d00459d Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 21:35:25 +0000 Subject: [PATCH 228/308] chore: port sdk artifact builder to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 29 +- docs/internal/IMPLEMENTATION_CHECKLIST.md | 21 +- src/bindings/wasix-rust/moon.yml | 5 +- src/sdks/js/moon.yml | 8 +- src/sdks/kotlin/moon.yml | 5 +- src/sdks/react-native/moon.yml | 8 +- src/sdks/rust/moon.yml | 6 +- src/sdks/swift/moon.yml | 6 +- .../policy/assertions/assert-ci-workflows.mjs | 4 +- tools/policy/check-moon-product-graph.mjs | 20 +- tools/policy/check-sdk-parity.sh | 2 +- tools/policy/check-tooling-stack.sh | 10 +- tools/release/build-sdk-ci-artifacts.mjs | 359 ++++++++++++++++++ tools/release/build-sdk-ci-artifacts.sh | 194 ---------- tools/release/check_artifact_targets.py | 20 +- tools/release/check_release_metadata.py | 10 +- 16 files changed, 454 insertions(+), 253 deletions(-) create mode 100755 tools/release/build-sdk-ci-artifacts.mjs delete mode 100755 tools/release/build-sdk-ci-artifacts.sh diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 62927f15..5e69e9a9 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,29 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Ported the shared SDK package artifact builder from + `tools/release/build-sdk-ci-artifacts.sh` to the Bun entrypoint + `tools/release/build-sdk-ci-artifacts.mjs`. Moon package-artifact tasks for + Rust, Swift, Kotlin, TypeScript, React Native, and WASIX Rust now call the + pinned Bun launcher directly; policy checks still require package-shape + outputs, staged SDK artifact validation, Kotlin Maven repository staging, + Swift release-manifest rendering, TypeScript JSR source staging, and WASIX + Rust registry-shaped crate packaging. Fresh checks passed: + `tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs --help`, + `node --check tools/release/build-sdk-ci-artifacts.mjs`, + `tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs + oliphaunt-wasix-rust`, `tools/dev/bun.sh + tools/policy/check-moon-product-graph.mjs`, `tools/dev/bun.sh + tools/policy/assertions/assert-ci-workflows.mjs`, `bash + tools/policy/check-sdk-parity.sh`, `bash tools/policy/check-tooling-stack.sh`, + `python3 -m py_compile tools/release/check_artifact_targets.py + tools/release/check_release_metadata.py`, `python3 + tools/release/check_artifact_targets.py`, and `python3 + tools/release/check_release_metadata.py`. Follow-up aggregate gates also + passed: `tools/release/release.py check`, `bash tools/policy/check-docs.sh`, + `tools/dev/bun.sh tools/policy/check-python-entrypoints.mjs --json`, `git + diff --check`, and a source-tree scan for stray `__pycache__` or `.pyc` + files. - 2026-06-27: Removed the obsolete `release.py ci-products` and `release.py ci-artifacts` compatibility commands after the release workflow and CI assertions moved to direct Bun `release_graph_query.mjs` calls. The @@ -1670,8 +1693,8 @@ until the current-state gates here are checked with fresh local evidence. `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`, + `bash -n src/sdks/swift/tools/check-sdk.sh`, + `tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs --help`, `python3 tools/release/check_release_metadata.py`, `python3 tools/release/check_consumer_shape.py --products-json '["oliphaunt-swift"]'`, `tools/dev/bun.sh @@ -2057,7 +2080,7 @@ until the current-state gates here are checked with fresh local evidence. 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 + `tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs 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 diff --git a/docs/internal/IMPLEMENTATION_CHECKLIST.md b/docs/internal/IMPLEMENTATION_CHECKLIST.md index d08c6d70..156870bd 100644 --- a/docs/internal/IMPLEMENTATION_CHECKLIST.md +++ b/docs/internal/IMPLEMENTATION_CHECKLIST.md @@ -249,7 +249,7 @@ or CI/build output proves the contract. staged validation rather than invoking `check-sdk.sh`, Gradle local publish, `cargo package`, or `cargo publish --dry-run`. - [x] Kotlin SDK builder artifacts use the consumer-facing Maven repository as - the package boundary. Evidence: `tools/release/build-sdk-ci-artifacts.sh` + the package boundary. Evidence: `tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs` stages `target/sdk-artifacts/oliphaunt-kotlin/maven` only, React Native Android derives the Kotlin dependency from that staged Maven repo, and `tools/dev/bun.sh tools/release/check-staged-artifacts.mjs` now requires the Maven repository @@ -458,7 +458,7 @@ or CI/build output proves the contract. - [x] Kotlin SDK package artifacts include an Android-consumable Maven repository layout for both `oliphaunt-android` and the `dev.oliphaunt.android` Gradle plugin. Evidence: - `tools/release/build-sdk-ci-artifacts.sh oliphaunt-kotlin` passes and stages + `tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs oliphaunt-kotlin` passes and stages both Maven artifacts under `target/sdk-artifacts/oliphaunt-kotlin/maven`. - [x] React Native package artifacts exclude native runtime/resource payloads. Evidence: `src/sdks/react-native/package.json` excludes @@ -492,7 +492,7 @@ or CI/build output proves the contract. package handoff. GitHub CI run `27744307637` adds same-SHA Android and iOS installed-app E2E evidence from staged mobile app artifacts. - [x] TypeScript package artifacts stay SDK-scoped. Evidence: - `tools/release/build-sdk-ci-artifacts.sh oliphaunt-js` stages the npm tarball + `tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs oliphaunt-js` stages the npm tarball and JSR source only; the affected planner now selects only `js-sdk-package` for `oliphaunt-js:package-artifacts`. Broker and Node-direct helper artifacts are built and downloaded only when the helper products themselves are being @@ -539,8 +539,7 @@ or CI/build output proves the contract. Run before claiming this architecture complete: -- [x] `bash -n tools/release/build-sdk-ci-artifacts.sh - src/sdks/swift/tools/check-sdk.sh` +- [x] `tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs --help` - [x] `python3 -m py_compile tools/release/release.py tools/release/build-extension-ci-artifacts.mjs tools/release/check_artifact_targets.py @@ -595,17 +594,17 @@ Run before claiming this architecture complete: - [x] `python3 tools/release/artifact_target_matrix.py liboliphaunt-wasix-aot-runtime` emits friendly `target_id` values for every WASIX AOT builder target from product-local target metadata. -- [x] `tools/release/build-sdk-ci-artifacts.sh oliphaunt-js` -- [x] `tools/release/build-sdk-ci-artifacts.sh oliphaunt-kotlin` -- [x] `tools/release/build-sdk-ci-artifacts.sh oliphaunt-react-native` -- [x] `tools/release/build-sdk-ci-artifacts.sh oliphaunt-rust` -- [x] `tools/release/build-sdk-ci-artifacts.sh oliphaunt-wasix-rust` +- [x] `tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs oliphaunt-js` +- [x] `tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs oliphaunt-kotlin` +- [x] `tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs oliphaunt-react-native` +- [x] `tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs oliphaunt-rust` +- [x] `tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs oliphaunt-wasix-rust` - [x] `MOON_BIN=$HOME/.proto/shims/moon .github/scripts/run-moon-targets.sh oliphaunt-rust:package-artifacts` - [x] `MOON_BIN=$HOME/.proto/shims/moon .github/scripts/run-moon-targets.sh oliphaunt-wasix-rust:package-artifacts` - [x] `OLIPHAUNT_SWIFT_RELEASE_ASSET_DIR=$PWD/target/test-fixtures/liboliphaunt-swift-release - tools/release/build-sdk-ci-artifacts.sh oliphaunt-swift` passes against a + tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs oliphaunt-swift` passes against a deterministic release-shaped liboliphaunt fixture whose Apple SwiftPM XCFramework zip has macOS, iOS device, and iOS simulator slices. This proves the Swift SDK package artifact path renders a checksum-pinned public diff --git a/src/bindings/wasix-rust/moon.yml b/src/bindings/wasix-rust/moon.yml index 2d5c8c4c..4068fd37 100644 --- a/src/bindings/wasix-rust/moon.yml +++ b/src/bindings/wasix-rust/moon.yml @@ -87,7 +87,7 @@ tasks: package-artifacts: tags: ["release", "artifact-package", "ci-wasix-rust-package"] - command: "bash tools/release/build-sdk-ci-artifacts.sh oliphaunt-wasix-rust" + command: "tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs oliphaunt-wasix-rust" deps: - "oliphaunt-wasix-rust:package" env: @@ -99,7 +99,8 @@ tasks: - "/src/bindings/wasix-rust/**/*" - "/src/runtimes/liboliphaunt/wasix/crates/**/*" - "/src/bindings/wasix-rust/tools/check-package.sh" - - "/tools/release/build-sdk-ci-artifacts.sh" + - "/tools/release/build-sdk-ci-artifacts.mjs" + - "/tools/release/check-staged-artifacts.mjs" - "/tools/release/package_oliphaunt_wasix_sdk_crate.mjs" outputs: - "/target/sdk-artifacts/oliphaunt-wasix-rust/**/*" diff --git a/src/sdks/js/moon.yml b/src/sdks/js/moon.yml index 8d9fbc9c..7d3e60df 100644 --- a/src/sdks/js/moon.yml +++ b/src/sdks/js/moon.yml @@ -112,7 +112,8 @@ tasks: - "/pnpm-workspace.yaml" - "/src/sdks/js/**/*" - "/src/runtimes/node-direct/**/*" - - "/tools/release/build-sdk-ci-artifacts.sh" + - "/tools/release/build-sdk-ci-artifacts.mjs" + - "/tools/release/check-staged-artifacts.mjs" - "/tools/release/release.py" - "/tools/test/**/*" - "/tools/runtime/**/*" @@ -123,7 +124,7 @@ tasks: runFromWorkspaceRoot: true package-artifacts: tags: ["release", "artifact-package", "ci-js-sdk-package"] - command: "bash tools/release/build-sdk-ci-artifacts.sh oliphaunt-js" + command: "tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs oliphaunt-js" deps: - "oliphaunt-js:package" inputs: @@ -134,7 +135,8 @@ tasks: - "/pnpm-workspace.yaml" - "/src/sdks/js/**/*" - "/src/runtimes/node-direct/**/*" - - "/tools/release/build-sdk-ci-artifacts.sh" + - "/tools/release/build-sdk-ci-artifacts.mjs" + - "/tools/release/check-staged-artifacts.mjs" - "/tools/release/release.py" - "/tools/test/**/*" - "/tools/runtime/**/*" diff --git a/src/sdks/kotlin/moon.yml b/src/sdks/kotlin/moon.yml index c268e5a7..4e320ce8 100644 --- a/src/sdks/kotlin/moon.yml +++ b/src/sdks/kotlin/moon.yml @@ -88,7 +88,7 @@ tasks: runFromWorkspaceRoot: true package-artifacts: tags: ["release", "artifact-package", "ci-kotlin-sdk-package"] - command: "bash tools/release/build-sdk-ci-artifacts.sh oliphaunt-kotlin" + command: "tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs oliphaunt-kotlin" deps: - "oliphaunt-kotlin:package" inputs: @@ -96,7 +96,8 @@ tasks: - "/src/runtimes/liboliphaunt/native/include/oliphaunt.h" - "/src/sdks/kotlin/**/*" - "/src/sdks/react-native/tools/android-smoke-artifacts.sh" - - "/tools/release/build-sdk-ci-artifacts.sh" + - "/tools/release/build-sdk-ci-artifacts.mjs" + - "/tools/release/check-staged-artifacts.mjs" - "/tools/runtime/**/*" outputs: - "/target/sdk-artifacts/oliphaunt-kotlin/**/*" diff --git a/src/sdks/react-native/moon.yml b/src/sdks/react-native/moon.yml index 94208cd6..49808861 100644 --- a/src/sdks/react-native/moon.yml +++ b/src/sdks/react-native/moon.yml @@ -497,7 +497,8 @@ tasks: - "!/src/sdks/react-native/**/build/**" - "!/src/sdks/react-native/**/lib/**" - "!/src/sdks/react-native/ios/vendor/**" - - "/tools/release/build-sdk-ci-artifacts.sh" + - "/tools/release/build-sdk-ci-artifacts.mjs" + - "/tools/release/check-staged-artifacts.mjs" - "/tools/release/release.py" outputs: - "/target/liboliphaunt-sdk-check/oliphaunt-react-native/package-shape/src/sdks/react-native/**/*" @@ -506,7 +507,7 @@ tasks: runFromWorkspaceRoot: true package-artifacts: tags: ["release", "artifact-package", "ci-react-native-sdk-package"] - command: "bash tools/release/build-sdk-ci-artifacts.sh oliphaunt-react-native" + command: "tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs oliphaunt-react-native" deps: - "oliphaunt-react-native:package" inputs: @@ -525,7 +526,8 @@ tasks: - "!/src/sdks/react-native/**/build/**" - "!/src/sdks/react-native/**/lib/**" - "!/src/sdks/react-native/ios/vendor/**" - - "/tools/release/build-sdk-ci-artifacts.sh" + - "/tools/release/build-sdk-ci-artifacts.mjs" + - "/tools/release/check-staged-artifacts.mjs" - "/tools/release/release.py" outputs: - "/target/sdk-artifacts/oliphaunt-react-native/**/*" diff --git a/src/sdks/rust/moon.yml b/src/sdks/rust/moon.yml index 9fbc95d3..306e93ff 100644 --- a/src/sdks/rust/moon.yml +++ b/src/sdks/rust/moon.yml @@ -103,7 +103,7 @@ tasks: runFromWorkspaceRoot: true package-artifacts: tags: ["release", "artifact-package", "ci-rust-sdk-package"] - command: "bash tools/release/build-sdk-ci-artifacts.sh oliphaunt-rust" + command: "tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs oliphaunt-rust" deps: - "oliphaunt-rust:package" env: @@ -114,7 +114,9 @@ tasks: - "/src/shared/fixtures/**/*" - "/rust-toolchain.toml" - "/src/sdks/rust/**/*" - - "/tools/release/build-sdk-ci-artifacts.sh" + - "/tools/release/build-sdk-ci-artifacts.mjs" + - "/tools/release/cargo-crate-filename.mjs" + - "/tools/release/check-staged-artifacts.mjs" - "/tools/runtime/**/*" outputs: - "/target/sdk-artifacts/oliphaunt-rust/**/*" diff --git a/src/sdks/swift/moon.yml b/src/sdks/swift/moon.yml index ca7c73c6..9cb10c9e 100644 --- a/src/sdks/swift/moon.yml +++ b/src/sdks/swift/moon.yml @@ -99,7 +99,7 @@ tasks: runFromWorkspaceRoot: true package-artifacts: tags: ["release", "artifact-package", "ci-swift-sdk-package"] - command: "bash tools/release/build-sdk-ci-artifacts.sh oliphaunt-swift" + command: "tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs oliphaunt-swift" deps: - "oliphaunt-swift:package" inputs: @@ -109,7 +109,9 @@ tasks: - "!/src/sdks/swift/.build" - "!/src/sdks/swift/.build/**" - "/src/runtimes/liboliphaunt/native/bin/build-ios-xcframework.sh" - - "/tools/release/build-sdk-ci-artifacts.sh" + - "/tools/release/build-sdk-ci-artifacts.mjs" + - "/tools/release/check-staged-artifacts.mjs" + - "/tools/release/render_swiftpm_release_package.mjs" - "/tools/runtime/**/*" outputs: - "/target/sdk-artifacts/oliphaunt-swift/**/*" diff --git a/tools/policy/assertions/assert-ci-workflows.mjs b/tools/policy/assertions/assert-ci-workflows.mjs index 567179c1..cfa9017e 100755 --- a/tools/policy/assertions/assert-ci-workflows.mjs +++ b/tools/policy/assertions/assert-ci-workflows.mjs @@ -325,8 +325,8 @@ assertSameItems( 'WASIX public Cargo packages must be exactly runtime, tools, ICU, runtime-AOT, and tools-AOT packages', ); requireText( - 'tools/release/build-sdk-ci-artifacts.sh', - 'package_oliphaunt_wasix_sdk_crate.mjs --output-dir "$artifact_root"', + 'tools/release/build-sdk-ci-artifacts.mjs', + '"tools/release/package_oliphaunt_wasix_sdk_crate.mjs", "--output-dir", artifactRoot', 'WASIX Rust package artifact builder must stage the registry-resolved WASIX SDK crate', ); requireText( diff --git a/tools/policy/check-moon-product-graph.mjs b/tools/policy/check-moon-product-graph.mjs index c07585af..57bcd8c4 100755 --- a/tools/policy/check-moon-product-graph.mjs +++ b/tools/policy/check-moon-product-graph.mjs @@ -83,7 +83,7 @@ function assertTaskCommand(tasks, projectId, taskId, expectedCommand) { throw new Error(`missing moon task ${projectId}:${taskId}`); } const actual = [task.command, ...(task.args ?? [])].join(' '); - if (expectedCommand.includes('.sh') && !expectedCommand.startsWith('bash ')) { + if (usesShellScriptPayload(expectedCommand) && !expectedCommand.startsWith('bash ')) { expectedCommand = `bash ${expectedCommand}`; } if (actual !== expectedCommand) { @@ -91,11 +91,15 @@ function assertTaskCommand(tasks, projectId, taskId, expectedCommand) { } } +function usesShellScriptPayload(command) { + return command.includes('.sh') && command !== 'tools/dev/bun.sh' && !command.startsWith('tools/dev/bun.sh '); +} + function assertShellTasksUseBash(tasks) { for (const [projectId, projectTasks] of Object.entries(tasks)) { for (const [taskId, task] of Object.entries(projectTasks ?? {})) { const command = [task.command, ...(task.args ?? [])].join(' '); - if (command.includes('.sh') && !command.startsWith('bash ')) { + if (usesShellScriptPayload(command) && !command.startsWith('bash ')) { throw new Error(`${projectId}:${taskId}: shell script commands must start with 'bash', got '${command}'`); } } @@ -724,17 +728,17 @@ assertTaskCommand(tasks, 'oliphaunt-swift', 'test', 'src/sdks/swift/tools/check- assertTaskCommand(tasks, 'oliphaunt-kotlin', 'check', 'src/sdks/kotlin/tools/check-sdk.sh check-static'); assertTaskCommand(tasks, 'oliphaunt-kotlin', 'test', 'src/sdks/kotlin/tools/check-sdk.sh test-unit'); assertTaskCommand(tasks, 'oliphaunt-rust', 'package', 'src/sdks/rust/tools/check-sdk.sh package-shape'); -assertTaskCommand(tasks, 'oliphaunt-rust', 'package-artifacts', 'tools/release/build-sdk-ci-artifacts.sh oliphaunt-rust'); +assertTaskCommand(tasks, 'oliphaunt-rust', 'package-artifacts', 'tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs oliphaunt-rust'); assertTaskCommand(tasks, 'oliphaunt-swift', 'package', 'src/sdks/swift/tools/check-sdk.sh package-shape'); -assertTaskCommand(tasks, 'oliphaunt-swift', 'package-artifacts', 'tools/release/build-sdk-ci-artifacts.sh oliphaunt-swift'); +assertTaskCommand(tasks, 'oliphaunt-swift', 'package-artifacts', 'tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs oliphaunt-swift'); assertTaskCommand(tasks, 'oliphaunt-kotlin', 'package', 'src/sdks/kotlin/tools/check-sdk.sh package-shape'); -assertTaskCommand(tasks, 'oliphaunt-kotlin', 'package-artifacts', 'tools/release/build-sdk-ci-artifacts.sh oliphaunt-kotlin'); +assertTaskCommand(tasks, 'oliphaunt-kotlin', 'package-artifacts', 'tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs oliphaunt-kotlin'); assertTaskCommand(tasks, 'oliphaunt-react-native', 'check', 'src/sdks/react-native/tools/check-sdk.sh check-static'); assertTaskCommand(tasks, 'oliphaunt-react-native', 'build-android-bridge', 'src/sdks/react-native/tools/check-sdk.sh build-android-bridge'); assertTaskCommand(tasks, 'oliphaunt-react-native', 'build-ios-bridge', 'src/sdks/react-native/tools/check-sdk.sh build-ios-bridge'); assertTaskCommand(tasks, 'oliphaunt-react-native', 'test', 'src/sdks/react-native/tools/check-sdk.sh test-unit'); assertTaskCommand(tasks, 'oliphaunt-react-native', 'package', 'src/sdks/react-native/tools/check-sdk.sh package-shape'); -assertTaskCommand(tasks, 'oliphaunt-react-native', 'package-artifacts', 'tools/release/build-sdk-ci-artifacts.sh oliphaunt-react-native'); +assertTaskCommand(tasks, 'oliphaunt-react-native', 'package-artifacts', 'tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs oliphaunt-react-native'); assertTaskCommand(tasks, 'oliphaunt-react-native', 'smoke-android', 'pnpm --dir src/sdks/react-native/examples/expo run smoke:android'); assertTaskCommand(tasks, 'oliphaunt-react-native', 'smoke-ios', 'pnpm --dir src/sdks/react-native/examples/expo run smoke:ios'); assertTaskCommand(tasks, 'oliphaunt-react-native', 'smoke-mobile', 'pnpm --dir src/sdks/react-native/examples/expo run smoke'); @@ -747,7 +751,7 @@ assertTaskCommand(tasks, 'oliphaunt-react-native', 'mobile-drill-ios', 'pnpm --d assertTaskCommand(tasks, 'oliphaunt-js', 'check', 'src/sdks/js/tools/check-sdk.sh check-static'); assertTaskCommand(tasks, 'oliphaunt-js', 'test', 'src/sdks/js/tools/check-sdk.sh test-unit'); assertTaskCommand(tasks, 'oliphaunt-js', 'package', 'src/sdks/js/tools/check-sdk.sh package-shape'); -assertTaskCommand(tasks, 'oliphaunt-js', 'package-artifacts', 'tools/release/build-sdk-ci-artifacts.sh oliphaunt-js'); +assertTaskCommand(tasks, 'oliphaunt-js', 'package-artifacts', 'tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs oliphaunt-js'); for (const projectId of [ 'oliphaunt-rust', 'oliphaunt-swift', @@ -1160,7 +1164,7 @@ for (const projectId of [ assertTaskCache(tasks, projectId, 'bench-run', false); } assertTaskCommand(tasks, 'oliphaunt-wasix-rust', 'package', 'src/bindings/wasix-rust/tools/check-package.sh'); -assertTaskCommand(tasks, 'oliphaunt-wasix-rust', 'package-artifacts', 'tools/release/build-sdk-ci-artifacts.sh oliphaunt-wasix-rust'); +assertTaskCommand(tasks, 'oliphaunt-wasix-rust', 'package-artifacts', 'tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs oliphaunt-wasix-rust'); 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'); diff --git a/tools/policy/check-sdk-parity.sh b/tools/policy/check-sdk-parity.sh index f449b973..746ce3cd 100755 --- a/tools/policy/check-sdk-parity.sh +++ b/tools/policy/check-sdk-parity.sh @@ -662,7 +662,7 @@ require_text src/sdks/swift/moon.yml 'command: "bash src/sdks/swift/tools/check- "Swift Moon smoke task must route through the SDK-owned runtime smoke" require_text src/sdks/swift/tools/check-sdk.sh "tools/runtime/preflight.sh ios-simulator" \ "Swift runtime smoke must include the shared PostgreSQL iOS simulator preflight" -require_text src/sdks/swift/moon.yml 'command: "bash tools/release/build-sdk-ci-artifacts.sh oliphaunt-swift"' \ +require_text src/sdks/swift/moon.yml 'command: "tools/dev/bun.sh tools/release/build-sdk-ci-artifacts.mjs oliphaunt-swift"' \ "Swift Moon package task must stage release-shaped SDK artifacts" require_text src/sdks/swift/tools/check-sdk.sh "build-ios-xcframework.sh --check-current" \ "Swift package shape must expose the iOS liboliphaunt artifact check" diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 0c881858..0cad17d8 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -441,17 +441,17 @@ fi if grep -Fq 'OLIPHAUNT_SKIP_TARGETS_COVERED_BY_PLANNED_JOBS' .github/workflows/ci.yml .github/scripts/select-affected-moon-targets.mjs; then fail "checks/tests jobs must be visible as their own affected Moon targets" fi -grep -Fq 'missing package-shape output' tools/release/build-sdk-ci-artifacts.sh || +grep -Fq 'missing package-shape output' tools/release/build-sdk-ci-artifacts.mjs || fail "SDK artifact builder must consume package-shape outputs produced by Moon task deps" -if grep -Fq 'OLIPHAUNT_SDK_CHECK_SCRATCH="$work_root/check"' tools/release/build-sdk-ci-artifacts.sh; then +if grep -Fq 'OLIPHAUNT_SDK_CHECK_SCRATCH="$work_root/check"' tools/release/build-sdk-ci-artifacts.mjs; 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 || +grep -Fq '"tools/release/cargo-crate-filename.mjs", manifest' tools/release/build-sdk-ci-artifacts.mjs || 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 +if grep -Fq 'python3 - "$manifest"' tools/release/build-sdk-ci-artifacts.mjs; 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 +if grep -Fq 'cargo_workspace_excludes_except()' tools/release/build-sdk-ci-artifacts.mjs; 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 || diff --git a/tools/release/build-sdk-ci-artifacts.mjs b/tools/release/build-sdk-ci-artifacts.mjs new file mode 100755 index 00000000..2c1f8fc8 --- /dev/null +++ b/tools/release/build-sdk-ci-artifacts.mjs @@ -0,0 +1,359 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; +import { + accessSync, + constants as fsConstants, + copyFileSync, + cpSync, + mkdirSync, + readdirSync, + readFileSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const PREFIX = "build-sdk-ci-artifacts.mjs"; +const BUN = process.execPath; +const SDK_PRODUCTS = [ + "oliphaunt-rust", + "oliphaunt-swift", + "oliphaunt-kotlin", + "oliphaunt-js", + "oliphaunt-react-native", + "oliphaunt-wasix-rust", +]; + +function fail(message) { + console.error(`${PREFIX}: ${message}`); + process.exit(1); +} + +function rel(file) { + const relative = path.relative(ROOT, String(file)); + if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) { + return String(file).split(path.sep).join("/"); + } + return relative.split(path.sep).join("/"); +} + +function compareText(left, right) { + return left < right ? -1 : left > right ? 1 : 0; +} + +function isFile(file) { + try { + return statSync(file).isFile(); + } catch { + return false; + } +} + +function isDirectory(file) { + try { + return statSync(file).isDirectory(); + } catch { + return false; + } +} + +function requireFile(file) { + if (!isFile(file)) { + fail(`missing package-shape output: ${rel(file)}`); + } +} + +function requireDir(file) { + if (!isDirectory(file)) { + fail(`missing package-shape output directory: ${rel(file)}`); + } +} + +function commandCandidates(command) { + if (command.includes("/") || command.includes("\\")) { + return [path.resolve(ROOT, command)]; + } + const pathEntries = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean); + const extensions = process.platform === "win32" + ? ["", ...(process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")] + : [""]; + return pathEntries.flatMap((entry) => extensions.map((extension) => path.join(entry, `${command}${extension}`))); +} + +function requireCommand(command) { + for (const candidate of commandCandidates(command)) { + try { + if (!statSync(candidate).isFile()) { + continue; + } + accessSync(candidate, process.platform === "win32" ? fsConstants.F_OK : fsConstants.X_OK); + return; + } catch { + // Keep scanning PATH. + } + } + fail(`missing required command: ${command}`); +} + +function copyDirContents(source, destination, { filter = () => true } = {}) { + mkdirSync(destination, { recursive: true }); + for (const entry of readdirSync(source, { withFileTypes: true }).sort((left, right) => compareText(left.name, right.name))) { + const sourcePath = path.join(source, entry.name); + const destinationPath = path.join(destination, entry.name); + cpSync(sourcePath, destinationPath, { + recursive: true, + filter, + }); + } +} + +function run(command, args, { cwd = ROOT, env = process.env, capture = false, label = command } = {}) { + const result = spawnSync(command, args, { + cwd, + env, + encoding: "utf8", + maxBuffer: 100 * 1024 * 1024, + stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit", + }); + if (result.error) { + fail(`${label} failed: ${result.error.message}`); + } + if (result.status !== 0) { + const stderr = capture && result.stderr ? result.stderr.trim() : ""; + fail(`${label} failed${stderr ? `: ${stderr}` : ""}`); + } + return capture ? result.stdout : ""; +} + +function cargoPackageDir() { + let targetDir = process.env.CARGO_TARGET_DIR ?? path.join(ROOT, "target"); + if (!path.isAbsolute(targetDir)) { + targetDir = path.join(ROOT, targetDir); + } + return path.join(targetDir, "package"); +} + +function rustCrateName(manifest) { + return run( + BUN, + ["tools/release/cargo-crate-filename.mjs", manifest], + { capture: true, label: "cargo crate filename" }, + ).trim(); +} + +function packageNpmWorkspace(packageDir, destination) { + requireCommand("pnpm"); + mkdirSync(destination, { recursive: true }); + const packJson = run( + "pnpm", + ["--dir", packageDir, "pack", "--pack-destination", destination, "--json"], + { capture: true, label: "pnpm pack" }, + ); + writeFileSync(path.join(destination, "pnpm-pack.json"), packJson); + let manifest; + try { + const parsed = JSON.parse(packJson); + manifest = Array.isArray(parsed) ? parsed[0] : parsed; + } catch (error) { + fail(`pnpm pack did not report valid JSON: ${error.message}`); + } + if (!manifest || typeof manifest !== "object" || typeof manifest.filename !== "string" || !manifest.filename.endsWith(".tgz")) { + fail("pnpm pack did not report a .tgz filename"); + } + const packFile = path.isAbsolute(manifest.filename) + ? manifest.filename + : path.join(destination, manifest.filename); + if (!isFile(packFile)) { + fail(`pnpm pack did not create ${rel(packFile)}`); + } +} + +function stageJsrSourceWorkspace(packageDir, destination) { + rmSync(destination, { recursive: true, force: true }); + mkdirSync(destination, { recursive: true }); + copyDirContents(packageDir, destination, { + filter: (source) => { + const relative = path.relative(packageDir, source); + if (!relative) { + return true; + } + const [topLevel] = relative.split(path.sep); + return !new Set(["node_modules", "lib", ".turbo"]).has(topLevel); + }, + }); + requireFile(path.join(destination, "jsr.json")); + requireFile(path.join(destination, "package.json")); + requireDir(path.join(destination, "src")); +} + +function kotlinVersion() { + const gradleProperties = readFileSync(path.join(ROOT, "src/sdks/kotlin/gradle.properties"), "utf8"); + const versions = gradleProperties + .split(/\r?\n/u) + .map((line) => line.match(/^VERSION_NAME=(.+)$/u)?.[1]?.trim()) + .filter(Boolean); + const version = versions.at(-1); + if (!version) { + fail("missing VERSION_NAME in src/sdks/kotlin/gradle.properties"); + } + return version; +} + +function stageRustSdkArtifacts(artifactRoot) { + requireCommand("cargo"); + const packageListing = path.join(ROOT, "target/liboliphaunt-sdk-check/rust-cargo-package-list.txt"); + requireFile(packageListing); + for (const packageName of ["oliphaunt", "oliphaunt-build"]) { + run("cargo", ["package", "-p", packageName, "--locked", "--allow-dirty", "--no-verify"], { + label: `cargo package ${packageName}`, + }); + const manifest = packageName === "oliphaunt" + ? path.join(ROOT, "src/sdks/rust/Cargo.toml") + : path.join(ROOT, "src/sdks/rust/crates/oliphaunt-build/Cargo.toml"); + const crateName = rustCrateName(manifest); + const packagedCrate = path.join(cargoPackageDir(), crateName); + requireFile(packagedCrate); + copyFileSync(packagedCrate, path.join(artifactRoot, crateName)); + } + copyFileSync(packageListing, path.join(artifactRoot, "cargo-package-files.txt")); +} + +function stageSwiftArtifacts(artifactRoot, workRoot) { + requireCommand("swift"); + const swiftSourceArchive = path.join( + ROOT, + "target/liboliphaunt-sdk-check/oliphaunt-swift/package-shape/swift-source-archive/Oliphaunt-source.zip", + ); + requireFile(swiftSourceArchive); + copyFileSync(swiftSourceArchive, path.join(artifactRoot, "Oliphaunt-source.zip")); + const assetDir = process.env.OLIPHAUNT_SWIFT_RELEASE_ASSET_DIR; + if (!assetDir) { + fail("oliphaunt-swift package artifacts require OLIPHAUNT_SWIFT_RELEASE_ASSET_DIR"); + } + run(BUN, [ + "tools/release/render_swiftpm_release_package.mjs", + "--asset-dir", + assetDir, + "--output", + path.join(artifactRoot, "Package.swift.release"), + "--generated-tree", + path.join(workRoot, "swiftpm-release-tree"), + ], { label: "render SwiftPM release package" }); + const releaseTree = path.join(artifactRoot, "release-tree"); + rmSync(releaseTree, { recursive: true, force: true }); + copyDirContents(path.join(workRoot, "swiftpm-release-tree"), releaseTree); + const manifest = readFileSync(path.join(artifactRoot, "Package.swift.release"), "utf8"); + if (!manifest.includes("liboliphaunt-native-v")) { + fail("staged SwiftPM release manifest must use the public liboliphaunt GitHub release URL"); + } + if (manifest.includes("file://")) { + fail("staged SwiftPM release manifest must not contain local file URLs"); + } +} + +function stageKotlinArtifacts(artifactRoot, workRoot) { + const mavenRepo = path.join(workRoot, "maven-local"); + const buildRoot = path.join(workRoot, "gradle-build"); + const cxxRoot = path.join(workRoot, "cxx-build"); + const cacheRoot = path.join(workRoot, "gradle-cache"); + const version = kotlinVersion(); + run(path.join(ROOT, "src/sdks/kotlin/gradlew"), [ + "-p", + path.join(ROOT, "src/sdks/kotlin"), + ":oliphaunt:publishAndroidReleasePublicationToMavenLocal", + ":oliphaunt-android-gradle-plugin:publishToMavenLocal", + `-Dmaven.repo.local=${mavenRepo}`, + "-PoliphauntAndroidAbiFilters=arm64-v8a,x86_64", + `-PoliphauntBuildRoot=${buildRoot}`, + `-PoliphauntCxxBuildRoot=${cxxRoot}`, + "--project-cache-dir", + cacheRoot, + "--no-configuration-cache", + ], { label: "Kotlin SDK Gradle package artifacts" }); + requireFile(path.join(mavenRepo, `dev/oliphaunt/oliphaunt-android/${version}/oliphaunt-android-${version}.aar`)); + requireFile(path.join(mavenRepo, `dev/oliphaunt/oliphaunt-android-gradle-plugin/${version}/oliphaunt-android-gradle-plugin-${version}.jar`)); + const destination = path.join(artifactRoot, "maven"); + copyDirContents(mavenRepo, destination); +} + +function stageJsArtifacts(artifactRoot) { + const packageShapeDir = path.join(ROOT, "target/liboliphaunt-sdk-check/oliphaunt-js/package-shape/src/sdks/js"); + requireDir(packageShapeDir); + packageNpmWorkspace(packageShapeDir, artifactRoot); + stageJsrSourceWorkspace(packageShapeDir, path.join(artifactRoot, "jsr-source")); +} + +function stageReactNativeArtifacts(artifactRoot) { + const packageShapeDir = path.join(ROOT, "target/liboliphaunt-sdk-check/oliphaunt-react-native/package-shape/src/sdks/react-native"); + requireDir(packageShapeDir); + packageNpmWorkspace(packageShapeDir, artifactRoot); +} + +function stageWasixRustArtifacts(artifactRoot) { + requireCommand("cargo"); + const packageListing = path.join(ROOT, "target/oliphaunt-wasix-rust/package/oliphaunt-wasix.package-files.txt"); + requireFile(packageListing); + run(BUN, ["tools/release/package_oliphaunt_wasix_sdk_crate.mjs", "--output-dir", artifactRoot], { + label: "package oliphaunt-wasix SDK crate", + }); + copyFileSync(packageListing, path.join(artifactRoot, "cargo-package-files.txt")); +} + +function writeArtifactIndex(artifactRoot) { + const entries = readdirSync(artifactRoot, { withFileTypes: true }) + .filter((entry) => entry.isFile() || entry.isDirectory()) + .map((entry) => path.join(artifactRoot, entry.name)) + .sort(compareText); + if (entries.length === 0) { + fail("no SDK artifacts were staged"); + } + const index = path.join(artifactRoot, "artifacts.txt"); + const lines = [...entries, index].sort(compareText).map((entry) => rel(entry)); + writeFileSync(index, `${lines.join("\n")}\n`); +} + +function main() { + const product = Bun.argv[2] ?? ""; + if (product === "--help" || product === "-h") { + console.log(`usage: tools/release/build-sdk-ci-artifacts.mjs <${SDK_PRODUCTS.join("|")}>`); + process.exit(0); + } + if (!product) { + fail(`usage: tools/release/build-sdk-ci-artifacts.mjs <${SDK_PRODUCTS.join("|")}>`); + } + if (!SDK_PRODUCTS.includes(product)) { + fail(`unsupported SDK product: ${product}`); + } + + const artifactRoot = path.join(ROOT, "target/sdk-artifacts", product); + const workRoot = path.join(ROOT, "target/sdk-artifacts-work", product); + rmSync(artifactRoot, { recursive: true, force: true }); + rmSync(workRoot, { recursive: true, force: true }); + mkdirSync(artifactRoot, { recursive: true }); + mkdirSync(workRoot, { recursive: true }); + + if (product === "oliphaunt-rust") { + stageRustSdkArtifacts(artifactRoot); + } else if (product === "oliphaunt-swift") { + stageSwiftArtifacts(artifactRoot, workRoot); + } else if (product === "oliphaunt-kotlin") { + stageKotlinArtifacts(artifactRoot, workRoot); + } else if (product === "oliphaunt-js") { + stageJsArtifacts(artifactRoot); + } else if (product === "oliphaunt-react-native") { + stageReactNativeArtifacts(artifactRoot); + } else if (product === "oliphaunt-wasix-rust") { + stageWasixRustArtifacts(artifactRoot); + } + + writeArtifactIndex(artifactRoot); + run(BUN, ["tools/release/check-staged-artifacts.mjs", "--require-sdk-product", product], { + label: "check staged SDK artifacts", + }); + console.log(`Staged ${product} SDK artifacts under ${rel(artifactRoot)}`); +} + +main(); diff --git a/tools/release/build-sdk-ci-artifacts.sh b/tools/release/build-sdk-ci-artifacts.sh deleted file mode 100755 index 91c1b54d..00000000 --- a/tools/release/build-sdk-ci-artifacts.sh +++ /dev/null @@ -1,194 +0,0 @@ -#!/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 "build-sdk-ci-artifacts.sh: $*" >&2 - exit 1 -} - -require() { - command -v "$1" >/dev/null 2>&1 || fail "missing required command: $1" -} - -require_file() { - local path="$1" - [ -f "$path" ] || fail "missing package-shape output: $path" -} - -require_dir() { - local path="$1" - [ -d "$path" ] || fail "missing package-shape output directory: $path" -} - -rust_crate_name() { - local manifest="$1" - bun tools/release/cargo-crate-filename.mjs "$manifest" -} - -cargo_package_dir() { - local target_dir="${CARGO_TARGET_DIR:-$root/target}" - if [[ "$target_dir" != /* ]]; then - target_dir="$root/$target_dir" - fi - printf '%s/package\n' "$target_dir" -} - -package_npm_workspace() { - local package_dir="$1" - local destination="$2" - require pnpm - mkdir -p "$destination" - local pack_json pack_file - pack_json="$(pnpm --dir "$package_dir" pack --pack-destination "$destination" --json)" - printf '%s\n' "$pack_json" >"$destination/pnpm-pack.json" - pack_file="$( - PACK_JSON="$pack_json" PACK_DIR="$destination" node -e " -const manifest = JSON.parse(process.env.PACK_JSON || '{}'); -if (!manifest.filename || !manifest.filename.endsWith('.tgz')) { - throw new Error('pnpm pack did not report a .tgz filename'); -} -const path = require('node:path'); -console.log(path.isAbsolute(manifest.filename) ? manifest.filename : path.join(process.env.PACK_DIR || '', manifest.filename)); -" - )" - [ -f "$pack_file" ] || fail "pnpm pack did not create $pack_file" -} - -stage_jsr_source_workspace() { - local package_dir="$1" - local destination="$2" - rm -rf "$destination" - mkdir -p "$destination" - ( - cd "$package_dir" - tar \ - --exclude='./node_modules' \ - --exclude='./node_modules/*' \ - --exclude='./lib' \ - --exclude='./lib/*' \ - --exclude='./.turbo' \ - --exclude='./.turbo/*' \ - -cf - . - ) | ( - cd "$destination" - tar -xf - - ) - [ -f "$destination/jsr.json" ] || fail "JSR source workspace is missing jsr.json" - [ -f "$destination/package.json" ] || fail "JSR source workspace is missing package.json" - [ -d "$destination/src" ] || fail "JSR source workspace is missing src/" -} - -product="${1:-}" -[ -n "$product" ] || fail "usage: tools/release/build-sdk-ci-artifacts.sh " - -artifact_root="$root/target/sdk-artifacts/$product" -work_root="$root/target/sdk-artifacts-work/$product" -rm -rf "$artifact_root" "$work_root" -mkdir -p "$artifact_root" "$work_root" - -case "$product" in - oliphaunt-rust) - require cargo - 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 - cargo package -p "$package" --locked --allow-dirty --no-verify - case "$package" in - oliphaunt) - manifest="$root/src/sdks/rust/Cargo.toml" - ;; - oliphaunt-build) - manifest="$root/src/sdks/rust/crates/oliphaunt-build/Cargo.toml" - ;; - *) - fail "unsupported Rust SDK package: $package" - ;; - esac - crate_name="$(rust_crate_name "$manifest")" - package_dir="$(cargo_package_dir)" - [ -f "$package_dir/$crate_name" ] || fail "cargo package did not create $package_dir/$crate_name" - cp "$package_dir/$crate_name" "$artifact_root/$crate_name" - done - cp "$package_listing" "$artifact_root/cargo-package-files.txt" - ;; - 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" - 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" - rm -rf "$artifact_root/release-tree" - mkdir -p "$artifact_root/release-tree" - cp -R "$work_root/swiftpm-release-tree/." "$artifact_root/release-tree/" - grep -Fq "liboliphaunt-native-v" "$artifact_root/Package.swift.release" || - fail "staged SwiftPM release manifest must use the public liboliphaunt GitHub release URL" - if grep -Fq "file://" "$artifact_root/Package.swift.release"; then - fail "staged SwiftPM release manifest must not contain local file URLs" - fi - ;; - oliphaunt-kotlin) - kotlin_maven_repo="$work_root/maven-local" - kotlin_build_root="$work_root/gradle-build" - kotlin_cxx_root="$work_root/cxx-build" - kotlin_cache_root="$work_root/gradle-cache" - kotlin_version="$(sed -n 's/^VERSION_NAME=//p' "$root/src/sdks/kotlin/gradle.properties" | tail -n 1)" - [ -n "$kotlin_version" ] || fail "missing VERSION_NAME in src/sdks/kotlin/gradle.properties" - "$root/src/sdks/kotlin/gradlew" -p "$root/src/sdks/kotlin" \ - :oliphaunt:publishAndroidReleasePublicationToMavenLocal \ - :oliphaunt-android-gradle-plugin:publishToMavenLocal \ - "-Dmaven.repo.local=$kotlin_maven_repo" \ - "-PoliphauntAndroidAbiFilters=arm64-v8a,x86_64" \ - "-PoliphauntBuildRoot=$kotlin_build_root" \ - "-PoliphauntCxxBuildRoot=$kotlin_cxx_root" \ - --project-cache-dir "$kotlin_cache_root" \ - --no-configuration-cache - [ -f "$kotlin_maven_repo/dev/oliphaunt/oliphaunt-android/$kotlin_version/oliphaunt-android-$kotlin_version.aar" ] || - fail "Kotlin SDK Maven artifact did not publish oliphaunt-android" - [ -f "$kotlin_maven_repo/dev/oliphaunt/oliphaunt-android-gradle-plugin/$kotlin_version/oliphaunt-android-gradle-plugin-$kotlin_version.jar" ] || - fail "Kotlin SDK Maven artifact did not publish the Android Gradle plugin" - mkdir -p "$artifact_root/maven" - cp -R "$kotlin_maven_repo/." "$artifact_root/maven/" - ;; - oliphaunt-js) - require node - package_shape_dir="$root/target/liboliphaunt-sdk-check/oliphaunt-js/package-shape/src/sdks/js" - require_dir "$package_shape_dir" - package_npm_workspace "$package_shape_dir" "$artifact_root" - stage_jsr_source_workspace "$package_shape_dir" "$artifact_root/jsr-source" - ;; - oliphaunt-react-native) - require node - package_shape_dir="$root/target/liboliphaunt-sdk-check/oliphaunt-react-native/package-shape/src/sdks/react-native" - require_dir "$package_shape_dir" - package_npm_workspace "$package_shape_dir" "$artifact_root" - ;; - oliphaunt-wasix-rust) - require cargo - require bun - 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" - cp "$package_listing" "$artifact_root/cargo-package-files.txt" - ;; - *) - fail "unsupported SDK product: $product" - ;; -esac - -find "$artifact_root" -mindepth 1 -maxdepth 1 \( -type f -o -type d \) -print | sort >"$artifact_root/artifacts.txt" -[ -s "$artifact_root/artifacts.txt" ] || fail "no SDK artifacts were staged for $product" -tools/dev/bun.sh tools/release/check-staged-artifacts.mjs --require-sdk-product "$product" -printf 'Staged %s SDK artifacts under %s\n' "$product" "$artifact_root" diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py index 89fe3bca..c66b2326 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -655,7 +655,7 @@ def validate_ci_release_artifacts() -> None: ) require_text( moon_file, - f"tools/release/build-sdk-ci-artifacts.sh {project_id}", + f"tools/release/build-sdk-ci-artifacts.mjs {project_id}", f"{project_id} package task must stage publishable SDK artifacts", ) require_text( @@ -752,27 +752,27 @@ def validate_ci_release_artifacts() -> None: "iOS mobile runner must consume the staged Swift source artifact when CI requires SDK artifacts", ) require_text( - "tools/release/build-sdk-ci-artifacts.sh", + "tools/release/build-sdk-ci-artifacts.mjs", "publishAndroidReleasePublicationToMavenLocal", "Kotlin SDK package builder must stage a Maven repository layout for Android consumers", ) require_text( - "tools/release/build-sdk-ci-artifacts.sh", - 'mkdir -p "$artifact_root/maven"', + "tools/release/build-sdk-ci-artifacts.mjs", + 'path.join(artifactRoot, "maven")', "Kotlin SDK package builder must stage Maven artifacts under target/sdk-artifacts/oliphaunt-kotlin/maven", ) require_text( - "tools/release/build-sdk-ci-artifacts.sh", - 'check-staged-artifacts.mjs --require-sdk-product "$product"', + "tools/release/build-sdk-ci-artifacts.mjs", + '"tools/release/check-staged-artifacts.mjs", "--require-sdk-product", product', "SDK package builders must validate staged package artifacts for runtime/extension payload leaks", ) reject_text( - "tools/release/build-sdk-ci-artifacts.sh", + "tools/release/build-sdk-ci-artifacts.mjs", "outputs/aar/*-release.aar", "Kotlin SDK package staging must not copy loose AARs; the staged Maven repository is the package boundary", ) require_text( - "tools/release/build-sdk-ci-artifacts.sh", + "tools/release/build-sdk-ci-artifacts.mjs", "oliphaunt-android-gradle-plugin:publishToMavenLocal", "Kotlin SDK package builder must stage the Android Gradle plugin Maven artifact", ) @@ -1135,8 +1135,8 @@ def validate_ci_release_artifacts() -> None: "release workflow must not stage native helper artifacts in a generic release-assets/native bucket", ) require_text( - "tools/release/build-sdk-ci-artifacts.sh", - 'stage_jsr_source_workspace "$package_shape_dir" "$artifact_root/jsr-source"', + "tools/release/build-sdk-ci-artifacts.mjs", + 'stageJsrSourceWorkspace(packageShapeDir, path.join(artifactRoot, "jsr-source"))', "TypeScript SDK builder must stage source for JSR publishing in addition to the npm tarball", ) require_text( diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 41432706..4df71ac6 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1114,22 +1114,22 @@ def validate_swift(swift_version: str, liboliphaunt_version: str) -> None: "Swift SDK package check must fail closed instead of fabricating local release assets", ) require_text( - "tools/release/build-sdk-ci-artifacts.sh", + "tools/release/build-sdk-ci-artifacts.mjs", "render_swiftpm_release_package.mjs", "Swift SDK package artifact builder must render the staged public SwiftPM release manifest", ) require_text( - "tools/release/build-sdk-ci-artifacts.sh", - '"$artifact_root/Package.swift.release"', + "tools/release/build-sdk-ci-artifacts.mjs", + 'path.join(artifactRoot, "Package.swift.release")', "Swift SDK package artifact builder must stage Package.swift.release as a release artifact", ) require_text( - "tools/release/build-sdk-ci-artifacts.sh", + "tools/release/build-sdk-ci-artifacts.mjs", "staged SwiftPM release manifest must not contain local file URLs", "Swift SDK package artifact builder must reject local file URLs in release artifacts", ) reject_text( - "tools/release/build-sdk-ci-artifacts.sh", + "tools/release/build-sdk-ci-artifacts.mjs", 'cp "$work_root/check/package-shape/Package.swift.release"', "Swift SDK package artifact builder must not stage the local validation manifest", ) From 10138bfae374011481a34806619629d51b62ce21 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 21:59:52 +0000 Subject: [PATCH 229/308] chore: port wasix cargo packager to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 43 +- examples/README.md | 2 +- .../fixtures/consumer-shape/products.json | 8 +- tools/policy/python-entrypoints.allowlist | 1 - tools/release/check_artifact_targets.py | 16 +- tools/release/check_consumer_shape.py | 21 +- tools/release/check_release_metadata.py | 19 +- tools/release/local_registry_publish.py | 4 +- ...age_liboliphaunt_wasix_cargo_artifacts.mjs | 1303 ++++++++++++++ ...kage_liboliphaunt_wasix_cargo_artifacts.py | 1577 ----------------- tools/release/release.py | 4 +- 11 files changed, 1369 insertions(+), 1629 deletions(-) create mode 100755 tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs delete mode 100644 tools/release/package_liboliphaunt_wasix_cargo_artifacts.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 5e69e9a9..32aafe44 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,27 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Ported the WASIX Cargo artifact packager from + `tools/release/package_liboliphaunt_wasix_cargo_artifacts.py` to the Bun + entrypoint `tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs`. + The generated package graph keeps root runtime crates to core runtime assets, + publishes `pg_dump` and `psql` through `oliphaunt-wasix-tools` and + `oliphaunt-wasix-tools-aot-*`, and continues to split only oversized internal + extension AOT payloads. Fresh smoke packaging from local release assets + produced 210 WASIX crate files with 0 crates over 10 MiB; the root + `liboliphaunt-wasix-portable` crate was 9,076,774 bytes, the + `oliphaunt-wasix-tools` crate was 1,206,842 bytes, and the largest crate was + the PostGIS WASIX AOT part crate at 10,212,312 bytes. A native linux-x64 + package smoke produced separate runtime part crates and an + `oliphaunt-tools-linux-x64-gnu` part crate, with 0 crates over 10 MiB. Direct + payload inspection showed native root packages contain `initdb`, `pg_ctl`, + and `postgres`, native tools contain `pg_dump` and `psql`, WASIX root contains + `initdb.wasix.wasm` with no split tool manifest entries, and WASIX tools + contain `pg_dump.wasix.wasm` and `psql.wasix.wasm`. Fresh checks passed: + `node --check` and `--help` for both Cargo packagers, Python `py_compile` for + touched release validators, `check_artifact_targets.py`, + `check_release_metadata.py`, and focused `check_consumer_shape.py` for + `liboliphaunt-wasix` and `liboliphaunt-native`. - 2026-06-27: Ported the shared SDK package artifact builder from `tools/release/build-sdk-ci-artifacts.sh` to the Bun entrypoint `tools/release/build-sdk-ci-artifacts.mjs`. Moon package-artifact tasks for @@ -2161,25 +2182,19 @@ until the current-state gates here are checked with fresh local evidence. - 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`. - The current inventory contains 9 tracked Python files: release orchestration, - release/package validators, the product metadata adapter, the WASIX Cargo - artifact packager, local registry publishing, release policy checks, and the - extension model generator. New Python files must either be intentionally - allowlisted or ported to Bun. The current migration order is: - 1. split `product_metadata.py` consumers onto already-existing Bun graph - helpers until the compatibility module has no direct callers; - 2. port release checkers in the release-graph cluster + The current inventory contains 7 tracked Python files: release orchestration, + release/package validators, local registry publishing, release policy checks, + and the extension model generator. New Python files must either be + intentionally allowlisted or ported to Bun. The current migration order is: + 1. port release checkers in the release-graph cluster (`check-release-policy.py`, `check_artifact_targets.py`, `check_release_metadata.py`, `check_consumer_shape.py`) behind parity smokes and then remove their Python compatibility imports; - 3. port `package_liboliphaunt_wasix_cargo_artifacts.py` after release graph - metadata is Bun-native, because it depends on exact package metadata and - crates.io size-limit enforcement; - 4. port `local_registry_publish.py` after artifact package generation and + 2. port `local_registry_publish.py` after artifact package generation and release metadata are Bun-native, preserving the local registry e2e path; - 5. port `release.py` last, when the underlying validators and registry helpers + 3. port `release.py` last, when the underlying validators and registry helpers have Bun entrypoints; - 6. port `src/extensions/tools/check-extension-model.py` as a separate + 4. port `src/extensions/tools/check-extension-model.py` as a separate generator migration, because it is the canonical multi-language extension model and needs generated-output parity across SDKs. - While those Python entrypoints remain, policy tooling now keeps Python compile diff --git a/examples/README.md b/examples/README.md index 36dda8b2..2a054313 100644 --- a/examples/README.md +++ b/examples/README.md @@ -29,7 +29,7 @@ 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 -python3 tools/release/package_liboliphaunt_wasix_cargo_artifacts.py \ +tools/dev/bun.sh tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs \ --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 diff --git a/src/shared/fixtures/consumer-shape/products.json b/src/shared/fixtures/consumer-shape/products.json index 5d41e667..188e5f44 100644 --- a/src/shared/fixtures/consumer-shape/products.json +++ b/src/shared/fixtures/consumer-shape/products.json @@ -43,7 +43,7 @@ "src/runtimes/liboliphaunt/wasix/crates/assets/Cargo.toml", "src/runtimes/liboliphaunt/wasix/crates/assets/README.md", "tools/release/wasix-cargo-artifact-contract.mjs", - "tools/release/package_liboliphaunt_wasix_cargo_artifacts.py" + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs" ], "requiredText": { "src/runtimes/liboliphaunt/wasix/release.toml": [ @@ -57,10 +57,10 @@ "name = \"liboliphaunt-wasix-portable\"", "links = \"oliphaunt_artifact_liboliphaunt_wasix_runtime\"" ], - "tools/release/package_liboliphaunt_wasix_cargo_artifacts.py": [ + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs": [ "CRATES_IO_MAX_BYTES", - "validate_crate_size", - "release_graph_json(\"wasix-cargo-artifact-contract\")" + "validateCrateSize", + "wasix-cargo-artifact-contract.mjs" ], "tools/release/wasix-cargo-artifact-contract.mjs": [ "oliphaunt-liboliphaunt-wasix-cargo-artifacts-v2" diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index 951789b9..903d7171 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -7,5 +7,4 @@ tools/release/check_artifact_targets.py release-metadata defer-release-graph-por tools/release/check_consumer_shape.py release-consumer-shape defer-release-graph-port validates cross-SDK package/runtime/install shape from generated release fixtures and source invariants tools/release/check_release_metadata.py release-metadata defer-release-graph-port validates release metadata and publish-step wiring through cached Bun release graph query rows tools/release/local_registry_publish.py local-registry defer-local-registry-port publishes local Cargo, npm, Maven, and Swift registries from current release artifacts for e2e example validation -tools/release/package_liboliphaunt_wasix_cargo_artifacts.py wasix-cargo-artifacts defer-wasix-packager-port generates split WASIX runtime, tools, ICU, and extension Cargo artifact crates with size-limit enforcement tools/release/release.py release-orchestrator defer-release-graph-port owns protected release planning, validation, registry checks, publish dry-runs, and publish dispatch diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py index c66b2326..eb40ddfd 100644 --- a/tools/release/check_artifact_targets.py +++ b/tools/release/check_artifact_targets.py @@ -1071,7 +1071,7 @@ def validate_ci_release_artifacts() -> None: ) require_text( "tools/release/release.py", - "package_liboliphaunt_wasix_cargo_artifacts.py", + "package_liboliphaunt_wasix_cargo_artifacts.mjs", "liboliphaunt-wasix Cargo artifact packages must be generated from staged WASIX release assets", ) require_text( @@ -1080,27 +1080,27 @@ def validate_ci_release_artifacts() -> None: "release CLI must package and validate direct WASIX Cargo artifact crates", ) require_text( - "tools/release/package_liboliphaunt_wasix_cargo_artifacts.py", + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", "CRATES_IO_MAX_BYTES", "WASIX Cargo artifact packager must enforce the crates.io package size limit", ) require_text( - "tools/release/package_liboliphaunt_wasix_cargo_artifacts.py", - "validate_crate_size", + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", + "validateCrateSize", "WASIX Cargo artifact packager must validate direct artifact crate sizes", ) reject_text( - "tools/release/package_liboliphaunt_wasix_cargo_artifacts.py", + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", "DEFAULT_PART_COUNT", "WASIX Cargo artifact packager must not generate reserved part crates", ) require_text( - "tools/release/package_liboliphaunt_wasix_cargo_artifacts.py", - "wasix_extension_aot_part_package_name", + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", + "wasixExtensionAotPartPackageName", "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", + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", "EXTENSION_AOT_SPLIT_THRESHOLD_BYTES", "WASIX Cargo artifact packager must keep extension AOT part splitting behind an explicit size threshold", ) diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 9b27dec6..eec03952 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -2332,14 +2332,14 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: severity="P0", ) release_source = read_text("tools/release/release.py") - wasix_packager_source = read_text("tools/release/package_liboliphaunt_wasix_cargo_artifacts.py") + wasix_packager_source = read_text("tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs") wasix_dependency_invariant_source = read_text("tools/policy/check-wasix-release-dependency-invariants.mjs") workflow_source = read_text(".github/workflows/release.yml") require( findings, product, "wasix-cargo-artifact-release-flow", - "package_liboliphaunt_wasix_cargo_artifacts.py" in release_source + "package_liboliphaunt_wasix_cargo_artifacts.mjs" in release_source and "liboliphaunt_wasix_cargo_artifact_crates" in release_source and "--product liboliphaunt-wasix --step crates-io" in workflow_source, "Release flow must generate and publish WASIX Cargo artifact crates from staged WASIX release assets.", @@ -2366,13 +2366,14 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: and "FORBIDDEN_RUNTIME_ARCHIVE_TOOL_FILES" in wasix_packager_source and ("import " + "product_metadata") not in wasix_packager_source and "product_metadata." not in wasix_packager_source - and 'release_graph_json("wasix-cargo-artifact-contract")' in wasix_packager_source - and 'release_graph_rows("wasix-extension-package-names")' in wasix_packager_source - and 'release_graph_rows("product-versions", ("--product", product))' in wasix_packager_source, + and 'from "./wasix-cargo-artifact-contract.mjs"' in wasix_packager_source + and "wasixExtensionPackageName" in wasix_packager_source + and "wasixExtensionAotPackageName" in wasix_packager_source + and "currentProductVersionSync(PRODUCT" 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", - "tools/release/package_liboliphaunt_wasix_cargo_artifacts.py", + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", ], severity="P0", ) @@ -2418,13 +2419,13 @@ def check_liboliphaunt_wasix(findings: list[Finding]) -> None: product, "wasix-direct-cargo-artifact-packaging", "CRATES_IO_MAX_BYTES" in wasix_packager_source - and "validate_crate_size" in wasix_packager_source + and "validateCrateSize" in wasix_packager_source and "DEFAULT_PART_COUNT" not in wasix_packager_source - and "wasix_extension_aot_part_package_name" in wasix_packager_source + and "wasixExtensionAotPartPackageName" in wasix_packager_source and "EXTENSION_AOT_SPLIT_THRESHOLD_BYTES" in wasix_packager_source - and '"role": "artifact"' in wasix_packager_source, + and 'role: "artifact"' in wasix_packager_source, "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", + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", severity="P0", ) version = read_current_version(product) diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 4df71ac6..8a9261eb 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -926,7 +926,7 @@ def validate_local_registry_publisher() -> None: "def stage_release_asset_cargo_packages" not in publisher or "package-liboliphaunt-cargo-artifacts.mjs" not in publisher or "package_broker_cargo_artifacts.mjs" not in publisher - or "package_liboliphaunt_wasix_cargo_artifacts.py" not in publisher + or "package_liboliphaunt_wasix_cargo_artifacts.mjs" not in publisher or "host_cargo_release_target()" not in publisher or "stage_release_asset_cargo_packages(roots, registry_root, dry_run, result, strict)" not in publisher or "strict=strict" not in publisher @@ -2022,7 +2022,7 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None 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") + wasix_packager_source = read_text("tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs") if ( wasix_core_runtime_archive_files() != ("oliphaunt/bin/initdb", "oliphaunt/bin/postgres") @@ -2032,16 +2032,15 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None != ("oliphaunt/bin/pg_ctl", "oliphaunt/bin/pg_dump", "oliphaunt/bin/psql") or wasix_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 + or "splitRuntimeToolsPayload" not in wasix_packager_source + or "splitAotToolsPayload" not in wasix_packager_source or "import product_metadata" in wasix_packager_source or "product_metadata." in wasix_packager_source - or 'release_graph_json("wasix-cargo-artifact-contract")' not in wasix_packager_source - or 'release_graph_rows("wasix-extension-package-names")' not in wasix_packager_source - or 'release_graph_rows("product-versions", ("--product", product))' not in wasix_packager_source - or "def wasix_extension_package_name(product" not in wasix_packager_source - or "def wasix_extension_aot_package_name(product" not in wasix_packager_source - or "text = re.sub(r'(?m)^publish = false\\n?', \"\", text)" not in wasix_packager_source + or 'from "./wasix-cargo-artifact-contract.mjs"' not in wasix_packager_source + or "wasixExtensionPackageName" not in wasix_packager_source + or "wasixExtensionAotPackageName" not in wasix_packager_source + or "currentProductVersionSync(PRODUCT" not in wasix_packager_source + or 'text.replace(/^publish = false\\n?/gmu, "")' not in wasix_packager_source ): fail("WASIX Cargo artifact packager must read the Bun WASIX artifact contract, split pg_dump/psql into publishable tools crates, and keep only postgres/initdb in root runtime crates") wasix_dependency_invariant_source = read_text("tools/policy/check-wasix-release-dependency-invariants.mjs") diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index 032fcdc4..6916e297 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -2785,8 +2785,8 @@ def stage_release_asset_cargo_packages( ) run( [ - "python3", - "tools/release/package_liboliphaunt_wasix_cargo_artifacts.py", + "tools/dev/bun.sh", + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", "--version", wasix_version, "--output-dir", diff --git a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs new file mode 100755 index 00000000..af483903 --- /dev/null +++ b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs @@ -0,0 +1,1303 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { + copyFileSync, + cpSync, + existsSync, + mkdirSync, + readFileSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { compareText } from "./release-graph.mjs"; +import { currentProductVersionSync } from "./release-artifact-targets.mjs"; +import { + AOT_PACKAGES, + AOT_TARGET_CFGS, + AOT_TARGET_TRIPLES, + CORE_RUNTIME_ARCHIVE_FILES, + FORBIDDEN_RUNTIME_ARCHIVE_TOOL_FILES, + ICU_PACKAGE, + ICU_PAYLOAD_ARCHIVE, + RUNTIME_PACKAGE, + TOOLS_AOT_ARTIFACTS, + TOOLS_AOT_PACKAGES, + TOOLS_PACKAGE, + TOOLS_PAYLOAD_FILES, + WASIX_CARGO_ARTIFACT_SCHEMA, + expectedExtensionAotTargets, + wasixExtensionAotPackageName, + wasixExtensionPackageName, +} from "./wasix-cargo-artifact-contract.mjs"; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const PRODUCT = "liboliphaunt-wasix"; +const PREFIX = "package_liboliphaunt_wasix_cargo_artifacts.mjs"; +const CRATES_IO_MAX_BYTES = 10 * 1024 * 1024; +const EXTENSION_AOT_SPLIT_THRESHOLD_BYTES = 9 * 1024 * 1024; +const EXPECTED_EXTENSION_AOT_TARGETS = new Set(expectedExtensionAotTargets()); + +function fail(message) { + console.error(`${PREFIX}: ${message}`); + process.exit(1); +} + +function rel(file) { + const relative = path.relative(ROOT, String(file)); + if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) { + return String(file).split(path.sep).join("/"); + } + return relative.split(path.sep).join("/"); +} + +function isFile(file) { + try { + return statSync(file).isFile(); + } catch { + return false; + } +} + +function isDirectory(file) { + try { + return statSync(file).isDirectory(); + } catch { + return false; + } +} + +function run(args, { cwd = ROOT, env = process.env, capture = false, label = args.join(" ") } = {}) { + if (!capture) { + console.log(`\n==> ${args.join(" ")}`); + } + const result = spawnSync(args[0], args.slice(1), { + cwd, + env, + encoding: capture ? "utf8" : undefined, + maxBuffer: 200 * 1024 * 1024, + stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit", + }); + if (result.error) { + fail(`${label} failed: ${result.error.message}`); + } + if (result.status !== 0) { + const stderr = capture && result.stderr ? result.stderr.trim() : ""; + fail(`${label} failed${stderr ? `: ${stderr}` : ""}`); + } + return capture ? result.stdout : ""; +} + +function sha256File(file) { + const digest = createHash("sha256"); + const data = readFileSync(file); + digest.update(data); + return digest.digest("hex"); +} + +function checkedTarMember(name, archive) { + const normalized = String(name).replaceAll("\\", "/").replace(/\/+$/u, ""); + const parts = normalized.split("/").filter((part) => part && part !== "."); + if (parts.length === 0 || normalized.startsWith("/") || parts.includes("..")) { + fail(`${rel(archive)} contains unsafe archive member ${JSON.stringify(name)}`); + } + return parts.join("/"); +} + +function tarZstdMembers(archive) { + const output = run(["tar", "--zstd", "-tf", archive], { + capture: true, + label: `list ${rel(archive)}`, + }); + const members = output + .split(/\r?\n/u) + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => line.replace(/\/+$/u, "")); + for (const member of members) { + checkedTarMember(member, archive); + } + return members; +} + +function extractTarZstd(archive, destination) { + rmSync(destination, { recursive: true, force: true }); + mkdirSync(destination, { recursive: true }); + tarZstdMembers(archive); + run(["tar", "--zstd", "-xf", archive, "-C", destination]); +} + +function payloadFiles(sourceRoot) { + const files = []; + if (!existsSync(sourceRoot)) { + return files; + } + for (const entry of fs.readdirSync(sourceRoot, { withFileTypes: true })) { + const fullPath = path.join(sourceRoot, entry.name); + if (entry.isDirectory()) { + files.push(...payloadFiles(fullPath)); + } else if (entry.isFile()) { + files.push(fullPath); + } + } + return files.sort(compareText); +} + +function targetAssetRoot(extracted) { + const root = path.join(extracted, "target/oliphaunt-wasix/assets"); + if (!isFile(path.join(root, "manifest.json"))) { + fail(`${rel(extracted)} does not contain target/oliphaunt-wasix/assets/manifest.json`); + } + return root; +} + +function targetAotRoot(extracted, triple) { + const root = path.join(extracted, "target/oliphaunt-wasix/aot", triple); + if (!isFile(path.join(root, "manifest.json"))) { + fail(`${rel(extracted)} does not contain target/oliphaunt-wasix/aot/${triple}/manifest.json`); + } + return root; +} + +function targetIcuRoot(extracted) { + const root = path.join(extracted, "target/oliphaunt-wasix/icu/share/icu"); + if (!isDirectory(root)) { + fail(`${rel(extracted)} does not contain target/oliphaunt-wasix/icu/share/icu`); + } + return root; +} + +function readJson(file) { + try { + return JSON.parse(readFileSync(file, "utf8")); + } catch (error) { + fail(`${rel(file)} is not valid JSON: ${error.message}`); + } +} + +function validateRuntimePayload(root) { + const extensionRoot = path.join(root, "extensions"); + const extensionFiles = isDirectory(extensionRoot) ? payloadFiles(extensionRoot) : []; + if (extensionFiles.length > 0) { + fail(`WASIX runtime Cargo payload must not contain extension archives: ${extensionFiles.slice(0, 5).map(rel).join(", ")}`); + } + const manifestPath = path.join(root, "manifest.json"); + const manifest = readJson(manifestPath); + if (JSON.stringify(manifest.extensions) !== "[]") { + fail(`${rel(manifestPath)} must have an empty extensions array`); + } + for (const toolKey of ["pg-dump", "psql"]) { + if (Object.hasOwn(manifest, toolKey)) { + fail(`${rel(manifestPath)} must not contain split WASIX tool entry ${toolKey}`); + } + } + for (const required of [ + "oliphaunt.wasix.tar.zst", + "bin/initdb.wasix.wasm", + "prepopulated/pgdata-template.tar.zst", + "prepopulated/pgdata-template.json", + ]) { + if (!isFile(path.join(root, required))) { + fail(`WASIX runtime Cargo payload is missing ${required}`); + } + } + const runtimeMembers = tarZstdMembers(path.join(root, "oliphaunt.wasix.tar.zst")); + const missingCoreRuntimeFiles = CORE_RUNTIME_ARCHIVE_FILES.filter((member) => !runtimeMembers.includes(member)).sort(compareText); + if (missingCoreRuntimeFiles.length > 0) { + fail(`WASIX runtime Cargo payload must bundle postgres/initdb inside oliphaunt.wasix.tar.zst; missing ${missingCoreRuntimeFiles.join(", ")}`); + } + const bundledIcu = runtimeMembers.filter((member) => member === "oliphaunt/share/icu" || member.startsWith("oliphaunt/share/icu/")); + if (bundledIcu.length > 0) { + fail(`WASIX runtime Cargo payload must not bundle ICU data; found ${bundledIcu[0]} in oliphaunt.wasix.tar.zst`); + } + const bundledTools = runtimeMembers.filter((member) => FORBIDDEN_RUNTIME_ARCHIVE_TOOL_FILES.includes(member)).sort(compareText); + if (bundledTools.length > 0) { + fail(`WASIX runtime Cargo payload must not bundle standalone tools inside oliphaunt.wasix.tar.zst; found ${bundledTools[0]}`); + } +} + +function validateToolsPayload(root) { + const actual = new Set(payloadFiles(root).map((file) => relPath(root, file))); + const expected = new Set(TOOLS_PAYLOAD_FILES); + if (!sameSet(actual, expected)) { + fail(`WASIX tools Cargo payload file set mismatch for ${rel(root)}: expected ${JSON.stringify([...expected].sort(compareText))}, got ${JSON.stringify([...actual].sort(compareText))}`); + } +} + +function relPath(root, file) { + return path.relative(root, file).split(path.sep).join("/"); +} + +function sameSet(left, right) { + if (left.size !== right.size) { + return false; + } + for (const item of left) { + if (!right.has(item)) { + return false; + } + } + return true; +} + +function pruneEmptyDirs(root) { + if (!isDirectory(root)) { + return; + } + const dirs = []; + for (const item of fs.readdirSync(root, { withFileTypes: true })) { + const fullPath = path.join(root, item.name); + if (item.isDirectory()) { + pruneEmptyDirs(fullPath); + dirs.push(fullPath); + } + } + for (const dir of dirs.sort(compareText).reverse()) { + try { + fs.rmdirSync(dir); + } catch { + // Directory still has payload files. + } + } +} + +function pruneRuntimeArchiveTools(archive, scratch) { + const runtimeMembers = tarZstdMembers(archive); + if (!runtimeMembers.some((member) => FORBIDDEN_RUNTIME_ARCHIVE_TOOL_FILES.includes(member))) { + return; + } + extractTarZstd(archive, scratch); + for (const member of FORBIDDEN_RUNTIME_ARCHIVE_TOOL_FILES) { + const file = path.join(scratch, member); + if (existsSync(file)) { + fs.unlinkSync(file); + } + } + pruneEmptyDirs(scratch); + const replacement = `${archive}.tmp`; + rmSync(replacement, { force: true }); + run([ + "tar", + "--sort=name", + "--owner=0", + "--group=0", + "--numeric-owner", + "--mtime=@0", + "--use-compress-program=zstd -19", + "-cf", + replacement, + "-C", + scratch, + "oliphaunt", + ]); + fs.renameSync(replacement, archive); +} + +function rewriteRuntimeCoreManifest(root) { + const manifestPath = path.join(root, "manifest.json"); + const manifest = readJson(manifestPath); + if (!manifest.runtime || typeof manifest.runtime !== "object" || Array.isArray(manifest.runtime)) { + fail(`${rel(manifestPath)} is missing runtime metadata`); + } + manifest.runtime.sha256 = sha256File(path.join(root, "oliphaunt.wasix.tar.zst")); + manifest.extensions = []; + delete manifest["pg-dump"]; + delete manifest.psql; + writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); +} + +function splitRuntimeToolsPayload(runtimeRoot, extractRoot) { + const coreRoot = path.join(extractRoot, "runtime-core-payload"); + const toolsRoot = path.join(extractRoot, "tools-payload"); + rmSync(coreRoot, { recursive: true, force: true }); + rmSync(toolsRoot, { recursive: true, force: true }); + cpSync(runtimeRoot, coreRoot, { recursive: true }); + rmSync(path.join(coreRoot, "extensions"), { recursive: true, force: true }); + const missing = []; + for (const relative of TOOLS_PAYLOAD_FILES) { + const source = path.join(runtimeRoot, relative); + if (!isFile(source)) { + missing.push(relative); + continue; + } + const destination = path.join(toolsRoot, relative); + mkdirSync(path.dirname(destination), { recursive: true }); + copyFileSync(source, destination); + const coreFile = path.join(coreRoot, relative); + if (existsSync(coreFile)) { + fs.unlinkSync(coreFile); + } + } + if (missing.length > 0) { + fail(`WASIX tools Cargo payload is missing ${missing.join(", ")}`); + } + pruneRuntimeArchiveTools( + path.join(coreRoot, "oliphaunt.wasix.tar.zst"), + path.join(extractRoot, "runtime-archive-core-pruned"), + ); + rewriteRuntimeCoreManifest(coreRoot); + pruneEmptyDirs(coreRoot); + return [coreRoot, toolsRoot]; +} + +function icuRootContainsData(root) { + if (!isDirectory(root)) { + return false; + } + for (const name of fs.readdirSync(root).sort(compareText)) { + const child = path.join(root, name); + if (isFile(child) && name.startsWith("icudt") && name.endsWith(".dat")) { + return true; + } + if (isDirectory(child) && name.startsWith("icudt") && payloadFiles(child).length > 0) { + return true; + } + } + return false; +} + +function canonicalIcuRoot(root) { + if (icuRootContainsData(root)) { + return root; + } + const candidates = fs.readdirSync(root) + .map((name) => path.join(root, name)) + .filter((child) => isDirectory(child) && icuRootContainsData(child)) + .sort(compareText); + if (candidates.length !== 1) { + fail(`${rel(root)} must contain exactly one ICU data directory, found ${candidates.length}`); + } + return candidates[0]; +} + +function validateIcuPayload(root) { + if (!icuRootContainsData(root)) { + fail(`ICU Cargo payload is missing icudt data under ${rel(root)}`); + } +} + +function writeIcuPayloadArchive(root, payloadRoot) { + const stage = path.join(path.dirname(payloadRoot), "icu-payload-stage"); + rmSync(stage, { recursive: true, force: true }); + rmSync(payloadRoot, { recursive: true, force: true }); + mkdirSync(path.join(stage, "share"), { recursive: true }); + mkdirSync(payloadRoot, { recursive: true }); + cpSync(root, path.join(stage, "share/icu"), { recursive: true }); + const archive = path.join(payloadRoot, ICU_PAYLOAD_ARCHIVE); + run([ + "tar", + "--sort=name", + "--owner=0", + "--group=0", + "--numeric-owner", + "--mtime=@0", + "--use-compress-program=zstd -19", + "-cf", + archive, + "-C", + stage, + "share/icu", + ]); + const members = tarZstdMembers(archive); + const unexpected = []; + let hasIcuData = false; + for (const member of members) { + if (member === "share/icu") { + continue; + } + if (!member.startsWith("share/icu/")) { + unexpected.push(member); + continue; + } + const relative = member.slice("share/icu/".length).split("/"); + if (relative.length >= 2 && relative[0].startsWith("icudt")) { + hasIcuData = true; + } + } + if (!hasIcuData) { + fail(`${rel(archive)} is missing share/icu/icudt* data`); + } + if (unexpected.length > 0) { + fail(`${rel(archive)} must contain only share/icu data, found ${unexpected[0]}`); + } + return payloadRoot; +} + +function validateAotPayload(root) { + const manifestPath = path.join(root, "manifest.json"); + const manifest = readJson(manifestPath); + const artifacts = manifest.artifacts; + if (!Array.isArray(artifacts) || artifacts.length === 0) { + fail(`${rel(manifestPath)} must contain AOT artifacts`); + } + const expected = new Set(["manifest.json"]); + for (const artifact of artifacts) { + const name = artifact?.name; + const artifactPath = artifact?.path; + if (typeof name !== "string" || !name) { + fail(`${rel(manifestPath)} contains an artifact without a name`); + } + if (name.startsWith("extension:")) { + fail(`WASIX AOT Cargo payload must not contain extension artifact ${name}`); + } + if (typeof artifactPath !== "string" || !artifactPath) { + fail(`AOT artifact ${name} is missing path`); + } + checkedTarMember(artifactPath, manifestPath); + if (!isFile(path.join(root, artifactPath))) { + fail(`AOT artifact ${name} file is missing: ${rel(path.join(root, artifactPath))}`); + } + expected.add(artifactPath); + } + const actual = new Set(payloadFiles(root).map((file) => relPath(root, file))); + if (!sameSet(actual, expected)) { + fail(`WASIX AOT Cargo payload file set mismatch for ${rel(root)}: expected ${JSON.stringify([...expected].sort(compareText))}, got ${JSON.stringify([...actual].sort(compareText))}`); + } +} + +function splitAotToolsPayload(aotRoot, extractRoot, targetId) { + const manifestPath = path.join(aotRoot, "manifest.json"); + const manifest = readJson(manifestPath); + if (!Array.isArray(manifest.artifacts)) { + fail(`${rel(manifestPath)} must contain an artifacts array`); + } + const coreRoot = path.join(extractRoot, `${targetId}-aot-core-payload`); + const toolsRoot = path.join(extractRoot, `${targetId}-aot-tools-payload`); + rmSync(coreRoot, { recursive: true, force: true }); + rmSync(toolsRoot, { recursive: true, force: true }); + const coreArtifacts = []; + const toolsArtifacts = []; + for (const artifact of manifest.artifacts) { + if (!artifact || typeof artifact !== "object" || Array.isArray(artifact)) { + fail(`${rel(manifestPath)} contains a non-object artifact`); + } + const name = artifact.name; + const artifactPath = artifact.path; + if (typeof name !== "string" || typeof artifactPath !== "string") { + fail(`${rel(manifestPath)} contains an artifact without name/path`); + } + const targetRoot = TOOLS_AOT_ARTIFACTS.includes(name) ? toolsRoot : coreRoot; + const targetArtifacts = TOOLS_AOT_ARTIFACTS.includes(name) ? toolsArtifacts : coreArtifacts; + const source = path.join(aotRoot, artifactPath); + if (!isFile(source)) { + fail(`${rel(manifestPath)} references missing AOT artifact ${artifactPath}`); + } + const destination = path.join(targetRoot, artifactPath); + mkdirSync(path.dirname(destination), { recursive: true }); + copyFileSync(source, destination); + targetArtifacts.push(artifact); + } + const missing = TOOLS_AOT_ARTIFACTS.filter((name) => !toolsArtifacts.some((item) => item.name === name)).sort(compareText); + if (missing.length > 0) { + fail(`${rel(manifestPath)} is missing WASIX tools AOT artifacts: ${missing.join(", ")}`); + } + if (coreArtifacts.length === 0) { + fail(`${rel(manifestPath)} generated no core WASIX AOT artifacts`); + } + for (const [targetRoot, targetArtifacts] of [[coreRoot, coreArtifacts], [toolsRoot, toolsArtifacts]]) { + mkdirSync(targetRoot, { recursive: true }); + writeFileSync( + path.join(targetRoot, "manifest.json"), + `${JSON.stringify({ ...manifest, artifacts: targetArtifacts }, null, 2)}\n`, + ); + } + return [coreRoot, toolsRoot]; +} + +function patchToolsAotTemplate(crateDir, target) { + const manifest = path.join(crateDir, "Cargo.toml"); + let text = readFileSync(manifest, "utf8"); + const links = `oliphaunt_artifact_oliphaunt_wasix_tools_aot_${target.replaceAll("-", "_")}`; + text = text.replace(/^links = "[^"]+"$/mu, `links = "${links}"`); + text = text.replace( + /^description = "[^"]+"$/mu, + `description = "Wasmer AOT pg_dump and psql artifacts for oliphaunt-wasix on ${target}"`, + ); + writeFileSync(manifest, text); + + const buildRs = path.join(crateDir, "build.rs"); + text = readFileSync(buildRs, "utf8"); + text = text + .replace('const ARTIFACT_PRODUCT: &str = "liboliphaunt-wasix";', 'const ARTIFACT_PRODUCT: &str = "oliphaunt-wasix-tools";') + .replace('const ARTIFACT_KIND: &str = "wasix-aot";', 'const ARTIFACT_KIND: &str = "wasix-tools-aot";') + .replace('.strip_prefix("liboliphaunt-wasix-aot-")', '.strip_prefix("oliphaunt-wasix-tools-aot-")') + .replace("AOT crate name starts with liboliphaunt-wasix-aot-", "AOT crate name starts with oliphaunt-wasix-tools-aot-"); + writeFileSync(buildRs, text); +} + +function rewriteCargoManifest(manifest, { packageName, version, extensionSources, extensionAotSources }) { + let text = readFileSync(manifest, "utf8"); + text = text.replace(/^name = "[^"]+"$/mu, `name = "${packageName}"`); + text = text.replace(/^version = "[^"]+"$/mu, `version = "${version}"`); + text = text.replace(/^publish = false\n?/gmu, ""); + if (packageName === RUNTIME_PACKAGE && extensionSources.length > 0) { + text = injectRuntimeExtensionDependencies(text, extensionSources, extensionAotSources); + } + if (!text.includes("\n[workspace]")) { + text = `${text.trimEnd()}\n\n[workspace]\n`; + } + writeFileSync(manifest, text); + const packageData = cargoMetadataPackage(manifest); + if (packageData.name !== packageName || packageData.version !== version) { + fail(`${rel(manifest)} generated the wrong package metadata: name=${JSON.stringify(packageData.name)}, version=${JSON.stringify(packageData.version)}`); + } +} + +function extensionFeatureName(packageName) { + if (!packageName.startsWith("oliphaunt-extension-")) { + fail(`invalid extension package name ${packageName}`); + } + return `extension-${packageName.slice("oliphaunt-extension-".length)}`; +} + +function injectRuntimeExtensionDependencies(text, extensionSources, extensionAotSources) { + const dependencyLines = []; + const targetDependencyLines = new Map(); + const aotByExtension = new Map(); + for (const source of extensionAotSources) { + const list = aotByExtension.get(source.spec.sqlName) ?? []; + list.push(source); + aotByExtension.set(source.spec.sqlName, list); + } + for (const source of extensionSources) { + const packageName = source.spec.name; + dependencyLines.push(`${packageName} = { version = "=${source.spec.version}", path = "../${packageName}", optional = true }`); + const feature = extensionFeatureName(source.spec.product); + const featureDeps = [`dep:${packageName}`]; + for (const aotSource of (aotByExtension.get(source.spec.sqlName) ?? []).sort((left, right) => compareText(left.spec.name, right.spec.name))) { + featureDeps.push(`dep:${aotSource.spec.name}`); + } + const replacement = `${feature} = [${featureDeps.map((dep) => JSON.stringify(dep)).join(", ")}]`; + const pattern = new RegExp(`^${escapeRegExp(feature)} = \\[[^\\n]*\\]$`, "mu"); + if (pattern.test(text)) { + text = text.replace(pattern, replacement); + } else { + text = text.replace("[features]\n", `[features]\n${replacement}\n`); + } + } + for (const source of extensionAotSources) { + const cfg = AOT_TARGET_CFGS[source.spec.target]; + if (cfg === undefined) { + fail(`unsupported extension AOT target ${source.spec.target}`); + } + const line = `${source.spec.name} = { version = "=${source.spec.version}", path = "../${source.spec.name}", optional = true }`; + const lines = targetDependencyLines.get(cfg) ?? []; + lines.push(line); + targetDependencyLines.set(cfg, lines); + } + if (dependencyLines.length > 0) { + text = text.replace("\n[build-dependencies]", `\n${dependencyLines.join("\n")}\n\n[build-dependencies]`); + } + if (targetDependencyLines.size > 0) { + const blocks = [...targetDependencyLines.entries()] + .sort(([left], [right]) => compareText(left, right)) + .map(([cfg, lines]) => `[target.'${cfg}'.dependencies]\n${lines.sort(compareText).join("\n")}`); + text = text.replace("\n[build-dependencies]", `\n${blocks.join("\n\n")}\n\n[build-dependencies]`); + } + return text; +} + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); +} + +function copyPackageSource(spec, sourceRoot, version, extensionSources, extensionAotSources) { + const crateDir = path.join(sourceRoot, spec.name); + if (existsSync(crateDir)) { + fail(`duplicate generated WASIX Cargo package source: ${rel(crateDir)}`); + } + cpSync(spec.templateDir, crateDir, { + recursive: true, + filter: (source) => !["target", "payload", "artifacts"].includes(path.basename(source)), + }); + if (spec.kind === "wasix-tools-aot") { + patchToolsAotTemplate(crateDir, spec.target); + } + cpSync(spec.payloadRoot, path.join(crateDir, spec.payloadDirName), { recursive: true }); + rewriteCargoManifest(path.join(crateDir, "Cargo.toml"), { + packageName: spec.name, + version, + extensionSources, + extensionAotSources, + }); + return crateDir; +} + +function cargoMetadataPackage(manifest) { + const stdout = run(["cargo", "metadata", "--no-deps", "--format-version", "1", "--manifest-path", manifest], { + capture: true, + label: `cargo metadata ${rel(manifest)}`, + }); + const data = JSON.parse(stdout); + if (!Array.isArray(data.packages) || data.packages.length !== 1 || typeof data.packages[0] !== "object") { + fail(`cargo metadata for ${rel(manifest)} did not return exactly one package`); + } + return data.packages[0]; +} + +function cargoPackage(crateDir, targetDir, { noVerify = false } = {}) { + const manifest = path.join(crateDir, "Cargo.toml"); + const packageData = cargoMetadataPackage(manifest); + const command = [ + "cargo", + "package", + "--manifest-path", + manifest, + "--target-dir", + targetDir, + "--allow-dirty", + ]; + if (noVerify) { + command.push("--no-verify"); + } + run(command, { + env: { ...process.env, OLIPHAUNT_ARTIFACT_CRATE_REQUIRE_PAYLOAD: "1" }, + }); + const cratePath = path.join(targetDir, "package", `${packageData.name}-${packageData.version}.crate`); + if (!isFile(cratePath)) { + fail(`cargo package did not create ${rel(cratePath)}`); + } + return cratePath; +} + +function packagedManifestText(text) { + return text.replace(/, path = "\.\.\/[^"]+"/gu, ""); +} + +function cargoPackageWithoutDependencyResolution(crateDir, targetDir) { + const manifest = path.join(crateDir, "Cargo.toml"); + const packageData = cargoMetadataPackage(manifest); + const packageRoot = `${packageData.name}-${packageData.version}`; + const stageRoot = path.join(targetDir, "manual-package-stage"); + const stageDir = path.join(stageRoot, packageRoot); + const cratePath = path.join(targetDir, "package", `${packageRoot}.crate`); + rmSync(stageDir, { recursive: true, force: true }); + mkdirSync(path.dirname(cratePath), { recursive: true }); + cpSync(crateDir, stageDir, { + recursive: true, + filter: (source) => !["target", ".git"].includes(path.basename(source)), + }); + const stagedManifest = path.join(stageDir, "Cargo.toml"); + writeFileSync(stagedManifest, packagedManifestText(readFileSync(stagedManifest, "utf8"))); + cargoMetadataPackage(stagedManifest); + rmSync(cratePath, { force: true }); + run([ + "tar", + "--sort=name", + "--owner=0", + "--group=0", + "--numeric-owner", + "--mtime=@0", + "-czf", + cratePath, + "-C", + stageRoot, + packageRoot, + ]); + if (!isFile(cratePath)) { + fail(`manual package did not create ${rel(cratePath)}`); + } + return cratePath; +} + +function validateCrateSize(cratePath) { + const size = statSync(cratePath).size; + if (size > CRATES_IO_MAX_BYTES) { + fail(`${rel(cratePath)} is ${size} bytes, above the crates.io 10 MiB package limit; reduce the WASIX Cargo payload before publishing`); + } +} + +function packageSpec(spec, { version, sourceRoot, outputDir, cargoTargetDir, extensionSources, extensionAotSources }) { + const crateDir = copyPackageSource(spec, sourceRoot, version, extensionSources, extensionAotSources); + const cratePath = spec.name === RUNTIME_PACKAGE && extensionSources.length > 0 + ? cargoPackageWithoutDependencyResolution(crateDir, cargoTargetDir) + : cargoPackage(crateDir, cargoTargetDir); + validateCrateSize(cratePath); + const output = path.join(outputDir, path.basename(cratePath)); + copyFileSync(cratePath, output); + return { + name: spec.name, + manifestPath: path.join(crateDir, "Cargo.toml"), + cratePath: output, + target: spec.target, + kind: spec.kind, + size: statSync(output).size, + sha256: sha256File(output), + }; +} + +function wasixExtensionAotPartPackageName(packageName, index) { + return `${packageName}-part-${String(index).padStart(3, "0")}`; +} + +function rustCrateIdent(packageName) { + return packageName.replaceAll("-", "_"); +} + +function discoverExtensionManifests(roots) { + const manifests = []; + for (const root of roots) { + if (isFile(root) && path.basename(root) === "extension-artifacts.json") { + manifests.push(root); + continue; + } + if (isDirectory(root)) { + for (const file of payloadFiles(root)) { + if (path.basename(file) === "extension-artifacts.json") { + manifests.push(file); + } + } + } + } + return [...new Set(manifests)].sort(compareText); +} + +function extensionWasixAsset(extensionDir, manifest) { + for (const asset of manifest.assets ?? []) { + if ( + asset && + typeof asset === "object" && + asset.family === "wasix" && + asset.kind === "wasix-runtime" && + asset.target === "wasix-portable" && + typeof asset.name === "string" + ) { + const assetPath = path.join(extensionDir, "release-assets", asset.name); + if (isFile(assetPath)) { + return assetPath; + } + } + } + return null; +} + +function extensionAotSpecs(extensionDir, { product, version, sqlName }) { + const aotRoot = path.join(extensionDir, "wasix-aot"); + if (!isDirectory(aotRoot)) { + return []; + } + const specs = []; + const seenTargets = new Set(); + for (const targetDir of fs.readdirSync(aotRoot).map((name) => path.join(aotRoot, name)).filter(isDirectory).sort(compareText)) { + const manifestPath = path.join(targetDir, "manifest.json"); + if (!isFile(manifestPath)) { + continue; + } + const data = readJson(manifestPath); + const target = data["target-triple"]; + const artifacts = data.artifacts; + if (typeof target !== "string" || !target) { + fail(`${rel(manifestPath)} is missing target-triple`); + } + if (seenTargets.has(target)) { + fail(`${rel(aotRoot)} has duplicate extension AOT target ${target}`); + } + if (!Array.isArray(artifacts) || artifacts.length === 0) { + fail(`${rel(manifestPath)} must contain extension AOT artifacts`); + } + const expectedPrefix = `extension:${sqlName}`; + for (const artifact of artifacts) { + const name = artifact?.name; + const artifactPath = artifact?.path; + if (typeof name !== "string" || !(name === expectedPrefix || name.startsWith(`${expectedPrefix}:`))) { + fail(`${rel(manifestPath)} contains AOT artifact ${JSON.stringify(name)} for ${sqlName}`); + } + if (typeof artifactPath !== "string" || !artifactPath) { + fail(`${rel(manifestPath)} artifact ${JSON.stringify(name)} is missing path`); + } + checkedTarMember(artifactPath, manifestPath); + if (!isFile(path.join(path.dirname(manifestPath), artifactPath))) { + fail(`${rel(manifestPath)} references missing AOT artifact ${artifactPath}`); + } + } + seenTargets.add(target); + specs.push({ + name: wasixExtensionAotPackageName(product, target), + version, + sqlName, + target, + sourceDir: path.dirname(manifestPath), + }); + } + return specs.sort((left, right) => compareText(left.target, right.target)); +} + +function extensionCargoSpecs(extensionRoots) { + const specs = []; + for (const manifestPath of discoverExtensionManifests(extensionRoots)) { + const manifest = readJson(manifestPath); + const { product, version, sqlName, nativeModuleStem } = manifest; + if (![product, version, sqlName].every((value) => typeof value === "string" && value)) { + fail(`${rel(manifestPath)} is missing product, version, or sqlName`); + } + const archive = extensionWasixAsset(path.dirname(manifestPath), manifest); + if (archive === null) { + continue; + } + specs.push({ + name: wasixExtensionPackageName(product), + product, + version, + sqlName, + archive, + sha256: sha256File(archive), + size: statSync(archive).size, + requiresAot: typeof nativeModuleStem === "string" && Boolean(nativeModuleStem), + aotTargets: extensionAotSpecs(path.dirname(manifestPath), { product, version, sqlName }), + }); + } + return specs.sort((left, right) => compareText(left.name, right.name)); +} + +function validateExtensionAotCoverage(extensionSpecs) { + for (const spec of extensionSpecs) { + if (!spec.requiresAot) { + continue; + } + const actualTargets = new Set(spec.aotTargets.map((aotSpec) => aotSpec.target)); + if (!sameSet(actualTargets, EXPECTED_EXTENSION_AOT_TARGETS)) { + fail(`${spec.product} has a WASIX native module but incomplete extension AOT artifacts; expected=${JSON.stringify([...EXPECTED_EXTENSION_AOT_TARGETS].sort(compareText))}, actual=${JSON.stringify([...actualTargets].sort(compareText))}`); + } + } +} + +function writeExtensionCargoSource(spec, sourceRoot) { + const crateDir = path.join(sourceRoot, spec.name); + if (existsSync(crateDir)) { + fail(`duplicate generated WASIX extension Cargo package source: ${rel(crateDir)}`); + } + mkdirSync(path.join(crateDir, "src"), { recursive: true }); + mkdirSync(path.join(crateDir, "payload"), { recursive: true }); + copyFileSync(spec.archive, path.join(crateDir, "payload/extension.tar.zst")); + writeFileSync(path.join(crateDir, "README.md"), [ + `# ${spec.name}`, + "", + `Cargo artifact package for the \`${spec.sqlName}\` Oliphaunt WASIX extension.`, + "", + ].join("\n")); + writeFileSync(path.join(crateDir, "Cargo.toml"), [ + "[package]", + `name = "${spec.name}"`, + `version = "${spec.version}"`, + 'edition = "2024"', + 'rust-version = "1.93"', + `description = "Oliphaunt WASIX artifact package for the ${spec.sqlName} 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]", + "", + ].join("\n")); + writeFileSync(path.join(crateDir, "src/lib.rs"), [ + "#![deny(unsafe_code)]", + "", + `pub const SQL_NAME: &str = "${spec.sqlName}";`, + `pub const ARCHIVE_SHA256: &str = "${spec.sha256}";`, + `pub const ARCHIVE_SIZE: u64 = ${spec.size};`, + "", + "pub fn archive() -> Option<&'static [u8]> {", + ' Some(include_bytes!("../payload/extension.tar.zst"))', + "}", + "", + ].join("\n")); + return { spec, sourceDir: crateDir }; +} + +function writeExtensionAotCargoSource(spec, sourceRoot) { + const crateDir = path.join(sourceRoot, spec.name); + if (existsSync(crateDir)) { + fail(`duplicate generated WASIX extension AOT Cargo package source: ${rel(crateDir)}`); + } + mkdirSync(path.join(crateDir, "src"), { recursive: true }); + const manifestPath = path.join(spec.sourceDir, "manifest.json"); + const manifest = readJson(manifestPath); + const artifacts = []; + for (const artifact of [...(manifest.artifacts ?? [])].sort((left, right) => compareText(left?.name ?? "", right?.name ?? ""))) { + const name = artifact?.name; + const artifactPath = artifact?.path; + if (typeof name !== "string" || typeof artifactPath !== "string") { + fail(`${rel(manifestPath)} contains an AOT artifact without name/path`); + } + const source = path.join(spec.sourceDir, artifactPath); + if (!isFile(source)) { + fail(`${rel(manifestPath)} references missing AOT artifact ${artifactPath}`); + } + artifacts.push([name, artifactPath, source, statSync(source).size]); + } + if (artifacts.length === 0) { + fail(`${rel(manifestPath)} must contain extension AOT artifacts`); + } + const splitParts = artifacts.reduce((sum, item) => sum + item[3], 0) > EXTENSION_AOT_SPLIT_THRESHOLD_BYTES; + const partSources = []; + if (splitParts) { + mkdirSync(path.join(crateDir, "artifacts"), { recursive: true }); + copyFileSync(manifestPath, path.join(crateDir, "artifacts/manifest.json")); + artifacts.forEach(([name, artifactPath, source], index) => { + const partName = wasixExtensionAotPartPackageName(spec.name, index); + const partDir = path.join(sourceRoot, partName); + if (existsSync(partDir)) { + fail(`duplicate generated WASIX extension AOT Cargo package source: ${rel(partDir)}`); + } + mkdirSync(path.join(partDir, "src"), { recursive: true }); + const destination = path.join(partDir, "artifacts", artifactPath); + mkdirSync(path.dirname(destination), { recursive: true }); + copyFileSync(source, destination); + writeFileSync(path.join(partDir, "README.md"), [ + `# ${partName}`, + "", + `Cargo artifact package part for \`${spec.sqlName}\` Oliphaunt WASIX AOT artifacts on \`${spec.target}\`.`, + "", + ].join("\n")); + writeFileSync(path.join(partDir, "Cargo.toml"), [ + "[package]", + `name = "${partName}"`, + `version = "${spec.version}"`, + 'edition = "2024"', + 'rust-version = "1.93"', + `description = "Oliphaunt WASIX AOT artifact package part for the ${spec.sqlName} 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]", + "", + ].join("\n")); + writeFileSync(path.join(partDir, "src/lib.rs"), [ + "#![deny(unsafe_code)]", + "", + `pub const SQL_NAME: &str = "${spec.sqlName}";`, + `pub const TARGET_TRIPLE: &str = "${spec.target}";`, + "", + "pub fn aot_artifact_bytes(name: &str) -> Option<&'static [u8]> {", + " match name {", + ` ${JSON.stringify(name)} => Some(include_bytes!("../artifacts/${artifactPath}")),`, + " _ => None,", + " }", + "}", + "", + ].join("\n")); + partSources.push({ + name: partName, + version: spec.version, + sqlName: spec.sqlName, + target: spec.target, + sourceDir: partDir, + }); + }); + } else { + cpSync(spec.sourceDir, path.join(crateDir, "artifacts"), { recursive: true }); + } + + const dependencyLines = partSources.map((part) => `${part.name} = { version = "=${part.version}", path = "../${part.name}" }`); + writeFileSync(path.join(crateDir, "README.md"), [ + `# ${spec.name}`, + "", + `Cargo artifact package for \`${spec.sqlName}\` Oliphaunt WASIX AOT artifacts on \`${spec.target}\`.`, + "", + ].join("\n")); + writeFileSync(path.join(crateDir, "Cargo.toml"), [ + "[package]", + `name = "${spec.name}"`, + `version = "${spec.version}"`, + 'edition = "2024"', + 'rust-version = "1.93"', + `description = "Oliphaunt WASIX AOT artifact package for the ${spec.sqlName} 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"', + "", + ...(partSources.length > 0 ? ["[dependencies]", ...dependencyLines, ""] : []), + "[workspace]", + "", + ].join("\n")); + + const artifactBytesBody = partSources.length > 0 + ? partSources.flatMap((part) => [ + ` if let Some(bytes) = ${rustCrateIdent(part.name)}::aot_artifact_bytes(name) {`, + " return Some(bytes);", + " }", + ]) + : [ + " match name {", + ...artifacts.map(([name, artifactPath]) => ` ${JSON.stringify(name)} => Some(include_bytes!("../artifacts/${artifactPath}")),`), + " _ => None,", + " }", + ]; + writeFileSync(path.join(crateDir, "src/lib.rs"), [ + "#![deny(unsafe_code)]", + "", + `pub const SQL_NAME: &str = "${spec.sqlName}";`, + `pub const TARGET_TRIPLE: &str = "${spec.target}";`, + 'pub const MANIFEST_JSON: &str = include_str!("../artifacts/manifest.json");', + "", + "pub fn aot_manifest_json() -> Option<&'static str> {", + " Some(MANIFEST_JSON)", + "}", + "", + "pub fn aot_artifact_bytes(name: &str) -> Option<&'static [u8]> {", + ...artifactBytesBody, + ...(partSources.length > 0 ? [" None"] : []), + "}", + "", + ].join("\n")); + return { spec, sourceDir: crateDir, partSources }; +} + +function packageExtensionSource(source, { outputDir, cargoTargetDir }) { + const cratePath = cargoPackage(source.sourceDir, cargoTargetDir); + validateCrateSize(cratePath); + const output = path.join(outputDir, path.basename(cratePath)); + copyFileSync(cratePath, output); + return { + name: source.spec.name, + manifestPath: path.join(source.sourceDir, "Cargo.toml"), + cratePath: output, + target: "wasix-portable", + kind: "wasix-extension", + size: statSync(output).size, + sha256: sha256File(output), + }; +} + +function packageExtensionAotSource(source, { outputDir, cargoTargetDir }) { + const packages = []; + for (const part of source.partSources ?? []) { + const cratePath = cargoPackage(part.sourceDir, cargoTargetDir); + validateCrateSize(cratePath); + const output = path.join(outputDir, path.basename(cratePath)); + copyFileSync(cratePath, output); + packages.push({ + name: part.name, + manifestPath: path.join(part.sourceDir, "Cargo.toml"), + cratePath: output, + target: part.target, + kind: "wasix-extension-aot", + size: statSync(output).size, + sha256: sha256File(output), + }); + } + const cratePath = source.partSources?.length > 0 + ? cargoPackageWithoutDependencyResolution(source.sourceDir, cargoTargetDir) + : cargoPackage(source.sourceDir, cargoTargetDir); + validateCrateSize(cratePath); + const output = path.join(outputDir, path.basename(cratePath)); + copyFileSync(cratePath, output); + packages.push({ + name: source.spec.name, + manifestPath: path.join(source.sourceDir, "Cargo.toml"), + cratePath: output, + target: source.spec.target, + kind: "wasix-extension-aot", + size: statSync(output).size, + sha256: sha256File(output), + }); + return packages; +} + +function packageSpecs(assetDir, extractRoot, version) { + const specs = []; + const runtimeArchive = path.join(assetDir, `liboliphaunt-wasix-${version}-runtime-portable.tar.zst`); + if (!isFile(runtimeArchive)) { + fail(`missing WASIX portable runtime release asset: ${rel(runtimeArchive)}`); + } + const runtimeExtract = path.join(extractRoot, "runtime-extracted"); + extractTarZstd(runtimeArchive, runtimeExtract); + const runtimeRoot = targetAssetRoot(runtimeExtract); + const [runtimeCoreRoot, toolsRoot] = splitRuntimeToolsPayload(runtimeRoot, extractRoot); + validateRuntimePayload(runtimeCoreRoot); + validateToolsPayload(toolsRoot); + specs.push({ + name: RUNTIME_PACKAGE, + target: "portable", + kind: "wasix-runtime", + templateDir: path.join(ROOT, "src/runtimes/liboliphaunt/wasix/crates/assets"), + payloadRoot: runtimeCoreRoot, + payloadDirName: "payload", + }); + specs.push({ + name: TOOLS_PACKAGE, + target: "portable", + kind: "wasix-tools", + templateDir: path.join(ROOT, "src/runtimes/liboliphaunt/wasix/crates/tools"), + payloadRoot: toolsRoot, + payloadDirName: "payload", + }); + + const icuArchive = path.join(assetDir, `liboliphaunt-wasix-${version}-icu-data.tar.zst`); + if (!isFile(icuArchive)) { + fail(`missing WASIX ICU data release asset: ${rel(icuArchive)}`); + } + const icuExtract = path.join(extractRoot, "icu-extracted"); + extractTarZstd(icuArchive, icuExtract); + const icuRoot = canonicalIcuRoot(targetIcuRoot(icuExtract)); + validateIcuPayload(icuRoot); + const icuPayloadRoot = writeIcuPayloadArchive(icuRoot, path.join(extractRoot, "icu-payload")); + specs.push({ + name: ICU_PACKAGE, + target: "portable", + kind: "icu-data", + templateDir: path.join(ROOT, "src/runtimes/liboliphaunt/icu"), + payloadRoot: icuPayloadRoot, + payloadDirName: "payload", + }); + + for (const [targetId, packageName] of Object.entries(AOT_PACKAGES).sort(([left], [right]) => compareText(left, right))) { + const archive = path.join(assetDir, `liboliphaunt-wasix-${version}-runtime-aot-${targetId}.tar.zst`); + if (!isFile(archive)) { + fail(`missing WASIX AOT release asset: ${rel(archive)}`); + } + const extracted = path.join(extractRoot, `${targetId}-extracted`); + extractTarZstd(archive, extracted); + const triple = AOT_TARGET_TRIPLES[targetId]; + const aotRoot = targetAotRoot(extracted, triple); + validateAotPayload(aotRoot); + const [aotCoreRoot, toolsAotRoot] = splitAotToolsPayload(aotRoot, extractRoot, targetId); + specs.push({ + name: packageName, + target: triple, + kind: "wasix-aot", + templateDir: path.join(ROOT, "src/runtimes/liboliphaunt/wasix/crates/aot", triple), + payloadRoot: aotCoreRoot, + payloadDirName: "artifacts", + }); + specs.push({ + name: TOOLS_AOT_PACKAGES[targetId], + target: triple, + kind: "wasix-tools-aot", + templateDir: path.join(ROOT, "src/runtimes/liboliphaunt/wasix/crates/tools-aot", triple), + payloadRoot: toolsAotRoot, + payloadDirName: "artifacts", + }); + } + return specs; +} + +function writePackagesManifest(packages, outputDir) { + const data = { + schema: WASIX_CARGO_ARTIFACT_SCHEMA, + product: PRODUCT, + packages: packages.map((packageData) => ({ + name: packageData.name, + target: packageData.target, + kind: packageData.kind, + role: "artifact", + manifestPath: rel(packageData.manifestPath), + cratePath: rel(packageData.cratePath), + size: packageData.size, + sha256: packageData.sha256, + })), + }; + writeFileSync(path.join(outputDir, "packages.json"), `${JSON.stringify(data, null, 2)}\n`); +} + +function parseArgs(argv) { + const args = { + assetDir: "target/oliphaunt-wasix/release-assets", + outputDir: "target/oliphaunt-wasix/cargo-artifacts", + version: null, + extensionArtifactRoots: ["target/extension-artifacts"], + }; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === "--help" || value === "-h") { + console.log("usage: tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs [--asset-dir DIR] [--output-dir DIR] [--version VERSION] [--extension-artifact-root DIR...]"); + process.exit(0); + } else if (value === "--asset-dir") { + args.assetDir = requiredValue(argv, ++index, value); + } else if (value.startsWith("--asset-dir=")) { + args.assetDir = value.slice("--asset-dir=".length); + } else if (value === "--output-dir") { + args.outputDir = requiredValue(argv, ++index, value); + } else if (value.startsWith("--output-dir=")) { + args.outputDir = value.slice("--output-dir=".length); + } else if (value === "--version") { + args.version = requiredValue(argv, ++index, value); + } else if (value.startsWith("--version=")) { + args.version = value.slice("--version=".length); + } else if (value === "--extension-artifact-root") { + args.extensionArtifactRoots.push(requiredValue(argv, ++index, value)); + } else if (value.startsWith("--extension-artifact-root=")) { + args.extensionArtifactRoots.push(value.slice("--extension-artifact-root=".length)); + } else { + fail(`unknown argument ${value}`); + } + } + args.version ??= currentProductVersionSync(PRODUCT, PREFIX); + return args; +} + +function requiredValue(argv, index, option) { + const value = argv[index]; + if (value === undefined || value.startsWith("--")) { + fail(`${option} requires a value`); + } + return value; +} + +function repoPath(value) { + return path.isAbsolute(value) ? value : path.join(ROOT, value); +} + +function main(argv) { + const args = parseArgs(argv); + const assetDir = repoPath(args.assetDir); + const outputDir = repoPath(args.outputDir); + const extensionRoots = args.extensionArtifactRoots.map(repoPath); + if (!isDirectory(assetDir)) { + fail(`WASIX release asset directory does not exist: ${rel(assetDir)}`); + } + + const sourceRoot = path.join(ROOT, "target/oliphaunt-wasix/cargo-package-sources"); + const extractRoot = path.join(ROOT, "target/oliphaunt-wasix/cargo-package-extracted"); + const cargoTargetDir = path.join(ROOT, "target/oliphaunt-wasix/cargo-package-target"); + rmSync(sourceRoot, { recursive: true, force: true }); + rmSync(extractRoot, { recursive: true, force: true }); + rmSync(outputDir, { recursive: true, force: true }); + rmSync(cargoTargetDir, { recursive: true, force: true }); + mkdirSync(sourceRoot, { recursive: true }); + mkdirSync(extractRoot, { recursive: true }); + mkdirSync(outputDir, { recursive: true }); + + const extensionSpecs = extensionCargoSpecs(extensionRoots); + validateExtensionAotCoverage(extensionSpecs); + const extensionSources = extensionSpecs.map((spec) => writeExtensionCargoSource(spec, sourceRoot)); + const extensionAotSources = extensionSpecs.flatMap((spec) => spec.aotTargets.map((aotSpec) => writeExtensionAotCargoSource(aotSpec, sourceRoot))); + const specs = packageSpecs(assetDir, extractRoot, args.version); + const packages = [ + ...extensionSources.map((source) => packageExtensionSource(source, { outputDir, cargoTargetDir })), + ...extensionAotSources.flatMap((source) => packageExtensionAotSource(source, { outputDir, cargoTargetDir })), + ...specs.map((spec) => packageSpec(spec, { + version: args.version, + sourceRoot, + outputDir, + cargoTargetDir, + extensionSources, + extensionAotSources, + })), + ]; + writePackagesManifest(packages, outputDir); + console.log("generated liboliphaunt-wasix Cargo artifact crates:"); + for (const packageData of packages) { + console.log(`${packageData.name} ${rel(packageData.cratePath)} ${packageData.size} bytes`); + } +} + +main(Bun.argv.slice(2)); diff --git a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py b/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py deleted file mode 100644 index f182006c..00000000 --- a/tools/release/package_liboliphaunt_wasix_cargo_artifacts.py +++ /dev/null @@ -1,1577 +0,0 @@ -#!/usr/bin/env python3 -"""Package liboliphaunt WASIX runtime assets as direct Cargo artifact crates.""" - -from __future__ import annotations - -import argparse -import hashlib -import json -import os -import re -import shutil -import subprocess -import sys -import tarfile -from dataclasses import dataclass -from functools import lru_cache -from pathlib import Path, PurePosixPath -from typing import Any, NoReturn - - -ROOT = Path(__file__).resolve().parents[2] -PRODUCT = "liboliphaunt-wasix" - - -def release_graph_json(command: str, args: tuple[str, ...] = ()) -> Any: - try: - output = subprocess.check_output( - ["tools/dev/bun.sh", "tools/release/release_graph_query.mjs", command, *args], - cwd=ROOT, - text=True, - stderr=subprocess.PIPE, - ) - except subprocess.CalledProcessError as error: - detail = (error.stderr or "").strip() - if detail: - raise RuntimeError(f"release graph {command} query failed: {detail}") from error - raise RuntimeError(f"release graph {command} query failed with exit code {error.returncode}") from error - try: - return json.loads(output) - except json.JSONDecodeError as error: - raise RuntimeError(f"release graph {command} query did not return valid JSON: {error}") from error - - -@lru_cache(maxsize=None) -def release_graph_rows(command: str, args: tuple[str, ...] = ()) -> tuple[dict[str, Any], ...]: - rows = release_graph_json(command, args) - if not isinstance(rows, list) or not all(isinstance(row, dict) for row in rows): - raise RuntimeError(f"release graph {command} query must return a JSON object list") - return tuple(rows) - - -@lru_cache(maxsize=1) -def wasix_cargo_artifact_contract() -> dict[str, Any]: - contract = release_graph_json("wasix-cargo-artifact-contract") - if not isinstance(contract, dict): - raise RuntimeError("release graph wasix-cargo-artifact-contract query must return a JSON object") - return contract - - -def wasix_contract_string(key: str) -> str: - value = wasix_cargo_artifact_contract().get(key) - if not isinstance(value, str) or not value: - raise RuntimeError(f"WASIX Cargo artifact contract {key} must be a non-empty string") - return value - - -def wasix_contract_string_list(key: str) -> tuple[str, ...]: - value = wasix_cargo_artifact_contract().get(key) - if not isinstance(value, list) or not all(isinstance(item, str) and item for item in value): - raise RuntimeError(f"WASIX Cargo artifact contract {key} must be a string list") - return tuple(value) - - -def wasix_contract_string_map(key: str) -> dict[str, str]: - value = wasix_cargo_artifact_contract().get(key) - if not isinstance(value, dict) or not all( - isinstance(item_key, str) - and item_key - and isinstance(item_value, str) - and item_value - for item_key, item_value in value.items() - ): - raise RuntimeError(f"WASIX Cargo artifact contract {key} must be a string map") - return dict(value) - - -def wasix_cargo_artifact_schema() -> str: - return wasix_contract_string("schema") - - -def wasix_runtime_package_name() -> str: - return wasix_contract_string("runtimePackage") - - -def wasix_tools_package_name() -> str: - return wasix_contract_string("toolsPackage") - - -def wasix_icu_package_name() -> str: - return wasix_contract_string("icuPackage") - - -def wasix_icu_payload_archive_name() -> str: - return wasix_contract_string("icuPayloadArchive") - - -def wasix_tools_payload_files() -> tuple[str, ...]: - return wasix_contract_string_list("toolsPayloadFiles") - - -def wasix_core_runtime_archive_files() -> tuple[str, ...]: - return wasix_contract_string_list("coreRuntimeArchiveFiles") - - -def wasix_forbidden_runtime_archive_tool_files() -> tuple[str, ...]: - return wasix_contract_string_list("forbiddenRuntimeArchiveToolFiles") - - -def wasix_tools_aot_artifacts() -> set[str]: - return set(wasix_contract_string_list("toolsAotArtifacts")) - - -def wasix_aot_packages() -> dict[str, str]: - return wasix_contract_string_map("aotPackages") - - -def wasix_tools_aot_packages() -> dict[str, str]: - return wasix_contract_string_map("toolsAotPackages") - - -def wasix_aot_target_triples() -> dict[str, str]: - return wasix_contract_string_map("aotTargetTriples") - - -def wasix_aot_target_cfgs() -> dict[str, str]: - return wasix_contract_string_map("aotTargetCfgs") - - -def wasix_expected_extension_aot_targets() -> tuple[str, ...]: - return wasix_contract_string_list("expectedExtensionAotTargets") - - -def public_cargo_package_names() -> tuple[str, ...]: - return wasix_contract_string_list("publicCargoPackageNames") - - -def public_aot_cargo_dependencies() -> dict[str, str]: - return wasix_contract_string_map("publicAotCargoDependencies") - - -def public_tools_aot_cargo_dependencies() -> dict[str, str]: - return wasix_contract_string_map("publicToolsAotCargoDependencies") - - -def public_tools_feature_dependencies() -> set[str]: - return set(wasix_contract_string_list("publicToolsFeatureDependencies")) - - -@lru_cache(maxsize=1) -def wasix_extension_package_rows() -> tuple[dict[str, Any], ...]: - rows = release_graph_rows("wasix-extension-package-names") - seen: set[str] = set() - for row in rows: - product = row.get("product") - package_name = row.get("packageName") - aot_packages = row.get("aotPackages") - if not isinstance(product, str) or not product: - raise RuntimeError("release graph wasix-extension-package-names rows must declare a non-empty product") - if product in seen: - raise RuntimeError(f"release graph wasix-extension-package-names returned duplicate product {product}") - seen.add(product) - if not isinstance(package_name, str) or not package_name: - raise RuntimeError(f"release graph wasix-extension-package-names {product}.packageName must be non-empty") - if not isinstance(aot_packages, list) or not all(isinstance(item, dict) for item in aot_packages): - raise RuntimeError(f"release graph wasix-extension-package-names {product}.aotPackages must be an object list") - if not rows: - raise RuntimeError("release graph returned no WASIX extension package names") - return rows - - -def wasix_extension_package_contract(product: str) -> dict[str, Any]: - matches = [row for row in wasix_extension_package_rows() if row.get("product") == product] - if len(matches) != 1: - raise RuntimeError(f"release graph wasix-extension-package-names returned {len(matches)} rows for {product}") - return dict(matches[0]) - - -def wasix_extension_package_name(product: str) -> str: - return str(wasix_extension_package_contract(product).get("packageName")) - - -def wasix_extension_aot_package_name(product: str, target: str) -> str: - rows = wasix_extension_package_contract(product).get("aotPackages") - assert isinstance(rows, list) - matches = [row for row in rows if row.get("target") == target] - if len(matches) != 1: - raise RuntimeError(f"release graph returned {len(matches)} WASIX extension AOT package names for {product}/{target}") - package_name = matches[0].get("packageName") - if not isinstance(package_name, str) or not package_name: - raise RuntimeError(f"release graph wasix-extension-package-names {product}/{target}.packageName must be non-empty") - return package_name - - -def read_current_version(product: str) -> str: - rows = release_graph_rows("product-versions", ("--product", product)) - if len(rows) != 1: - raise RuntimeError(f"release graph product-versions query returned {len(rows)} rows for {product}") - version = rows[0].get("version") - if not isinstance(version, str) or not version: - raise RuntimeError(f"release graph product-versions {product}.version must be a non-empty string") - return version - - -SCHEMA = wasix_cargo_artifact_schema() -CRATES_IO_MAX_BYTES = 10 * 1024 * 1024 -EXTENSION_AOT_SPLIT_THRESHOLD_BYTES = 9 * 1024 * 1024 -RUNTIME_PACKAGE = wasix_runtime_package_name() -TOOLS_PACKAGE = wasix_tools_package_name() -ICU_PACKAGE = wasix_icu_package_name() -ICU_PAYLOAD_ARCHIVE = wasix_icu_payload_archive_name() -TOOLS_PAYLOAD_FILES = wasix_tools_payload_files() -CORE_RUNTIME_ARCHIVE_FILES = wasix_core_runtime_archive_files() -FORBIDDEN_RUNTIME_ARCHIVE_TOOL_FILES = wasix_forbidden_runtime_archive_tool_files() -TOOLS_AOT_ARTIFACTS = wasix_tools_aot_artifacts() -AOT_PACKAGES = wasix_aot_packages() -TOOLS_AOT_PACKAGES = wasix_tools_aot_packages() -AOT_TARGET_TRIPLES = wasix_aot_target_triples() -AOT_TARGET_CFGS = wasix_aot_target_cfgs() -EXPECTED_EXTENSION_AOT_TARGETS = frozenset(wasix_expected_extension_aot_targets()) - - -@dataclass(frozen=True) -class PackageSpec: - name: str - target: str - kind: str - template_dir: Path - payload_root: Path - payload_dir_name: str - - -@dataclass(frozen=True) -class GeneratedPackage: - name: str - manifest_path: Path - crate_path: Path - target: str - kind: str - size: int - sha256: str - - -@dataclass(frozen=True) -class ExtensionCargoSpec: - name: str - product: str - version: str - sql_name: str - archive: Path - sha256: str - size: int - requires_aot: bool - 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 - 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: - print(f"package_liboliphaunt_wasix_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 checked_tar_member(name: str, archive: Path) -> PurePosixPath: - path = PurePosixPath(name) - parts = tuple(part for part in path.parts if part not in {"", "."}) - if not parts or any(part == ".." for part in parts) or path.is_absolute(): - fail(f"{rel(archive)} contains unsafe archive member {name!r}") - return PurePosixPath(*parts) - - -def tar_zstd_members(archive: Path) -> list[str]: - result = subprocess.run( - ["tar", "--zstd", "-tf", str(archive)], - cwd=ROOT, - text=True, - capture_output=True, - check=False, - ) - if result.returncode != 0: - fail(f"could not list {rel(archive)}: {result.stderr.strip()}") - members = [line.rstrip("/") for line in result.stdout.splitlines() if line.strip()] - for member in members: - checked_tar_member(member, archive) - return members - - -def extract_tar_zstd(archive: Path, destination: Path) -> None: - shutil.rmtree(destination, ignore_errors=True) - destination.mkdir(parents=True, exist_ok=True) - tar_zstd_members(archive) - run(["tar", "--zstd", "-xf", str(archive), "-C", str(destination)]) - - -def payload_files(source_root: Path) -> list[Path]: - return sorted(path for path in source_root.rglob("*") if path.is_file()) - - -def target_asset_root(extracted: Path) -> Path: - root = extracted / "target/oliphaunt-wasix/assets" - if not (root / "manifest.json").is_file(): - fail(f"{rel(extracted)} does not contain target/oliphaunt-wasix/assets/manifest.json") - return root - - -def target_aot_root(extracted: Path, triple: str) -> Path: - root = extracted / "target/oliphaunt-wasix/aot" / triple - if not (root / "manifest.json").is_file(): - fail(f"{rel(extracted)} does not contain target/oliphaunt-wasix/aot/{triple}/manifest.json") - return root - - -def target_icu_root(extracted: Path) -> Path: - root = extracted / "target/oliphaunt-wasix/icu/share/icu" - if not root.is_dir(): - fail(f"{rel(extracted)} does not contain target/oliphaunt-wasix/icu/share/icu") - return root - - -def validate_runtime_payload(root: Path) -> None: - extension_files = sorted(path for path in (root / "extensions").rglob("*") if path.is_file()) if (root / "extensions").exists() else [] - if extension_files: - fail("WASIX runtime Cargo payload must not contain extension archives: " + ", ".join(rel(path) for path in extension_files[:5])) - 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 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", - "prepopulated/pgdata-template.tar.zst", - "prepopulated/pgdata-template.json", - ]: - 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 - if member == "oliphaunt/share/icu" or member.startswith("oliphaunt/share/icu/") - ] - if bundled_icu: - fail( - "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 FORBIDDEN_RUNTIME_ARCHIVE_TOOL_FILES - ) - if bundled_tools: - fail( - "WASIX runtime Cargo payload must not bundle standalone tools inside " - f"oliphaunt.wasix.tar.zst; found {bundled_tools[0]}" - ) - - -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 FORBIDDEN_RUNTIME_ARCHIVE_TOOL_FILES for member in runtime_members): - return - - extract_tar_zstd(archive, scratch) - for member in FORBIDDEN_RUNTIME_ARCHIVE_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 - 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_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 - - -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: - if not root.is_dir(): - return False - for child in sorted(root.iterdir()): - name = child.name - if child.is_file() and name.startswith("icudt") and name.endswith(".dat"): - return True - if child.is_dir() and name.startswith("icudt") and any(path.is_file() for path in child.rglob("*")): - return True - return False - - -def canonical_icu_root(root: Path) -> Path: - if icu_root_contains_data(root): - return root - candidates = [child for child in sorted(root.iterdir()) if child.is_dir() and icu_root_contains_data(child)] - if len(candidates) != 1: - fail(f"{rel(root)} must contain exactly one ICU data directory, found {len(candidates)}") - return candidates[0] - - -def validate_icu_payload(root: Path) -> None: - if not icu_root_contains_data(root): - 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") - if not isinstance(artifacts, list) or not artifacts: - fail(f"{rel(root / 'manifest.json')} must contain AOT artifacts") - expected = {"manifest.json"} - for artifact in artifacts: - name = artifact.get("name") - path = artifact.get("path") - if not isinstance(name, str) or not name: - fail(f"{rel(root / 'manifest.json')} contains an artifact without a name") - if name.startswith("extension:"): - fail(f"WASIX AOT Cargo payload must not contain extension artifact {name}") - if not isinstance(path, str) or not path: - fail(f"AOT artifact {name} is missing path") - checked = PurePosixPath(path) - if checked.is_absolute() or any(part in {"", ".", ".."} for part in checked.parts): - fail(f"AOT artifact {name} path must be simple relative path, got {path!r}") - if not (root / path).is_file(): - fail(f"AOT artifact {name} file is missing: {rel(root / path)}") - expected.add(path) - actual = {path.relative_to(root).as_posix() for path in payload_files(root)} - if actual != expected: - 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 = "Wasmer AOT pg_dump and psql artifacts for oliphaunt-wasix 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, - *, - 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)^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: - 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") - package = cargo_metadata_package(manifest) - if package["name"] != package_name or package["version"] != version: - fail( - f"{rel(manifest)} generated the wrong package metadata: " - f"name={package['name']!r}, version={package['version']!r}" - ) - - -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(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}") - 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)}") - shutil.copytree( - spec.template_dir, - 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", - package_name=spec.name, - version=version, - extension_sources=extension_sources, - extension_aot_sources=extension_aot_sources, - ) - return crate_dir - - -def cargo_metadata_package(manifest: Path) -> dict[str, object]: - result = subprocess.run( - ["cargo", "metadata", "--no-deps", "--format-version", "1", "--manifest-path", str(manifest)], - cwd=ROOT, - text=True, - capture_output=True, - check=False, - ) - if result.returncode != 0: - fail(f"cargo metadata failed for {rel(manifest)}: {result.stderr.strip()}") - data = json.loads(result.stdout) - packages = data.get("packages") - if not isinstance(packages, list) or len(packages) != 1: - fail(f"cargo metadata for {rel(manifest)} did not return exactly one package") - package = packages[0] - if not isinstance(package, dict): - fail(f"cargo metadata for {rel(manifest)} returned an invalid package") - return package - - -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"] - version = package["version"] - command = [ - "cargo", - "package", - "--manifest-path", - str(manifest), - "--target-dir", - 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" - if not crate_path.is_file(): - fail(f"cargo package did not create {rel(crate_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: - fail( - f"{rel(crate_path)} is {size} bytes, above the crates.io 10 MiB package limit; " - "reduce the WASIX Cargo payload before publishing" - ) - - -def package_spec( - spec: PackageSpec, - *, - version: str, - 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, 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) - return GeneratedPackage( - name=spec.name, - manifest_path=crate_dir / "Cargo.toml", - crate_path=output, - target=spec.target, - kind=spec.kind, - size=output.stat().st_size, - sha256=sha256_file(output), - ) - - -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 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: - 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=wasix_extension_aot_package_name(product, 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") - 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) - if archive is None: - continue - specs.append( - ExtensionCargoSpec( - name=wasix_extension_package_name(str(product)), - product=str(product), - version=str(version), - sql_name=str(sql_name), - 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), - version=str(version), - sql_name=str(sql_name), - ), - ) - ) - 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(): - 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) - 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.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' - ) - 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"', - "", - *( - [ - "[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( - [ - "#![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", - artifact_bytes_body, - " None\n" if part_sources else "", - "}\n", - ] - ), - encoding="utf-8", - ) - return ExtensionAotCargoSource(spec=spec, source_dir=crate_dir, part_sources=tuple(part_sources)) - - -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, -) -> 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) - 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]: - specs: list[PackageSpec] = [] - runtime_archive = asset_dir / f"liboliphaunt-wasix-{version}-runtime-portable.tar.zst" - if not runtime_archive.is_file(): - fail(f"missing WASIX portable runtime release asset: {rel(runtime_archive)}") - runtime_extract = extract_root / "runtime-extracted" - extract_tar_zstd(runtime_archive, runtime_extract) - runtime_root = target_asset_root(runtime_extract) - 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, - target="portable", - kind="wasix-runtime", - template_dir=ROOT / "src/runtimes/liboliphaunt/wasix/crates/assets", - 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", - ) - ) - icu_archive = asset_dir / f"liboliphaunt-wasix-{version}-icu-data.tar.zst" - if not icu_archive.is_file(): - fail(f"missing WASIX ICU data release asset: {rel(icu_archive)}") - icu_extract = extract_root / "icu-extracted" - 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_payload_root, - payload_dir_name="payload", - ) - ) - - for target_id, package_name in sorted(AOT_PACKAGES.items()): - archive = asset_dir / f"liboliphaunt-wasix-{version}-runtime-aot-{target_id}.tar.zst" - if not archive.is_file(): - fail(f"missing WASIX AOT release asset: {rel(archive)}") - extracted = extract_root / f"{target_id}-extracted" - extract_tar_zstd(archive, extracted) - 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_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", - ) - ) - return specs - - -def write_packages_manifest(packages: list[GeneratedPackage], output_dir: Path) -> None: - data = { - "schema": SCHEMA, - "product": PRODUCT, - "packages": [ - { - "name": package.name, - "target": package.target, - "kind": package.kind, - "role": "artifact", - "manifestPath": rel(package.manifest_path), - "cratePath": rel(package.crate_path), - "size": package.size, - "sha256": package.sha256, - } - for package in packages - ], - } - (output_dir / "packages.json").write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") - - -def parse_args(argv: list[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--asset-dir", - default="target/oliphaunt-wasix/release-assets", - help="directory containing checked liboliphaunt-wasix release assets", - ) - parser.add_argument( - "--output-dir", - default="target/oliphaunt-wasix/cargo-artifacts", - help="directory where generated .crate files are written", - ) - parser.add_argument("--version", default=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) - - -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 - 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)}") - - source_root = ROOT / "target/oliphaunt-wasix/cargo-package-sources" - extract_root = ROOT / "target/oliphaunt-wasix/cargo-package-extracted" - cargo_target_dir = ROOT / "target/oliphaunt-wasix/cargo-package-target" - shutil.rmtree(source_root, ignore_errors=True) - shutil.rmtree(extract_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) - extract_root.mkdir(parents=True, exist_ok=True) - 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 - ] - 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 - for source in extension_aot_sources - for package in package_extension_aot_source( - source, - output_dir=output_dir, - cargo_target_dir=cargo_target_dir, - ) - ], - *[ - 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:") - for package in packages: - print(f"{package.name} {rel(package.crate_path)} {package.size} bytes") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) diff --git a/tools/release/release.py b/tools/release/release.py index 3c5268a0..e76b52eb 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -3285,8 +3285,8 @@ def liboliphaunt_wasix_cargo_artifact_crates(version: str) -> list[tuple[str, Pa output_dir = ROOT / "target" / "oliphaunt-wasix" / "cargo-artifacts" run( [ - "python3", - "tools/release/package_liboliphaunt_wasix_cargo_artifacts.py", + "tools/dev/bun.sh", + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", "--version", version, "--output-dir", From 125c47f0c633bf3013b72d4a5d78513a77e2f767 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 22:19:22 +0000 Subject: [PATCH 230/308] chore: port release policy check to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 16 +- moon.yml | 6 +- src/shared/contracts/test-matrix.toml | 2 +- tools/policy/check-release-policy.mjs | 1757 +++++++++++++++++ tools/policy/check-release-policy.py | 1627 --------------- tools/policy/python-entrypoints.allowlist | 1 - tools/release/check_release_metadata.py | 6 +- tools/release/release.py | 2 +- 8 files changed, 1773 insertions(+), 1644 deletions(-) create mode 100644 tools/policy/check-release-policy.mjs delete mode 100644 tools/policy/check-release-policy.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 32aafe44..bc8e24f7 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -2182,14 +2182,14 @@ until the current-state gates here are checked with fresh local evidence. - 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`. - The current inventory contains 7 tracked Python files: release orchestration, - release/package validators, local registry publishing, release policy checks, - and the extension model generator. New Python files must either be - intentionally allowlisted or ported to Bun. The current migration order is: - 1. port release checkers in the release-graph cluster - (`check-release-policy.py`, `check_artifact_targets.py`, - `check_release_metadata.py`, `check_consumer_shape.py`) behind parity - smokes and then remove their Python compatibility imports; + The current inventory contains 6 tracked Python files: release orchestration, + release/package validators, local registry publishing, and the extension + model generator. New Python files must either be intentionally allowlisted or + ported to Bun. The current migration order is: + 1. port the remaining release checkers in the release-graph cluster + (`check_artifact_targets.py`, `check_release_metadata.py`, + `check_consumer_shape.py`) behind parity smokes and then remove their + Python compatibility imports; 2. port `local_registry_publish.py` after artifact package generation and release metadata are Bun-native, preserving the local registry e2e path; 3. port `release.py` last, when the underlying validators and registry helpers diff --git a/moon.yml b/moon.yml index ec34c3d3..6d24b239 100644 --- a/moon.yml +++ b/moon.yml @@ -94,7 +94,7 @@ tasks: - "/docs/architecture/final-product-source-architecture.md" - "/docs/maintainers/tooling.md" - "/tools/policy/assertions/assert-ci-workflows.mjs" - - "/tools/policy/check-release-policy.py" + - "/tools/policy/check-release-policy.mjs" - "/tools/release/**/*" options: cache: true @@ -114,7 +114,7 @@ tasks: runFromWorkspaceRoot: true release-policy: tags: ["policy", "assertion", "quality", "static"] - command: "python3 tools/policy/check-release-policy.py" + command: "tools/dev/bun.sh tools/policy/check-release-policy.mjs" inputs: - "/.github/**/*" - "/.moon/workspace.yml" @@ -138,7 +138,7 @@ tasks: - "!/src/**/Pods/**" - "!/src/**/DerivedData/**" - "/tools/release/**/*" - - "/tools/policy/check-release-policy.py" + - "/tools/policy/check-release-policy.mjs" options: cache: true runFromWorkspaceRoot: true diff --git a/src/shared/contracts/test-matrix.toml b/src/shared/contracts/test-matrix.toml index b19bec04..d8c3d1e8 100644 --- a/src/shared/contracts/test-matrix.toml +++ b/src/shared/contracts/test-matrix.toml @@ -245,6 +245,6 @@ non_consumers = [ "oliphaunt-wasix-rust", ] evidence = [ - { consumer = "policy-tools", kind = "fixture-file", path = "tools/policy/check-release-policy.py", markers = ["src/shared/fixtures/consumer-shape/products.json"] }, + { consumer = "policy-tools", kind = "fixture-file", path = "tools/policy/check-release-policy.mjs", markers = ["src/shared/fixtures/consumer-shape/products.json"] }, { consumer = "release-tools", kind = "fixture-file", path = "tools/release/check_consumer_shape.py", markers = ["src/shared/fixtures/consumer-shape/products.json"] }, ] diff --git a/tools/policy/check-release-policy.mjs b/tools/policy/check-release-policy.mjs new file mode 100644 index 00000000..0adda3c0 --- /dev/null +++ b/tools/policy/check-release-policy.mjs @@ -0,0 +1,1757 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; +import { existsSync, readFileSync, statSync } from "node:fs"; +import path from "node:path"; + +const ROOT = path.resolve(import.meta.dir, "../.."); + +const BASE_PRODUCTS = new Set([ + "liboliphaunt-native", + "liboliphaunt-wasix", + "oliphaunt-rust", + "oliphaunt-broker", + "oliphaunt-node-direct", + "oliphaunt-swift", + "oliphaunt-kotlin", + "oliphaunt-react-native", + "oliphaunt-js", + "oliphaunt-wasix-rust", +]); +const CONSUMER_SHAPE_PRODUCTS_FIXTURE = "src/shared/fixtures/consumer-shape/products.json"; + +function fail(message) { + console.error(message); + process.exit(1); +} + +function isObject(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function sorted(values) { + return [...values].sort(); +} + +function formatList(values) { + return JSON.stringify(sorted(values)); +} + +function union(...sets) { + const result = new Set(); + for (const set of sets) { + for (const value of set) { + result.add(value); + } + } + return result; +} + +function difference(left, right) { + return new Set([...left].filter((value) => !right.has(value))); +} + +function intersection(left, right) { + return new Set([...left].filter((value) => right.has(value))); +} + +function isSubset(left, right) { + for (const value of left) { + if (!right.has(value)) { + return false; + } + } + return true; +} + +function setEquals(left, right) { + return left.size === right.size && isSubset(left, right); +} + +function bunJson(args) { + const result = spawnSync("tools/dev/bun.sh", args, { + cwd: ROOT, + encoding: "utf8", + maxBuffer: 100 * 1024 * 1024, + }); + if (result.status !== 0) { + const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim(); + throw new Error(output || `tools/dev/bun.sh ${args.join(" ")} failed`); + } + try { + return JSON.parse(result.stdout); + } catch (error) { + throw new Error(`tools/dev/bun.sh ${args.join(" ")} did not return JSON: ${error.message}`); + } +} + +function stringSet(value, label) { + if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) { + fail(`${label} must be a JSON string list`); + } + return new Set(value); +} + +function optionalStringSet(value, label) { + if (value === null || value === undefined) { + return null; + } + return stringSet(value, label); +} + +function jsonFlag(value) { + if (value === null || value === undefined) { + return "null"; + } + return JSON.stringify(sorted(value)); +} + +class CiPlanClient { + constructor() { + const config = bunJson(["tools/graph/ci_plan.mjs", "config"]); + if (!isObject(config)) { + fail("CI planner config query must return an object"); + } + this.BASE_JOBS = stringSet(config.baseJobs, "baseJobs"); + this.BUILDER_JOBS = stringSet(config.builderJobs, "builderJobs"); + const targets = config.ciJobTargets; + if (!isObject(targets)) { + fail("ciJobTargets must be an object"); + } + this.CI_JOB_TARGETS = {}; + for (const [job, jobTargets] of Object.entries(targets)) { + this.CI_JOB_TARGETS[job] = sorted(stringSet(jobTargets, `ciJobTargets.${job}`)); + } + } + + query(...args) { + return bunJson(["tools/graph/ci_plan.mjs", ...args]); + } + + planJobsForAffected(directProjects, tasks) { + return stringSet( + this.query( + "jobs-for-affected", + "--direct-projects-json", + jsonFlag(directProjects), + "--tasks-json", + jsonFlag(tasks), + ), + "jobs-for-affected", + ); + } + + nativeTargetSubsetForJobs(jobs, tasks) { + return optionalStringSet( + this.query( + "native-target-subset", + "--jobs-json", + jsonFlag(jobs), + "--tasks-json", + jsonFlag(tasks), + ), + "native-target-subset", + ); + } + + selectedExtensionProductsForPlan(directProjects, tasks, jobs) { + return optionalStringSet( + this.query( + "selected-extension-products", + "--direct-projects-json", + jsonFlag(directProjects), + "--tasks-json", + jsonFlag(tasks), + "--jobs-json", + jsonFlag(jobs), + ), + "selected-extension-products", + ); + } + + planForFullRun({ wasmTarget = "all", nativeTarget = "all", mobileTarget = "all" } = {}) { + const value = this.query( + "plan-full", + "--wasm-target", + wasmTarget, + "--native-target", + nativeTarget, + "--mobile-target", + mobileTarget, + ); + if (!isObject(value)) { + fail("plan-full must return an object"); + } + if (typeof value.reason !== "string") { + fail("plan-full reason must be a string"); + } + return { + jobs: stringSet(value.jobs, "plan-full.jobs"), + projects: stringSet(value.projects, "plan-full.projects"), + tasks: stringSet(value.tasks, "plan-full.tasks"), + reason: value.reason, + selectedTargets: optionalStringSet(value.selectedTargets, "plan-full.selectedTargets"), + }; + } + + mobileExtensionPackageNativeTargets(jobs, selectedTargets) { + return sorted( + stringSet( + this.query( + "mobile-extension-package-native-targets", + "--jobs-json", + jsonFlag(jobs), + "--selected-targets-json", + jsonFlag(selectedTargets), + ), + "mobile-extension-package-native-targets", + ), + ); + } + + extensionArtifactsNativeMatrix(nativeTarget, selectedTargets, selectedProducts = null) { + const value = this.query( + "matrix", + "extension-artifacts-native", + "--native-target", + nativeTarget, + "--selected-targets-json", + jsonFlag(selectedTargets), + "--selected-products-json", + jsonFlag(selectedProducts), + ); + if (!isObject(value)) { + fail("extension-artifacts-native matrix must be an object"); + } + return value; + } + + extensionArtifactsWasixMatrix(wasmTarget, selectedProducts = null) { + const value = this.query( + "matrix", + "extension-artifacts-wasix", + "--wasm-target", + wasmTarget, + "--selected-products-json", + jsonFlag(selectedProducts), + ); + if (!isObject(value)) { + fail("extension-artifacts-wasix matrix must be an object"); + } + return value; + } +} + +const ciPlan = new CiPlanClient(); + +function readText(repoPath) { + return readFileSync(path.join(ROOT, repoPath), "utf8"); +} + +function assertDirectReleasePythonToolsAreExecutable(releaseScript) { + const directInvocations = new Set(); + for (const match of releaseScript.matchAll(/\[\s*"([^"]+\.py)"/gm)) { + if (match[1].startsWith("tools/release/")) { + directInvocations.add(match[1]); + } + } + for (const tool of sorted(directInvocations)) { + const file = path.join(ROOT, tool); + if (!existsSync(file) || !statSync(file).isFile()) { + fail(`directly invoked release tool does not exist: ${tool}`); + } + if ((statSync(file).mode & 0o111) === 0) { + fail(`directly invoked release tool must be executable or called through python3: ${tool}`); + } + } +} + +function readToml(repoPath) { + const file = path.isAbsolute(repoPath) ? repoPath : path.join(ROOT, repoPath); + const value = Bun.TOML.parse(readFileSync(file, "utf8")); + if (!isObject(value)) { + fail(`${path.relative(ROOT, file)} must contain a TOML table`); + } + return value; +} + +function releaseGraph() { + const value = bunJson(["tools/release/release_graph_query.mjs", "graph"]); + if (!isObject(value)) { + fail("release graph query did not return an object"); + } + return value; +} + +function releaseProductProjects() { + const value = bunJson(["tools/release/release_graph_query.mjs", "product-projects"]); + if (!isObject(value) || !Object.entries(value).every(([key, item]) => typeof key === "string" && typeof item === "string")) { + fail("release graph product-project query did not return a string map"); + } + return value; +} + +function releaseProductConfigs() { + const value = bunJson(["tools/release/release_graph_query.mjs", "product-configs"]); + if (!Array.isArray(value) || !value.every(isObject)) { + fail("release graph product-configs query did not return an object list"); + } + const rows = {}; + for (const row of value) { + const product = row.product; + const configId = row.id; + if (typeof product !== "string" || product.length === 0) { + fail("release graph product-configs rows must declare non-empty products"); + } + if (rows[product] !== undefined) { + fail(`release graph product-configs query returned duplicate product ${product}`); + } + if (configId !== product) { + fail(`release graph product-configs ${product}.id must match the product id`); + } + for (const key of ["kind", "owner", "path", "changelog_path", "tag_prefix"]) { + if (typeof row[key] !== "string" || row[key].length === 0) { + fail(`release graph product-configs ${product}.${key} must be a non-empty string`); + } + } + for (const key of ["publish_targets", "release_artifacts", "version_files"]) { + if (!Array.isArray(row[key]) || row[key].length === 0 || !row[key].every((item) => typeof item === "string" && item.length > 0)) { + fail(`release graph product-configs ${product}.${key} must be a non-empty string list`); + } + } + rows[product] = row; + } + if (Object.keys(rows).length === 0) { + fail("release graph returned no product configs"); + } + return rows; +} + +function moonProjectRows() { + const value = bunJson(["tools/release/release_graph_query.mjs", "moon-projects"]); + if (!Array.isArray(value) || !value.every(isObject)) { + fail("release graph moon-projects query did not return an object list"); + } + const rows = {}; + for (const row of value) { + const projectId = row.id; + if (typeof projectId !== "string" || projectId.length === 0) { + fail("release graph moon-projects rows must declare non-empty ids"); + } + if (rows[projectId] !== undefined) { + fail(`release graph moon-projects query returned duplicate project ${projectId}`); + } + const tags = row.tags; + const dependencyScopes = row.dependencyScopes; + if (!Array.isArray(tags) || !tags.every((item) => typeof item === "string")) { + fail(`release graph moon-projects ${projectId}.tags must be a string list`); + } + if (!isObject(dependencyScopes) || !Object.entries(dependencyScopes).every(([key, item]) => typeof key === "string" && typeof item === "string")) { + fail(`release graph moon-projects ${projectId}.dependencyScopes must be a string map`); + } + rows[projectId] = row; + } + return rows; +} + +function extensionMetadataRows() { + const value = bunJson(["tools/release/release_graph_query.mjs", "extension-metadata"]); + if (!Array.isArray(value) || !value.every(isObject)) { + fail("release graph extension-metadata query did not return an object list"); + } + const rows = {}; + for (const row of value) { + const product = row.product; + if (typeof product !== "string" || product.length === 0) { + fail("release graph extension-metadata rows must declare non-empty products"); + } + if (rows[product] !== undefined) { + fail(`release graph extension-metadata query returned duplicate product ${product}`); + } + for (const key of ["sqlName", "class", "versioning", "sourcePath"]) { + if (typeof row[key] !== "string" || row[key].length === 0) { + fail(`release graph extension-metadata ${product}.${key} must be a non-empty string`); + } + } + if (!isObject(row.compatibility)) { + fail(`release graph extension-metadata ${product}.compatibility must be an object`); + } + rows[product] = row; + } + if (Object.keys(rows).length === 0) { + fail("release graph returned no extension metadata rows"); + } + return rows; +} + +function extensionProductIds() { + return Object.keys(extensionMetadataRows()).sort(); +} + +function artifactTargetRows({ product, kind, publishedOnly }) { + const args = [ + "tools/release/release_graph_query.mjs", + "artifact-targets", + "--product", + product, + "--kind", + kind, + ]; + if (publishedOnly) { + args.push("--published-only"); + } + const value = bunJson(args); + if (!Array.isArray(value) || !value.every(isObject)) { + fail("release graph artifact-targets query did not return an object list"); + } + for (const row of value) { + const targetId = row.id; + if (typeof targetId !== "string" || targetId.length === 0) { + fail("release graph artifact-targets rows must declare non-empty ids"); + } + if (row.product !== product || row.kind !== kind) { + fail(`release graph artifact-targets returned unexpected row ${targetId}`); + } + if (typeof row.target !== "string" || row.target.length === 0) { + fail(`release graph artifact-targets ${targetId}.target must be a non-empty string`); + } + if (typeof (row.extension_artifacts ?? true) !== "boolean") { + fail(`release graph artifact-targets ${targetId}.extension_artifacts must be true or false`); + } + } + return value; +} + +function releasePlansForSinglePaths(paths) { + const value = bunJson([ + "tools/release/release_graph_query.mjs", + "plans-for-paths", + "--paths-json", + JSON.stringify(paths), + ]); + if (!isObject(value) || !Object.entries(value).every(([key, item]) => typeof key === "string" && isObject(item))) { + fail("release graph plans-for-paths query did not return a plan map"); + } + return value; +} + +function extensionProductId(sqlName) { + return `oliphaunt-extension-${sqlName.replaceAll("_", "-").toLowerCase()}`; +} + +function expectedExtensionProductsFromSdkCatalog() { + const data = JSON.parse(readText("src/extensions/generated/sdk/rust.json")); + const rows = data.extensions; + if (!Array.isArray(rows) || rows.length === 0) { + fail("generated Rust extension catalog must define public extensions"); + } + const products = new Set(); + for (const row of rows) { + if (!isObject(row)) { + fail("generated Rust extension catalog rows must be objects"); + } + const sqlName = row["sql-name"]; + if (typeof sqlName !== "string" || sqlName.length === 0) { + fail("generated Rust extension catalog rows must declare sql-name"); + } + products.add(extensionProductId(sqlName)); + } + return products; +} + +function expectedContribExtensionProductsFromManifest() { + const data = readToml("src/extensions/contrib/postgres18.toml"); + const rows = data.extensions; + if (!Array.isArray(rows) || rows.length === 0) { + fail("PostgreSQL contrib extension manifest must define extension rows"); + } + const products = new Set(); + for (const row of rows) { + if (!isObject(row)) { + fail("PostgreSQL contrib extension manifest rows must be tables"); + } + const sqlName = row["sql-name"]; + if (typeof sqlName !== "string" || sqlName.length === 0) { + fail("PostgreSQL contrib extension manifest rows must declare sql-name"); + } + products.add(extensionProductId(sqlName)); + } + return products; +} + +function expectedProducts() { + return union(BASE_PRODUCTS, expectedExtensionProductsFromSdkCatalog()); +} + +function projectReleaseMetadata(project) { + return isObject(project.release) ? project.release : null; +} + +function projectDependencyScopes(project) { + return isObject(project.dependencyScopes) ? { ...project.dependencyScopes } : {}; +} + +function assertNoFile(repoPath) { + if (existsSync(path.join(ROOT, repoPath))) { + fail(`${repoPath} must not exist; Moon is the only dependency/affectedness graph`); + } +} + +function assertContains(repoPath, snippet, message) { + if (!readText(repoPath).includes(snippet)) { + fail(message); + } +} + +function assertNotContains(repoPath, snippet, message) { + if (readText(repoPath).includes(snippet)) { + fail(message); + } +} + +function workflowJobBlocks(repoPath) { + const text = readText(repoPath); + const jobsSection = text.includes("\njobs:\n") ? text.split("\njobs:\n", 2)[1] : ""; + if (!jobsSection) { + fail(`${repoPath} must declare a jobs section`); + } + const matches = [...jobsSection.matchAll(/^ ([A-Za-z0-9_-]+):\n/gm)]; + if (matches.length === 0) { + fail(`${repoPath} parser found no jobs`); + } + const blocks = {}; + for (const [index, match] of matches.entries()) { + const end = index + 1 < matches.length ? matches[index + 1].index : jobsSection.length; + blocks[match[1]] = jobsSection.slice(match.index, end); + } + return blocks; +} + +function workflowStepBlocks(jobBlock) { + const matches = [...jobBlock.matchAll(/^ - name: (.+)\n/gm)]; + const blocks = {}; + for (const [index, match] of matches.entries()) { + const end = index + 1 < matches.length ? matches[index + 1].index : jobBlock.length; + blocks[match[1].trim()] = jobBlock.slice(match.index, end); + } + return blocks; +} + +function workflowJobNeeds(blocks, job) { + const block = blocks[job]; + if (block === undefined) { + fail(`CI workflow is missing job ${job}`); + } + const match = block.match(/^ needs:\n(?(?: - [A-Za-z0-9_-]+\n)+)/ms); + if (match === null) { + return new Set(); + } + return new Set( + match.groups.body + .split(/\r?\n/u) + .map((line) => line.replace(/^ - /u, "").trim()) + .filter(Boolean), + ); +} + +function assertJobContains(blocks, job, snippet, message) { + const block = blocks[job]; + if (block === undefined) { + fail(`CI workflow is missing job ${job}`); + } + if (!block.includes(snippet)) { + fail(message); + } +} + +function assertStepContains(steps, step, snippet, message) { + const block = steps[step]; + if (block === undefined) { + fail(`workflow is missing step ${JSON.stringify(step)}`); + } + if (!block.includes(snippet)) { + fail(message); + } +} + +function assertStepIfContainsPublishGuard(steps, step) { + const block = steps[step]; + if (block === undefined) { + fail(`workflow is missing step ${JSON.stringify(step)}`); + } + if (!block.includes("inputs.operation == 'publish'")) { + fail(`${JSON.stringify(step)} must be guarded by inputs.operation == 'publish'`); + } +} + +function normalizedShell(text) { + return text.replace(/\s+/gu, " ").trim(); +} + +function assertTextOrder(text, snippets, message) { + let index = -1; + for (const snippet of snippets) { + const nextIndex = text.indexOf(snippet, index + 1); + if (nextIndex === -1) { + fail(`${message}: missing ${JSON.stringify(snippet)}`); + } + index = nextIndex; + } +} + +function checkReleaseMetadata() { + const products = releaseProductConfigs(); + if (!setEquals(new Set(Object.keys(products)), expectedProducts())) { + fail(`release product set mismatch: expected ${formatList(expectedProducts())}, got ${formatList(Object.keys(products))}`); + } + const extensionMetadata = extensionMetadataRows(); + const modeledExtensionProducts = new Set(extensionProductIds()); + const expectedExtensionProducts = expectedExtensionProductsFromSdkCatalog(); + if (!setEquals(modeledExtensionProducts, expectedExtensionProducts)) { + fail( + "exact-extension release products must match the public generated extension catalog: " + + `expected ${formatList(expectedExtensionProducts)}, got ${formatList(modeledExtensionProducts)}`, + ); + } + + const projects = moonProjectRows(); + const productProjects = releaseProductProjects(); + for (const [product, config] of Object.entries(products)) { + const releasePath = path.join(ROOT, config.path, "release.toml"); + const raw = readToml(releasePath); + for (const forbidden of ["depends_on", "source_globs", "package_visible_globs"]) { + if (Object.prototype.hasOwnProperty.call(raw, forbidden)) { + fail(`${path.relative(ROOT, releasePath)} must not declare ${forbidden}; Moon owns graph shape`); + } + } + for (const key of ["id", "owner", "kind", "publish_targets", "release_artifacts"]) { + if (!Object.prototype.hasOwnProperty.call(raw, key)) { + fail(`${path.relative(ROOT, releasePath)} must declare ${key}`); + } + } + if (!config.tag_prefix || !config.version_files || !config.changelog_path) { + fail(`${product} must have release-please tag/version/changelog metadata`); + } + + const projectId = productProjects[product]; + const project = projects[projectId]; + if (project === undefined) { + fail(`${product} has no owning Moon project`); + } + const tags = new Set(project.tags ?? []); + if (!tags.has("release-product")) { + fail(`${projectId} must be tagged release-product`); + } + const release = projectReleaseMetadata(project); + if (release === null) { + fail(`${projectId} must declare project.release metadata`); + } + if (release.component !== product) { + fail(`${projectId} release component expected ${product}, got ${release.component}`); + } + if (release.packagePath !== config.path) { + fail(`${projectId} packagePath expected ${config.path}, got ${release.packagePath}`); + } + if (config.kind === "exact-extension-artifact") { + if (extensionMetadata[product] === undefined) { + fail(`${product} exact-extension product is missing release graph extension metadata`); + } + if (project.layer !== "library") { + fail(`${projectId} must be a library layer project; exact extension artifacts are publishable runtime-compatible products`); + } + const scopes = projectDependencyScopes(project); + for (const dependency of ["extension-runtime-contract", "liboliphaunt-native", "liboliphaunt-wasix"]) { + if (scopes[dependency] !== "production") { + fail(`${projectId} must declare a production Moon dependency on ${dependency}`); + } + } + } + } + + const extensionModel = projects["extension-model"]; + if (extensionModel === undefined) { + fail("extension-model project is missing"); + } + if (Object.prototype.hasOwnProperty.call(projectDependencyScopes(extensionModel), "extensions")) { + fail("extension-model must not depend on the aggregate extensions project; exact extension runtime deps must remain acyclic"); + } +} + +function checkReleasePlanning() { + const allExtensionProducts = expectedExtensionProductsFromSdkCatalog(); + const contribExtensionProducts = expectedContribExtensionProductsFromManifest(); + const containsCases = new Map([ + ["src/shared/js-core/src/query.ts", new Set(["oliphaunt-js", "oliphaunt-react-native"])], + [ + "src/postgres/versions/18/source.toml", + union( + new Set([ + "liboliphaunt-native", + "liboliphaunt-wasix", + "oliphaunt-rust", + "oliphaunt-swift", + "oliphaunt-kotlin", + "oliphaunt-react-native", + "oliphaunt-js", + "oliphaunt-wasix-rust", + ]), + contribExtensionProducts, + ), + ], + ["src/extensions/contrib/postgres18.toml", contribExtensionProducts], + [ + "src/shared/extension-runtime-contract/contract.toml", + union(new Set(["liboliphaunt-native", "liboliphaunt-wasix"]), allExtensionProducts), + ], + ["src/runtimes/liboliphaunt/native/VERSION", union(new Set(["liboliphaunt-native"]), allExtensionProducts)], + ["src/runtimes/liboliphaunt/wasix/VERSION", union(new Set(["liboliphaunt-wasix"]), allExtensionProducts)], + ]); + const exactCases = new Map([ + ["src/extensions/contrib/amcheck/release.toml", new Set(["oliphaunt-extension-amcheck"])], + ["src/extensions/external/vector/source.toml", new Set(["oliphaunt-extension-vector"])], + ["src/shared/fixtures/protocol/query-response-cases.json", new Set()], + ["docs/maintainers/release.md", new Set()], + ]); + const plans = releasePlansForSinglePaths(sorted(new Set([...containsCases.keys(), ...exactCases.keys()]))); + for (const [repoPath, expected] of containsCases.entries()) { + const actual = new Set(plans[repoPath]?.releaseProducts ?? []); + if (!isSubset(expected, actual)) { + fail(`${repoPath} release plan expected at least ${formatList(expected)}, got ${formatList(actual)}`); + } + } + for (const [repoPath, expected] of exactCases.entries()) { + const actual = new Set(plans[repoPath]?.releaseProducts ?? []); + if (!setEquals(actual, expected)) { + fail(`${repoPath} release plan expected exactly ${formatList(expected)}, got ${formatList(actual)}`); + } + } +} + +function checkCiPolicy() { + assertNoFile("tools/graph/jobs.toml"); + assertNoFile("tools/release/release-inputs.toml"); + const ci = readText(".github/workflows/ci.yml"); + for (const forbidden of ["targets=(", "tools/graph/jobs.toml", "tools/release/release-inputs.toml"]) { + if (ci.includes(forbidden)) { + fail(`CI workflow must not contain ${forbidden}`); + } + } + assertContains("tools/graph/ci_plan.mjs", "moon([\"query\", \"tasks\"])", "CI planner must read Moon task tags"); + assertContains("tools/graph/ci_plan.mjs", "ci-", "CI planner must document ci-* task tags"); + assertContains( + "tools/graph/ci_plan.mjs", + "extension_package_products_csv", + "CI planner must emit selected exact-extension products for artifact package builders", + ); + assertContains( + ".github/workflows/ci.yml", + "OLIPHAUNT_EXTENSION_PACKAGE_PRODUCTS", + "CI extension package builders must consume selected exact-extension products from the affected plan", + ); + assertContains( + "tools/release/build-extension-ci-artifacts.mjs", + "OLIPHAUNT_EXTENSION_PACKAGE_PRODUCTS", + "exact-extension package builder must support selected product subsets", + ); + assertContains( + ".github/scripts/select-planned-moon-targets.mjs", + "OLIPHAUNT_CI_JOB_TARGETS_JSON", + "CI product jobs must consume planned Moon targets through the Bun selector", + ); + if (Object.keys(ciPlan.CI_JOB_TARGETS).length === 0) { + fail("CI planner found no Moon ci-* task tags"); + } + if (ciPlan.BUILDER_JOBS.has("liboliphaunt-wasix-aot-targets")) { + fail("builder_jobs must contain artifact-producing jobs, not the WASIX AOT target planner"); + } + + const workflowBlocks = workflowJobBlocks(".github/workflows/ci.yml"); + const workflowJobs = new Set(Object.keys(workflowBlocks)); + if (workflowJobs.size === 0) { + fail("CI workflow parser found no jobs"); + } + const moonJobs = new Set(Object.keys(ciPlan.CI_JOB_TARGETS)); + const builderMoonJobs = intersection(moonJobs, ciPlan.BUILDER_JOBS); + const noMoonTargetJobs = new Set([ + "affected", + "check-targets", + "policy-targets", + "release-intent", + "checks", + "test-targets", + "tests", + "builds", + "mobile-e2e-android", + "mobile-e2e-ios", + "e2e", + "required", + ]); + const allowedWorkflowJobs = union(builderMoonJobs, noMoonTargetJobs); + const missingWorkflowJobs = sorted(difference(ciPlan.BUILDER_JOBS, workflowJobs)); + if (missingWorkflowJobs.length > 0) { + fail(`builder Moon ci-* tags have no CI workflow job: ${JSON.stringify(missingWorkflowJobs)}`); + } + const untaggedWorkflowJobs = sorted(difference(workflowJobs, allowedWorkflowJobs)); + if (untaggedWorkflowJobs.length > 0) { + fail(`CI workflow must only define phase gates, builder jobs, and aggregate exceptions: ${JSON.stringify(untaggedWorkflowJobs)}`); + } + const nonBuilderWorkflowJobs = sorted(intersection(difference(moonJobs, ciPlan.BUILDER_JOBS), workflowJobs)); + if (nonBuilderWorkflowJobs.length > 0) { + fail(`CI workflow must not define non-builder Moon jobs as dedicated artifact build jobs: ${JSON.stringify(nonBuilderWorkflowJobs)}`); + } + + const requiredMatch = ci.match(/^ required:\n.*?^ needs:\n(?(?: - [A-Za-z0-9_-]+\n)+)/ms); + if (requiredMatch === null) { + fail("CI workflow required job must declare a static needs list"); + } + const requiredNeeds = new Set( + requiredMatch.groups.body + .split(/\r?\n/u) + .map((line) => line.replace(/^ - /u, "").trim()) + .filter(Boolean), + ); + const expectedRequiredNeeds = new Set(["affected", "release-intent", "checks", "tests", "builds", "e2e"]); + if (!setEquals(requiredNeeds, expectedRequiredNeeds)) { + fail( + "required.needs must be the CI phase gates only: " + + "['affected', 'release-intent', 'checks', 'tests', 'builds', 'e2e']; " + + `got ${formatList(requiredNeeds)}`, + ); + } + + const buildsMatch = ci.match(/^ builds:\n.*?^ needs:\n(?(?: - [A-Za-z0-9_-]+\n)+)/ms); + if (buildsMatch === null) { + fail("CI workflow builds job must declare a static needs list"); + } + const buildsNeeds = new Set( + buildsMatch.groups.body + .split(/\r?\n/u) + .map((line) => line.replace(/^ - /u, "").trim()) + .filter(Boolean), + ); + const missingBuilders = sorted(difference(ciPlan.BUILDER_JOBS, buildsNeeds)); + if (missingBuilders.length > 0) { + fail(`builds.needs is missing builder jobs: ${JSON.stringify(missingBuilders)}`); + } + if (buildsNeeds.has("tests")) { + fail("builds.needs must not include the global Tests job; artifact builders must only wait on real artifact producers"); + } + + const plannedJobInvocations = new Set([...ci.matchAll(/run-planned-moon-job[.]sh ([A-Za-z0-9_-]+)/g)].map((match) => match[1])); + const missingPlannedInvocations = sorted(difference(builderMoonJobs, plannedJobInvocations)); + if (missingPlannedInvocations.length > 0) { + fail(`builder workflow jobs do not consume planned Moon targets: ${JSON.stringify(missingPlannedInvocations)}`); + } + for (const [index, line] of ci.split(/\r?\n/u).entries()) { + const match = line.match(/run-planned-moon-job[.]sh ([A-Za-z0-9_-]+)/); + if (match === null) { + continue; + } + const job = match[1]; + if (ciPlan.BUILDER_JOBS.has(job) && !line.includes("MOON_CACHE=off")) { + fail(`builder job ${job} must disable Moon cache in CI at .github/workflows/ci.yml:${index + 1}`); + } + const artifactConsumerJobs = new Set([ + "extension-artifacts-wasix", + "extension-packages", + "mobile-extension-packages", + "liboliphaunt-native-release-assets", + "liboliphaunt-wasix-aot", + "liboliphaunt-wasix-release-assets", + "mobile-build-android", + "mobile-build-ios", + ]); + if (artifactConsumerJobs.has(job) && !line.includes("OLIPHAUNT_MOON_UPSTREAM=none")) { + fail( + `artifact consumer job ${job} must not re-run upstream Moon artifact producers in CI at .github/workflows/ci.yml:${index + 1}`, + ); + } + if (difference(ciPlan.BUILDER_JOBS, artifactConsumerJobs).has(job) && line.includes("OLIPHAUNT_MOON_UPSTREAM=none")) { + fail(`builder job ${job} must allow Moon upstream task inheritance in CI at .github/workflows/ci.yml:${index + 1}`); + } + } + + const expectedMobileBuildNeeds = { + "mobile-build-android": new Set([ + "affected", + "mobile-extension-packages", + "liboliphaunt-native-android", + "kotlin-sdk-package", + "react-native-sdk-package", + ]), + "mobile-build-ios": new Set([ + "affected", + "mobile-extension-packages", + "liboliphaunt-native-ios", + "react-native-sdk-package", + "swift-sdk-package", + ]), + }; + for (const [job, expected] of Object.entries(expectedMobileBuildNeeds)) { + const actual = workflowJobNeeds(workflowBlocks, job); + if (!setEquals(actual, expected)) { + fail(`${job}.needs must consume staged runtime, SDK, and exact-extension builders: expected ${formatList(expected)}, got ${formatList(actual)}`); + } + for (const snippet of [ + 'OLIPHAUNT_EXPO_ALLOW_NATIVE_BUILDS: "0"', + 'OLIPHAUNT_EXPO_REQUIRE_SDK_ARTIFACTS: "1"', + 'OLIPHAUNT_EXPO_REQUIRE_PREBUILT_EXTENSIONS: "1"', + "OLIPHAUNT_EXPO_EXTENSION_ARTIFACT_ROOT:", + "oliphaunt-mobile-extension-package-artifacts", + "--require-mobile-prebuilt-extensions", + ]) { + assertJobContains(workflowBlocks, job, snippet, `${job} must use staged SDK/runtime/exact-extension artifacts and reject source-build fallbacks`); + } + } + assertJobContains( + workflowBlocks, + "mobile-build-android", + "OLIPHAUNT_EXPO_ANDROID_BUILD_TYPE: release", + "Android mobile app builder must publish the same release-mode artifact that installed-app E2E consumes", + ); + assertJobContains( + workflowBlocks, + "mobile-build-ios", + "OLIPHAUNT_EXPO_IOS_CONFIGURATION: Release", + "iOS mobile app builder must publish the same release-mode artifact that installed-app E2E consumes", + ); + assertJobContains( + workflowBlocks, + "mobile-build-ios", + "OLIPHAUNT_EXPO_IOS_SDK: iphonesimulator", + "iOS mobile app builder must publish a simulator artifact for free installed-app E2E", + ); + assertJobContains( + workflowBlocks, + "mobile-e2e-ios", + 'MAESTRO_DRIVER_STARTUP_TIMEOUT: "300000"', + "iOS installed-app E2E must give Maestro's XCTest driver enough startup time on macOS runners", + ); + + const androidBuild = workflowBlocks["mobile-build-android"]; + for (const snippet of [ + "matrix: ${{ fromJson(needs.affected.outputs.react_native_android_mobile_app_matrix) }}", + "liboliphaunt-native-target-${{ matrix.target }}", + "OLIPHAUNT_EXPO_ANDROID_ABI: ${{ matrix.abi }}", + "oliphaunt-kotlin-sdk-package-artifacts", + "oliphaunt-react-native-sdk-package-artifacts", + "react-native-mobile-android-app-${{ matrix.target }}", + ]) { + if (!androidBuild.includes(snippet)) { + fail(`mobile-build-android must download/upload ${snippet}`); + } + } + for (const [repoPath, snippet, message] of [ + [ + "src/sdks/react-native/android/build.gradle", + "OLIPHAUNT_ANDROID_LINK_EVIDENCE_FILE", + "React Native Android Gradle packaging must pass static-extension link evidence into CMake", + ], + [ + "src/sdks/react-native/android/src/main/cpp/CMakeLists.txt", + "oliphaunt-android-static-extension-link-v1", + "React Native Android CMake packaging must emit deterministic static-extension link evidence", + ], + [ + "src/sdks/react-native/tools/expo-android-runner.sh", + "androidLinkEvidence", + "React Native Android mobile build reports must include static-extension link evidence", + ], + [ + "tools/release/check-staged-artifacts.mjs", + "checkAndroidPrebuiltExtensionLinkage", + "staged mobile artifact checks must validate Android static-extension link evidence", + ], + ]) { + if (!readText(repoPath).includes(snippet)) { + fail(message); + } + } + + const iosBuild = workflowBlocks["mobile-build-ios"]; + for (const snippet of [ + "liboliphaunt-native-target-ios-xcframework", + "oliphaunt-swift-sdk-package-artifacts", + "oliphaunt-react-native-sdk-package-artifacts", + "react-native-mobile-ios-app", + ]) { + if (!iosBuild.includes(snippet)) { + fail(`mobile-build-ios must download/upload ${snippet}`); + } + } + + const wasixExtensionPackager = readText("src/extensions/artifacts/wasix/tools/package-release-assets.sh"); + if (wasixExtensionPackager.includes("--strict-generated")) { + fail("WASIX exact-extension packaging must consume portable runtime outputs; strict generation checks belong to the portable runtime builder"); + } + + const mobileE2e = readText(".github/workflows/mobile-e2e.yml"); + for (const snippet of [ + "name: E2E", + 'workflows: ["CI"]', + "BUILD_GATE_JOB: Builds", + 'bun .github/scripts/resolve-mobile-e2e.mjs', + 'bun .github/scripts/check-ci-gate.mjs allow-skipped', + "react-native-mobile-android-app-android-x86_64", + "react-native-mobile-ios-app", + "uses: ./.github/actions/setup-maestro", + "tools/dev/start-android-emulator-ci.sh", + "bash src/sdks/react-native/tools/mobile-e2e.sh android", + "bash src/sdks/react-native/tools/mobile-e2e.sh ios", + "OLIPHAUNT_EXPO_ANDROID_BUILD_TYPE: release", + "OLIPHAUNT_EXPO_IOS_CONFIGURATION: Release", + "OLIPHAUNT_EXPO_IOS_SDK: iphonesimulator", + 'MAESTRO_DRIVER_STARTUP_TIMEOUT: "300000"', + ]) { + if (!mobileE2e.includes(snippet)) { + fail(`E2E workflow must consume built app artifacts with pinned installed-app tooling: missing ${snippet}`); + } + } + for (const forbidden of [ + "run-planned-moon-job.sh", + "mobile-build:android", + "mobile-build:ios", + "tools/mobile-build.sh", + "OLIPHAUNT_EXPO_ALLOW_NATIVE_BUILDS", + ]) { + if (mobileE2e.includes(forbidden)) { + fail(`E2E workflow must not rebuild source artifacts or invoke builder tasks: ${forbidden}`); + } + } + + const releaseWorkflowBlocks = workflowJobBlocks(".github/workflows/release.yml"); + const releaseToolPatterns = ["tools/release/release.py", "tools/release/artifact_target_matrix.mjs"]; + const missingMoonSetup = sorted( + Object.entries(releaseWorkflowBlocks) + .filter(([, block]) => releaseToolPatterns.some((pattern) => block.includes(pattern)) && !block.includes("./.github/actions/setup-moon")) + .map(([job]) => job), + ); + if (missingMoonSetup.length > 0) { + fail(`release workflow jobs invoke release metadata without setup-moon: ${JSON.stringify(missingMoonSetup)}`); + } + + if (!existsSync(path.join(ROOT, CONSUMER_SHAPE_PRODUCTS_FIXTURE))) { + fail(`missing consumer shape fixture: ${CONSUMER_SHAPE_PRODUCTS_FIXTURE}`); + } + assertContains( + "tools/release/release.py", + "check_release_pr_coverage.mjs", + "release checks must verify release-please version bumps cover Moon-selected products", + ); + for (const repoPath of [ + ".github/workflows/release.yml", + "tools/release/release.py", + "tools/release/upload_github_release_assets.mjs", + ]) { + assertNotContains( + repoPath, + "replace_conflicting_assets", + "GitHub release asset replacement must stay a manual repair, not a release workflow switch", + ); + assertNotContains( + repoPath, + "replace-conflicting-assets", + "GitHub release asset replacement must stay a manual repair, not a release CLI switch", + ); + } + assertNotContains("tools/release/upload_github_release_assets.mjs", "--clobber", "GitHub release asset upload must not overwrite existing assets"); + assertContains( + "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", + ); +} + +function checkReleaseWorkflowPolicy() { + const releaseBlocks = workflowJobBlocks(".github/workflows/release.yml"); + const publishBlock = releaseBlocks.publish; + if (publishBlock === undefined) { + fail("Release workflow must define a publish job"); + } + const publishSteps = workflowStepBlocks(publishBlock); + + for (const permission of ["actions: read", "attestations: write", "contents: write", "id-token: write"]) { + if (!publishBlock.includes(permission)) { + fail(`Release publish job must declare ${permission}`); + } + } + const releaseWorkflow = readText(".github/workflows/release.yml"); + for (const snippet of [ + "release_commit:", + ".github/scripts/resolve-release-head.sh", + "id: release_head", + "RELEASE_HEAD_SHA", + "Create release-please target branch", + "target-branch: ${{ steps.release_head.outputs.target_branch }}", + "Remove release-please target branch", + 'tools/dev/bun.sh tools/release/release_plan.mjs --from-product-tags --include-current-tags --head-ref "$RELEASE_HEAD_SHA" --format github-output', + ]) { + if (!releaseWorkflow.includes(snippet)) { + fail(`Release workflow must resolve and publish from an explicit release commit: missing ${JSON.stringify(snippet)}`); + } + } + if (releaseWorkflow.includes("tools/release/release.py plan")) { + fail("Release workflow must call the Bun release plan entrypoint directly"); + } + for (const legacyReleaseQuery of ["tools/release/release.py ci-products", "tools/release/release.py ci-artifacts"]) { + if (releaseWorkflow.includes(legacyReleaseQuery)) { + fail("Release workflow must call Bun release graph queries for CI artifact handoffs"); + } + } + + assertTextOrder( + publishBlock, + [ + "Resolve release commit", + "Plan product releases", + "Require release-commit CI build gate", + "Download WASIX runtime build artifacts", + "Download WASIX release assets", + "Download exact-extension package artifacts", + "Download SDK package artifacts", + "Download liboliphaunt release assets", + "Install TypeScript release tooling", + "Download native helper release assets", + "Download Node direct optional npm packages", + "Validate selected release product dry-runs", + "Create release-please target branch", + "Create release-please GitHub releases", + "Remove release-please target branch", + "Publish liboliphaunt GitHub release assets", + ], + "Release publish must validate release-commit builder outputs before creating release tags", + ); + + for (const snippet of [ + "id: ci_build_gate", + 'require-workflow-success.sh CI "$RELEASE_HEAD_SHA" 7200 --job Builds', + "CI_RUN_ID: ${{ steps.ci_build_gate.outputs.run_id }}", + '--run-id "$CI_RUN_ID"', + '--run-id "${CI_RUN_ID}"', + "--job Builds", + "--artifact liboliphaunt-wasix-release-assets", + "--artifact oliphaunt-extension-package-artifacts", + "--artifact liboliphaunt-native-release-assets", + '--artifact "$artifact"', + "PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }}", + 'tools/dev/bun.sh tools/release/release_graph_query.mjs ci-products --family sdk-package --products-json "$PRODUCTS_JSON" --format lines', + 'tools/dev/bun.sh tools/release/release_graph_query.mjs ci-artifact-names --product "$product" --family sdk-package --format lines', + 'tools/dev/bun.sh tools/release/release_graph_query.mjs ci-artifact-names --product "$product" --kind "$kind" --family release-assets --format lines', + 'tools/dev/bun.sh tools/release/release_graph_query.mjs ci-artifact-names --product oliphaunt-node-direct --kind node-direct-addon --family npm-package --format lines', + "pnpm install --frozen-lockfile", + "target/oliphaunt-broker/release-assets", + "target/oliphaunt-node-direct/release-assets", + "tools/release/release.py publish-dry-run --products-json", + '--head-ref "$RELEASE_HEAD_SHA"', + ]) { + if (!publishBlock.includes(snippet)) { + fail(`Release workflow dry-run handoff is missing ${JSON.stringify(snippet)}`); + } + } + for (const legacyEnv of [ + "PRODUCT_OLIPHAUNT_RUST", + "PRODUCT_OLIPHAUNT_SWIFT", + "PRODUCT_OLIPHAUNT_KOTLIN", + "PRODUCT_OLIPHAUNT_REACT_NATIVE", + "PRODUCT_OLIPHAUNT_JS", + "PRODUCT_OLIPHAUNT_WASIX_RUST", + ]) { + if (publishBlock.includes(legacyEnv)) { + fail(`Release workflow must not hard-code SDK product selection with ${legacyEnv}`); + } + } + if (publishBlock.includes("target/release-assets/native")) { + fail("Release workflow must download native helper artifacts into product-owned release asset roots"); + } + + const downloadCalls = [...publishBlock.matchAll(/bun [.]github\/scripts\/download-build-artifacts[.]mjs/g)]; + if (downloadCalls.length === 0) { + fail("Release workflow must download staged builder artifacts from the CI workflow"); + } + for (const [index, call] of downloadCalls.entries()) { + const nextCall = index + 1 < downloadCalls.length ? downloadCalls[index + 1].index : -1; + const nextStep = publishBlock.indexOf("\n - name:", call.index + call[0].length); + const endCandidates = [nextCall, nextStep].filter((candidate) => candidate !== -1); + const end = endCandidates.length > 0 ? Math.min(...endCandidates) : publishBlock.length; + const callText = normalizedShell(publishBlock.slice(call.index, end)); + for (const required of ["CI", '"$RELEASE_HEAD_SHA"', "--run-id", "--job Builds"]) { + if (!callText.includes(required)) { + fail(`Release artifact download must require ${required}: ${callText.slice(0, 240)}`); + } + } + if (!callText.includes("--artifact") && !callText.includes("artifact_args")) { + fail(`Release artifact download must require explicit artifact arguments: ${callText.slice(0, 240)}`); + } + } + + const buildArtifactScript = readText(".github/scripts/download-build-artifacts.mjs"); + for (const snippet of [ + "--run-id", + "selectedRunId", + "requiredJobSuccess(repo, runId", + "artifactPresent(repo, runId, artifact)", + "actions/runs/${runId}/artifacts?per_page=100", + '"run", "view", runId, "--repo", repo, "--json", "jobs"', + "Bun.argv", + "mergeDownloadedArtifact", + "mergeChecksumManifest", + '-release-assets.sha256"', + "would overwrite", + ]) { + if (!buildArtifactScript.includes(snippet)) { + fail(`shared CI artifact downloader must support and verify pinned run ids: missing ${JSON.stringify(snippet)}`); + } + } + if (buildArtifactScript.includes("GH_RUN_JSON=")) { + fail("shared CI artifact downloader must not pass full workflow job JSON through the environment"); + } + + const requireWorkflowScript = readText(".github/scripts/require-workflow-success.sh"); + for (const snippet of [ + "--run-id", + "GITHUB_OUTPUT", + "run_id=", + 'emit_run_id "$run_id"', + "actions/runs/$run_id/artifacts?per_page=100", + 'gh run view "$run_id" --repo "$GH_REPO" --json jobs > "$jobs_file"', + "Bun.argv", + ]) { + if (!requireWorkflowScript.includes(snippet)) { + fail(`CI build gate must emit and validate selected run ids: missing ${JSON.stringify(snippet)}`); + } + } + if (requireWorkflowScript.includes("GH_RUN_JSON=")) { + fail("CI build gate must not pass full workflow job JSON through the environment"); + } + + const releaseScript = readText("tools/release/release.py"); + assertDirectReleasePythonToolsAreExecutable(releaseScript); + for (const forbidden of [ + "validate_wasix_runtime_inputs", + "materialized_wasix_runtime_crate_payloads", + "materialize_core_wasix_asset_payload", + "materialize_core_wasix_aot_payload", + "wasm_aot_target_triples", + 'xtask(["assets", "check"])', + 'xtask(["assets", "check-aot"', + '"assets", "aot-targets"', + ]) { + if (releaseScript.includes(forbidden)) { + fail( + "release CLI must validate staged liboliphaunt-wasix release archives, " + + `not raw WASIX build inputs or private crate payloads: found ${JSON.stringify(forbidden)}`, + ); + } + } + for (const snippet of [ + "validate_wasix_release_assets", + 'expected_assets(product, version, surface="github-release")', + "parse_local_checksum_manifest", + "target/oliphaunt-wasix/release-assets", + "validate_wasix_release_asset_contents", + ]) { + if (!releaseScript.includes(snippet)) { + fail(`release-staged WASIX assets must validate staged GitHub release assets: missing ${JSON.stringify(snippet)}`); + } + } + for (const forbidden of [ + "liboliphaunt-wasix:crates-io", + "publish_wasix_runtime_staged_crates", + "publish_wasix_runtime_crates_io", + 'package_check.extend(["--package", package])', + ]) { + if (releaseScript.includes(forbidden)) { + fail(`liboliphaunt-wasix must not publish private WASIX runtime crates to crates.io: found ${JSON.stringify(forbidden)}`); + } + } + for (const snippet of [ + '["pnpm", "exec", "jsr", "publish", "--dry-run"]', + 'command.append("--allow-dirty")', + "run(command, cwd=jsr_source)", + '"--product",\n "oliphaunt-node-direct",\n "--require-published"', + ]) { + if (!releaseScript.includes(snippet)) { + fail(`release dry-runs and package publishes must cover registry-native checks: missing ${JSON.stringify(snippet)}`); + } + } + + const cratePackageScript = readText("tools/policy/check-crate-package.sh"); + const cratePackageHelper = readText("tools/policy/list-publishable-cargo-packages.mjs"); + for (const snippet of [ + "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', + ]) { + if (!cratePackageScript.includes(snippet)) { + fail( + "crate package policy must package oliphaunt-wasix through the " + + `release-shaped local helper instead of crates.io resolution: missing ${JSON.stringify(snippet)}`, + ); + } + } + for (const snippet of [ + "'cargo', ['metadata', '--no-deps', '--format-version', '1']", + "Array.isArray(cargoPackage.publish) && cargoPackage.publish.length === 0", + "cargoPackage.name === 'oliphaunt-wasix'", + ]) { + if (!cratePackageHelper.includes(snippet)) { + fail( + "crate package policy must derive default publishable crates from cargo metadata " + + `with oliphaunt-wasix handled by the release-shaped helper: missing ${JSON.stringify(snippet)}`, + ); + } + } + + const releaseHeadScript = readText(".github/scripts/resolve-release-head.sh"); + for (const snippet of [ + "INPUT_RELEASE_COMMIT", + "40-character commit SHA", + "git merge-base --is-ancestor", + "release-target/", + "release-tooling changes", + ".github/workflows/*", + "tools/release/*", + "tools/xtask/*", + "RELEASE_HEAD_SHA", + ]) { + if (!releaseHeadScript.includes(snippet)) { + fail(`release commit resolver must pin safe publish-from-commit behavior: missing ${JSON.stringify(snippet)}`); + } + } + + const wasixDownloadScript = readText(".github/scripts/download-wasix-runtime-build-artifacts.mjs"); + for (const snippet of ["RELEASE_HEAD_SHA", "CI_RUN_ID", 'args.push("--run-id", process.env.CI_RUN_ID)', "--required-job", "Builds"]) { + if (!wasixDownloadScript.includes(snippet)) { + fail(`WASIX runtime artifact handoff must consume the selected CI run id: missing ${JSON.stringify(snippet)}`); + } + } + if (!publishBlock.includes("bun .github/scripts/download-wasix-runtime-build-artifacts.mjs")) { + fail("Release workflow must run WASIX runtime artifact handoff through the Bun wrapper"); + } + + const guardedPublishSteps = new Set([ + "Create release-please target branch", + "Create release-please GitHub releases", + "Remove release-please target branch", + "Publish liboliphaunt GitHub release assets", + "Publish selected extension GitHub release assets", + "Attest selected extension release assets", + "Attest liboliphaunt release assets", + "Publish Swift SDK GitHub release and SwiftPM tags", + "Publish Kotlin SDK to Maven Central", + "Publish React Native package to npm", + "Publish WASIX Rust binding to crates.io", + "Publish Rust SDK to crates.io", + "Publish broker GitHub release assets", + "Attest broker release assets", + "Publish Node direct GitHub release assets", + "Attest Node direct release assets", + "Publish Node direct optional packages to npm", + "Publish TypeScript packages to npm and JSR", + "Upload WASIX GitHub release assets", + "Attest WASIX release assets", + "Verify published release", + "Run consumer shape gates", + ]); + for (const step of guardedPublishSteps) { + assertStepIfContainsPublishGuard(publishSteps, step); + } + + const attestationRequirements = { + "Attest selected extension release assets": [ + "actions/attest-build-provenance@", + "target/extension-artifacts/*/release-assets/*.tar.gz", + "target/extension-artifacts/*/release-assets/*.tar.zst", + "target/extension-artifacts/*/release-assets/*.zip", + "target/extension-artifacts/*/release-assets/*.json", + "target/extension-artifacts/*/release-assets/*.properties", + "target/extension-artifacts/*/release-assets/*.sha256", + ], + "Attest liboliphaunt release assets": [ + "actions/attest-build-provenance@", + "target/liboliphaunt/release-assets/*.tar.gz", + "target/liboliphaunt/release-assets/*.tar.zst", + "target/liboliphaunt/release-assets/*.zip", + "target/liboliphaunt/release-assets/*.tsv", + "target/liboliphaunt/release-assets/*.sha256", + ], + "Attest broker release assets": [ + "actions/attest-build-provenance@", + "target/oliphaunt-broker/release-assets/*.tar.gz", + "target/oliphaunt-broker/release-assets/*.zip", + "target/oliphaunt-broker/release-assets/*.sha256", + ], + "Attest Node direct release assets": [ + "actions/attest-build-provenance@", + "target/oliphaunt-node-direct/release-assets/*.tar.gz", + "target/oliphaunt-node-direct/release-assets/*.zip", + "target/oliphaunt-node-direct/release-assets/*.sha256", + ], + "Attest WASIX release assets": [ + "actions/attest-build-provenance@", + "target/oliphaunt-wasix/release-assets/*.tar.zst", + "target/oliphaunt-wasix/release-assets/*.sha256", + ], + }; + for (const [step, snippets] of Object.entries(attestationRequirements)) { + for (const snippet of snippets) { + assertStepContains(publishSteps, step, snippet, `${step} must attest ${snippet}`); + } + } + + assertStepContains( + publishSteps, + "Verify published release", + "tools/release/release.py verify-release --products-json", + "Release workflow must verify published products through the release CLI", + ); + assertContains( + "tools/release/release.py", + "tools/release/verify_github_release_attestations.mjs", + "release.py verify-release must verify GitHub artifact attestations", + ); + for (const snippet of ["--signer-workflow", ".github/workflows/release.yml", "--source-ref", "refs/heads/main", "--deny-self-hosted-runners"]) { + assertContains( + "tools/release/verify_github_release_attestations.mjs", + snippet, + "Release attestation verification must pin signer workflow, source ref, and runner trust", + ); + } +} + +function extensionNativeTargets(jobs, tasks) { + const selectedTargets = ciPlan.nativeTargetSubsetForJobs(jobs, tasks); + const matrix = ciPlan.extensionArtifactsNativeMatrix("all", selectedTargets); + const include = matrix.include; + if (!Array.isArray(include)) { + fail("native extension artifact matrix must declare include rows"); + } + const targets = new Set(include.filter(isObject).map((row) => row.target)); + if (![...targets].every((target) => typeof target === "string")) { + fail("native extension artifact matrix rows must declare string target"); + } + return targets; +} + +function csvProductsFromMatrix(matrix) { + const products = new Set(); + for (const row of matrix.include ?? []) { + if (!isObject(row)) { + continue; + } + for (const item of String(row.extensions_csv ?? "").split(",")) { + if (item) { + products.add(item); + } + } + } + return products; +} + +function assertSingleExtensionMatrixSelection(product) { + const jobs = ciPlan.planJobsForAffected(new Set([product]), new Set([`${product}:assemble-release`])); + const selection = ciPlan.selectedExtensionProductsForPlan(new Set([product]), new Set([`${product}:assemble-release`]), jobs); + if (!setEquals(selection ?? new Set(), new Set([product]))) { + fail(`single exact-extension changes must narrow extension artifact matrices, got ${formatList(selection ?? new Set())}`); + } + const nativeMatrix = ciPlan.extensionArtifactsNativeMatrix("all", null, selection); + const matrixProducts = csvProductsFromMatrix(nativeMatrix); + if (!setEquals(matrixProducts, new Set([product]))) { + fail(`single exact-extension native matrix must include only ${product}, got ${formatList(matrixProducts)}`); + } + + const aggregateTasks = new Set([ + `${product}:assemble-release`, + "extension-artifacts-native:build-target", + "extension-artifacts-wasix:build-target", + "extension-packages:assemble-release", + ]); + const aggregateJobs = ciPlan.planJobsForAffected(new Set([product]), aggregateTasks); + const aggregateSelection = ciPlan.selectedExtensionProductsForPlan(new Set([product]), aggregateTasks, aggregateJobs); + if (!setEquals(aggregateSelection ?? new Set(), new Set([product]))) { + fail( + "single exact-extension changes must stay product-scoped even when aggregate artifact/package tasks are selected, " + + `got ${formatList(aggregateSelection ?? new Set())}`, + ); + } + const aggregateNativeProducts = csvProductsFromMatrix(ciPlan.extensionArtifactsNativeMatrix("all", null, aggregateSelection)); + if (!setEquals(aggregateNativeProducts, new Set([product]))) { + fail(`single exact-extension aggregate native matrix must include only ${product}, got ${formatList(aggregateNativeProducts)}`); + } + const aggregateWasixProducts = csvProductsFromMatrix(ciPlan.extensionArtifactsWasixMatrix("all", aggregateSelection)); + if (!setEquals(aggregateWasixProducts, new Set([product]))) { + fail(`single exact-extension aggregate WASIX matrix must include only ${product}, got ${formatList(aggregateWasixProducts)}`); + } +} + +function checkCiBuilderPlanning() { + const fullPlan = ciPlan.planForFullRun(); + const fullJobs = fullPlan.jobs; + const allowedFullNonBuilders = ciPlan.BASE_JOBS; + const unexpectedFullJobs = sorted(difference(difference(fullJobs, ciPlan.BUILDER_JOBS), allowedFullNonBuilders)); + if (unexpectedFullJobs.length > 0) { + fail(`full non-PR CI runs must select artifact-producing builder jobs only; unexpected jobs: ${JSON.stringify(unexpectedFullJobs)}`); + } + const forbiddenFullJobs = sorted( + intersection( + fullJobs, + new Set([ + "coverage-summary", + "docs", + "js-regression", + "mobile-e2e-android", + "mobile-e2e-ios", + "release-intent", + "release-readiness", + "repo", + "rust-regression", + "wasm-regression", + ]), + ), + ); + if (forbiddenFullJobs.length > 0) { + fail(`full non-PR CI runs must not select check/regression/policy jobs: ${JSON.stringify(forbiddenFullJobs)}`); + } + + const focusedWasixJobs = ciPlan.planForFullRun({ wasmTarget: "linux-x64-gnu" }).jobs; + const expectedFocusedWasixJobs = new Set(["affected", "liboliphaunt-wasix-runtime", "liboliphaunt-wasix-aot"]); + if (!setEquals(focusedWasixJobs, expectedFocusedWasixJobs)) { + fail(`focused WASIX target CI runs must build only the portable runtime and requested AOT target, got ${formatList(focusedWasixJobs)}`); + } + + const focusedMobileExpectations = { + android: new Set([ + "affected", + "extension-artifacts-native", + "kotlin-sdk-package", + "liboliphaunt-native-android", + "mobile-build-android", + "mobile-extension-packages", + "react-native-sdk-package", + ]), + ios: new Set([ + "affected", + "extension-artifacts-native", + "liboliphaunt-native-ios", + "mobile-build-ios", + "mobile-extension-packages", + "react-native-sdk-package", + "swift-sdk-package", + ]), + }; + for (const [target, expectedJobs] of Object.entries(focusedMobileExpectations)) { + const focusedJobs = ciPlan.planForFullRun({ mobileTarget: target }).jobs; + if (!isSubset(expectedJobs, focusedJobs)) { + fail(`focused ${target} CI run is missing builder jobs: expected at least ${formatList(expectedJobs)}, got ${formatList(focusedJobs)}`); + } + const focusedForbidden = intersection(focusedJobs, new Set(["mobile-e2e-android", "mobile-e2e-ios"])); + if (focusedForbidden.size > 0) { + fail(`focused ${target} CI run must build app artifacts only, not E2E jobs: ${formatList(focusedForbidden)}`); + } + } + + const androidArmPlan = ciPlan.planForFullRun({ nativeTarget: "android-arm64-v8a", mobileTarget: "android" }); + if (!setEquals(androidArmPlan.selectedTargets ?? new Set(), new Set(["android-arm64-v8a"]))) { + fail( + "focused Android mobile CI run with native_target=android-arm64-v8a must narrow every " + + `target-scoped builder to android-arm64-v8a, got ${formatList(androidArmPlan.selectedTargets ?? new Set())}`, + ); + } + if (JSON.stringify(ciPlan.mobileExtensionPackageNativeTargets(androidArmPlan.jobs, androidArmPlan.selectedTargets)) !== JSON.stringify(["android-arm64-v8a"])) { + fail("focused Android mobile extension package targets must match the selected Android native target"); + } + + const iosFocusedPlan = ciPlan.planForFullRun({ nativeTarget: "ios-xcframework", mobileTarget: "ios" }); + if (!setEquals(iosFocusedPlan.selectedTargets ?? new Set(), new Set(["ios-xcframework"]))) { + fail( + "focused iOS mobile CI run with native_target=ios-xcframework must narrow every " + + `target-scoped builder to ios-xcframework, got ${formatList(iosFocusedPlan.selectedTargets ?? new Set())}`, + ); + } + if (JSON.stringify(ciPlan.mobileExtensionPackageNativeTargets(iosFocusedPlan.jobs, iosFocusedPlan.selectedTargets)) !== JSON.stringify(["ios-xcframework"])) { + fail("focused iOS mobile extension package targets must match the selected iOS native target"); + } + + try { + ciPlan.planForFullRun({ nativeTarget: "ios-xcframework", mobileTarget: "android" }); + fail("focused Android mobile CI run must reject native_target=ios-xcframework"); + } catch (error) { + if (!String(error.message).includes("not valid for mobile_target=android")) { + fail(`focused Android/iOS target mismatch failed with an unclear error: ${error.message}`); + } + } + + try { + ciPlan.planForFullRun({ nativeTarget: "android-arm64-v8a", mobileTarget: "both" }); + fail("focused mobile_target=both must reject a single native target"); + } catch (error) { + if (!String(error.message).includes("mobile_target=both requires native_target=all")) { + fail(`focused mobile_target=both mismatch failed with an unclear error: ${error.message}`); + } + } + + const reactNativeJobs = ciPlan.planJobsForAffected(new Set(), new Set(["oliphaunt-react-native:package-artifacts"])); + const reactNativeExpectedJobs = new Set([ + "extension-artifacts-native", + "kotlin-sdk-package", + "liboliphaunt-native-android", + "liboliphaunt-native-ios", + "mobile-build-android", + "mobile-build-ios", + "mobile-extension-packages", + "react-native-sdk-package", + "swift-sdk-package", + ]); + if (!isSubset(reactNativeExpectedJobs, reactNativeJobs)) { + fail( + "React Native SDK package changes must build both mobile app artifacts from staged SDK/runtime/extension inputs; " + + `missing ${formatList(difference(reactNativeExpectedJobs, reactNativeJobs))} from ${formatList(reactNativeJobs)}`, + ); + } + const reactNativeTargets = ciPlan.nativeTargetSubsetForJobs(reactNativeJobs, new Set(["oliphaunt-react-native:package-artifacts"])); + const expectedReactNativeTargets = new Set(["android-arm64-v8a", "android-x86_64", "ios-xcframework"]); + if (!setEquals(reactNativeTargets ?? new Set(), expectedReactNativeTargets)) { + fail(`React Native SDK package changes must request Android and iOS native runtime targets, got ${formatList(reactNativeTargets ?? new Set())}`); + } + + assertSingleExtensionMatrixSelection("oliphaunt-extension-vector"); + assertSingleExtensionMatrixSelection("oliphaunt-extension-amcheck"); + const broadSelection = ciPlan.selectedExtensionProductsForPlan( + new Set(["extensions"]), + new Set(["extension-packages:assemble-release"]), + new Set(["extension-packages", "extension-artifacts-native", "extension-artifacts-wasix"]), + ); + const allExtensionProducts = expectedExtensionProductsFromSdkCatalog(); + if (!setEquals(broadSelection ?? new Set(), allExtensionProducts)) { + fail(`broad extension catalog changes must select the full exact-extension product set, got ${formatList(broadSelection ?? new Set())}`); + } + + const fullBuilderSelection = ciPlan.selectedExtensionProductsForPlan( + new Set(), + new Set([ + "extension-packages:assemble-release", + "extension-packages:assemble-mobile", + "oliphaunt-react-native:mobile-build-android", + "oliphaunt-react-native:mobile-build-ios", + ]), + new Set([ + "extension-artifacts-native", + "extension-artifacts-wasix", + "extension-packages", + "mobile-build-android", + "mobile-build-ios", + "mobile-extension-packages", + ]), + ); + if (!setEquals(fullBuilderSelection ?? new Set(), allExtensionProducts)) { + fail(`full builder runs must select the full exact-extension product set, got ${formatList(fullBuilderSelection ?? new Set())}`); + } + + const mobileFocusedSelection = ciPlan.selectedExtensionProductsForPlan( + new Set(), + new Set(["oliphaunt-react-native:mobile-build-android"]), + new Set(["mobile-build-android", "mobile-extension-packages", "extension-artifacts-native"]), + ); + if (!setEquals(mobileFocusedSelection ?? new Set(), new Set(["oliphaunt-extension-vector"]))) { + fail(`focused mobile builder runs must build only the selected smoke extension, got ${formatList(mobileFocusedSelection ?? new Set())}`); + } + + const androidTasks = new Set(["oliphaunt-react-native:mobile-build-android"]); + const androidJobs = ciPlan.planJobsForAffected(new Set(), androidTasks); + if (!androidJobs.has("extension-artifacts-native")) { + fail("Android mobile build must build selected native extension artifacts"); + } + const androidTargets = extensionNativeTargets(androidJobs, androidTasks); + if (!setEquals(androidTargets, new Set(["android-arm64-v8a", "android-x86_64"]))) { + fail(`Android mobile build must only request Android extension artifacts, got ${formatList(androidTargets)}`); + } + + const androidE2eJobs = ciPlan.planJobsForAffected(new Set(), new Set(["oliphaunt-react-native:mobile-e2e-android"])); + if (!setEquals(androidE2eJobs, ciPlan.BASE_JOBS)) { + fail(`CI must not select Android E2E jobs; got ${formatList(androidE2eJobs)}`); + } + + const iosTasks = new Set(["oliphaunt-react-native:mobile-build-ios"]); + const iosJobs = ciPlan.planJobsForAffected(new Set(), iosTasks); + if (!iosJobs.has("extension-artifacts-native")) { + fail("iOS mobile build must build selected native extension artifacts"); + } + const iosTargets = extensionNativeTargets(iosJobs, iosTasks); + if (!setEquals(iosTargets, new Set(["ios-xcframework"]))) { + fail(`iOS mobile build must only request iOS extension artifacts, got ${formatList(iosTargets)}`); + } + + const iosE2eJobs = ciPlan.planJobsForAffected(new Set(), new Set(["oliphaunt-react-native:mobile-e2e-ios"])); + if (!setEquals(iosE2eJobs, ciPlan.BASE_JOBS)) { + fail(`CI must not select iOS E2E jobs; got ${formatList(iosE2eJobs)}`); + } + + const extensionTasks = new Set(["extension-packages:assemble-release"]); + const extensionJobs = ciPlan.planJobsForAffected(new Set(), extensionTasks); + const fullTargets = extensionNativeTargets(extensionJobs, extensionTasks); + const expectedFullTargets = new Set( + artifactTargetRows({ product: "liboliphaunt-native", kind: "native-runtime", publishedOnly: true }) + .filter((target) => target.extension_artifacts ?? true) + .map((target) => target.target), + ); + if (!setEquals(fullTargets, expectedFullTargets)) { + fail(`extension package build must request all supported native extension artifacts, got ${formatList(fullTargets)}`); + } + + const swiftJobs = ciPlan.planJobsForAffected(new Set(), new Set(["oliphaunt-swift:package-artifacts"])); + if (!swiftJobs.has("liboliphaunt-native-ios")) { + fail("Swift SDK package build must build the Apple liboliphaunt XCFramework"); + } + const swiftTargets = ciPlan.nativeTargetSubsetForJobs(swiftJobs, new Set(["oliphaunt-swift:package-artifacts"])); + if (!setEquals(swiftTargets ?? new Set(), new Set(["ios-xcframework"]))) { + fail(`Swift SDK package build must only request the Apple XCFramework runtime target, got ${formatList(swiftTargets ?? new Set())}`); + } + + const kotlinJobs = ciPlan.planJobsForAffected(new Set(), new Set(["oliphaunt-kotlin:package-artifacts"])); + if (!setEquals(kotlinJobs, union(ciPlan.BASE_JOBS, new Set(["kotlin-sdk-package"])))) { + fail(`Kotlin SDK package build must only package the Kotlin SDK, got ${formatList(kotlinJobs)}`); + } + + const rustJobs = ciPlan.planJobsForAffected(new Set(), new Set(["oliphaunt-rust:package-artifacts"])); + if (!setEquals(rustJobs, union(ciPlan.BASE_JOBS, new Set(["rust-sdk-package"])))) { + fail(`Rust SDK package build must only package the Rust SDK, got ${formatList(rustJobs)}`); + } + + const jsJobs = ciPlan.planJobsForAffected(new Set(), new Set(["oliphaunt-js:package-artifacts"])); + if (!setEquals(jsJobs, union(ciPlan.BASE_JOBS, new Set(["js-sdk-package"])))) { + fail(`TypeScript SDK package build must only package the TypeScript SDK, got ${formatList(jsJobs)}`); + } + + const wasixRustJobs = ciPlan.planJobsForAffected(new Set(), new Set(["oliphaunt-wasix-rust:package-artifacts"])); + if (!setEquals(wasixRustJobs, union(ciPlan.BASE_JOBS, new Set(["wasix-rust-package"])))) { + fail(`WASIX Rust binding package build must only package the binding crate, got ${formatList(wasixRustJobs)}`); + } +} + +function main() { + const graph = releaseGraph(); + const policy = graph.policy; + if (!isObject(policy)) { + fail("release metadata must define policy"); + } + if (policy.repository !== "f0rr0/oliphaunt") { + fail("release policy repository must be f0rr0/oliphaunt"); + } + if (policy.versioning !== "independent") { + fail("release policy must use independent versioning"); + } + + checkReleaseMetadata(); + checkReleasePlanning(); + checkCiPolicy(); + checkReleaseWorkflowPolicy(); + checkCiBuilderPlanning(); + console.log("release policy checks passed"); + return 0; +} + +try { + process.exit(main()); +} catch (error) { + fail(error?.message ?? String(error)); +} diff --git a/tools/policy/check-release-policy.py b/tools/policy/check-release-policy.py deleted file mode 100644 index 17d21813..00000000 --- a/tools/policy/check-release-policy.py +++ /dev/null @@ -1,1627 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import json -import re -import pathlib -import subprocess -import tomllib - - -ROOT = pathlib.Path(__file__).resolve().parents[2] - - -BASE_PRODUCTS = { - "liboliphaunt-native", - "liboliphaunt-wasix", - "oliphaunt-rust", - "oliphaunt-broker", - "oliphaunt-node-direct", - "oliphaunt-swift", - "oliphaunt-kotlin", - "oliphaunt-react-native", - "oliphaunt-js", - "oliphaunt-wasix-rust", -} -CONSUMER_SHAPE_PRODUCTS_FIXTURE = "src/shared/fixtures/consumer-shape/products.json" - - -def fail(message: str) -> None: - raise SystemExit(message) - - -def bun_json(args: list[str]) -> object: - try: - output = subprocess.check_output( - ["tools/dev/bun.sh", *args], - cwd=ROOT, - stderr=subprocess.STDOUT, - text=True, - ) - except subprocess.CalledProcessError as error: - raise RuntimeError(error.output.strip()) from error - return json.loads(output) - - -def string_set(value: object, label: str) -> set[str]: - if not isinstance(value, list) or not all(isinstance(item, str) for item in value): - fail(f"{label} must be a JSON string list") - return set(value) - - -def optional_string_set(value: object, label: str) -> set[str] | None: - if value is None: - return None - return string_set(value, label) - - -def json_flag(value: set[str] | None) -> str: - if value is None: - return "null" - return json.dumps(sorted(value), separators=(",", ":")) - - -class CiPlanClient: - def __init__(self) -> None: - config = bun_json(["tools/graph/ci_plan.mjs", "config"]) - if not isinstance(config, dict): - fail("CI planner config query must return an object") - self.BASE_JOBS = string_set(config.get("baseJobs"), "baseJobs") - self.BUILDER_JOBS = string_set(config.get("builderJobs"), "builderJobs") - targets = config.get("ciJobTargets") - if not isinstance(targets, dict): - fail("ciJobTargets must be an object") - self.CI_JOB_TARGETS = { - str(job): sorted(string_set(job_targets, f"ciJobTargets.{job}")) - for job, job_targets in targets.items() - } - - def query(self, *args: str) -> object: - return bun_json(["tools/graph/ci_plan.mjs", *args]) - - def plan_jobs_for_affected(self, direct_projects: set[str], tasks: set[str]) -> set[str]: - return string_set( - self.query( - "jobs-for-affected", - "--direct-projects-json", - json_flag(direct_projects), - "--tasks-json", - json_flag(tasks), - ), - "jobs-for-affected", - ) - - def native_target_subset_for_jobs(self, jobs: set[str], tasks: set[str]) -> set[str] | None: - return optional_string_set( - self.query( - "native-target-subset", - "--jobs-json", - json_flag(jobs), - "--tasks-json", - json_flag(tasks), - ), - "native-target-subset", - ) - - def selected_extension_products_for_plan( - self, - direct_projects: set[str], - tasks: set[str], - jobs: set[str], - ) -> set[str] | None: - return optional_string_set( - self.query( - "selected-extension-products", - "--direct-projects-json", - json_flag(direct_projects), - "--tasks-json", - json_flag(tasks), - "--jobs-json", - json_flag(jobs), - ), - "selected-extension-products", - ) - - def plan_for_full_run( - self, - *, - wasm_target: str = "all", - native_target: str = "all", - mobile_target: str = "all", - ) -> tuple[set[str], set[str], set[str], str, set[str] | None]: - value = self.query( - "plan-full", - "--wasm-target", - wasm_target, - "--native-target", - native_target, - "--mobile-target", - mobile_target, - ) - if not isinstance(value, dict): - fail("plan-full must return an object") - reason = value.get("reason") - if not isinstance(reason, str): - fail("plan-full reason must be a string") - return ( - string_set(value.get("jobs"), "plan-full.jobs"), - string_set(value.get("projects"), "plan-full.projects"), - string_set(value.get("tasks"), "plan-full.tasks"), - reason, - optional_string_set(value.get("selectedTargets"), "plan-full.selectedTargets"), - ) - - def mobile_extension_package_native_targets( - self, - jobs: set[str], - selected_targets: set[str] | None, - ) -> list[str]: - value = self.query( - "mobile-extension-package-native-targets", - "--jobs-json", - json_flag(jobs), - "--selected-targets-json", - json_flag(selected_targets), - ) - return sorted(string_set(value, "mobile-extension-package-native-targets")) - - def extension_artifacts_native_matrix( - self, - native_target: str, - selected_targets: set[str] | None, - selected_products: set[str] | None = None, - ) -> dict: - value = self.query( - "matrix", - "extension-artifacts-native", - "--native-target", - native_target, - "--selected-targets-json", - json_flag(selected_targets), - "--selected-products-json", - json_flag(selected_products), - ) - if not isinstance(value, dict): - fail("extension-artifacts-native matrix must be an object") - return value - - def extension_artifacts_wasix_matrix( - self, - wasm_target: str, - selected_products: set[str] | None = None, - ) -> dict: - value = self.query( - "matrix", - "extension-artifacts-wasix", - "--wasm-target", - wasm_target, - "--selected-products-json", - json_flag(selected_products), - ) - if not isinstance(value, dict): - fail("extension-artifacts-wasix matrix must be an object") - return value - - -ci_plan = CiPlanClient() - - -def read_text(path: str) -> str: - return (ROOT / path).read_text(encoding="utf-8") - - -def assert_direct_release_python_tools_are_executable(release_script: str) -> None: - direct_invocations = sorted( - set( - match.group(1) - for match in re.finditer( - r'\[\s*"(tools/release/[^"]+\.py)"', - release_script, - flags=re.MULTILINE, - ) - ) - ) - for tool in direct_invocations: - path = ROOT / tool - if not path.is_file(): - fail(f"directly invoked release tool does not exist: {tool}") - if path.stat().st_mode & 0o111 == 0: - fail( - f"directly invoked release tool must be executable or called through python3: {tool}" - ) - - -def read_toml(path: pathlib.Path) -> dict: - with path.open("rb") as handle: - return tomllib.load(handle) - - -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_product_configs() -> dict[str, dict]: - value = bun_json(["tools/release/release_graph_query.mjs", "product-configs"]) - if not isinstance(value, list) or not all(isinstance(item, dict) for item in value): - fail("release graph product-configs query did not return an object list") - rows: dict[str, dict] = {} - for row in value: - product = row.get("product") - config_id = row.get("id") - if not isinstance(product, str) or not product: - fail("release graph product-configs rows must declare non-empty products") - if product in rows: - fail(f"release graph product-configs query returned duplicate product {product}") - if config_id != product: - fail(f"release graph product-configs {product}.id must match the product id") - for key in ("kind", "owner", "path", "changelog_path", "tag_prefix"): - if not isinstance(row.get(key), str) or not row[key]: - fail(f"release graph product-configs {product}.{key} must be a non-empty string") - for key in ("publish_targets", "release_artifacts", "version_files"): - if not isinstance(row.get(key), list) or not row[key] or not all( - isinstance(item, str) and item for item in row[key] - ): - fail(f"release graph product-configs {product}.{key} must be a non-empty string list") - rows[product] = row - if not rows: - fail("release graph returned no product configs") - return rows - - -def moon_project_rows() -> dict[str, dict]: - value = bun_json(["tools/release/release_graph_query.mjs", "moon-projects"]) - if not isinstance(value, list) or not all(isinstance(item, dict) for item in value): - fail("release graph moon-projects query did not return an object list") - rows: dict[str, dict] = {} - for row in value: - project_id = row.get("id") - if not isinstance(project_id, str) or not project_id: - fail("release graph moon-projects rows must declare non-empty ids") - if project_id in rows: - fail(f"release graph moon-projects query returned duplicate project {project_id}") - tags = row.get("tags") - dependency_scopes = row.get("dependencyScopes") - if not isinstance(tags, list) or not all(isinstance(item, str) for item in tags): - fail(f"release graph moon-projects {project_id}.tags must be a string list") - if not isinstance(dependency_scopes, dict) or not all( - isinstance(key, str) and isinstance(value, str) - for key, value in dependency_scopes.items() - ): - fail(f"release graph moon-projects {project_id}.dependencyScopes must be a string map") - rows[project_id] = row - return rows - - -def extension_metadata_rows() -> dict[str, dict]: - value = bun_json(["tools/release/release_graph_query.mjs", "extension-metadata"]) - if not isinstance(value, list) or not all(isinstance(item, dict) for item in value): - fail("release graph extension-metadata query did not return an object list") - rows: dict[str, dict] = {} - for row in value: - product = row.get("product") - if not isinstance(product, str) or not product: - fail("release graph extension-metadata rows must declare non-empty products") - if product in rows: - fail(f"release graph extension-metadata query returned duplicate product {product}") - for key in ("sqlName", "class", "versioning", "sourcePath"): - if not isinstance(row.get(key), str) or not row[key]: - fail(f"release graph extension-metadata {product}.{key} must be a non-empty string") - compatibility = row.get("compatibility") - if not isinstance(compatibility, dict): - fail(f"release graph extension-metadata {product}.compatibility must be an object") - rows[product] = row - if not rows: - fail("release graph returned no extension metadata rows") - return rows - - -def extension_product_ids() -> list[str]: - return sorted(extension_metadata_rows()) - - -def artifact_target_rows( - *, - product: str, - kind: str, - published_only: bool, -) -> list[dict]: - args = [ - "tools/release/release_graph_query.mjs", - "artifact-targets", - "--product", - product, - "--kind", - kind, - ] - if published_only: - args.append("--published-only") - value = bun_json(args) - if not isinstance(value, list) or not all(isinstance(item, dict) for item in value): - fail("release graph artifact-targets query did not return an object list") - for row in value: - target_id = row.get("id") - if not isinstance(target_id, str) or not target_id: - fail("release graph artifact-targets rows must declare non-empty ids") - if row.get("product") != product or row.get("kind") != kind: - fail(f"release graph artifact-targets returned unexpected row {target_id}") - if not isinstance(row.get("target"), str) or not row["target"]: - fail(f"release graph artifact-targets {target_id}.target must be a non-empty string") - if not isinstance(row.get("extension_artifacts", True), bool): - fail(f"release graph artifact-targets {target_id}.extension_artifacts must be true or false") - 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() - - -def expected_extension_products_from_sdk_catalog() -> set[str]: - data = json.loads(read_text("src/extensions/generated/sdk/rust.json")) - rows = data.get("extensions") - if not isinstance(rows, list) or not rows: - fail("generated Rust extension catalog must define public extensions") - products = set() - for row in rows: - if not isinstance(row, dict): - fail("generated Rust extension catalog rows must be objects") - sql_name = row.get("sql-name") - if not isinstance(sql_name, str) or not sql_name: - fail("generated Rust extension catalog rows must declare sql-name") - products.add(extension_product_id(sql_name)) - return products - - -def expected_contrib_extension_products_from_manifest() -> set[str]: - data = read_toml(ROOT / "src/extensions/contrib/postgres18.toml") - rows = data.get("extensions") - if not isinstance(rows, list) or not rows: - fail("PostgreSQL contrib extension manifest must define extension rows") - products = set() - for row in rows: - if not isinstance(row, dict): - fail("PostgreSQL contrib extension manifest rows must be tables") - sql_name = row.get("sql-name") - if not isinstance(sql_name, str) or not sql_name: - fail("PostgreSQL contrib extension manifest rows must declare sql-name") - products.add(extension_product_id(sql_name)) - return products - - -def expected_products() -> set[str]: - return BASE_PRODUCTS | expected_extension_products_from_sdk_catalog() - - -def project_release_metadata(project: dict) -> dict | None: - release = project.get("release") - return release if isinstance(release, dict) else None - - -def project_dependency_scopes(project: dict) -> dict[str, str]: - scopes = project.get("dependencyScopes") - return dict(scopes) if isinstance(scopes, dict) else {} - - -def assert_no_file(path: str) -> None: - if (ROOT / path).exists(): - fail(f"{path} must not exist; Moon is the only dependency/affectedness graph") - - -def assert_contains(path: str, snippet: str, message: str) -> None: - if snippet not in read_text(path): - fail(message) - - -def assert_not_contains(path: str, snippet: str, message: str) -> None: - if snippet in read_text(path): - fail(message) - - -def workflow_job_blocks(path: str) -> dict[str, str]: - text = read_text(path) - jobs_section = text.split("\njobs:\n", 1)[1] if "\njobs:\n" in text else "" - if not jobs_section: - fail(f"{path} must declare a jobs section") - matches = list(re.finditer(r"^ ([A-Za-z0-9_-]+):\n", jobs_section, flags=re.MULTILINE)) - if not matches: - fail(f"{path} parser found no jobs") - blocks: dict[str, str] = {} - for index, match in enumerate(matches): - end = matches[index + 1].start() if index + 1 < len(matches) else len(jobs_section) - blocks[match.group(1)] = jobs_section[match.start():end] - return blocks - - -def workflow_step_blocks(job_block: str) -> dict[str, str]: - matches = list(re.finditer(r"^ - name: (.+)\n", job_block, flags=re.MULTILINE)) - blocks: dict[str, str] = {} - for index, match in enumerate(matches): - end = matches[index + 1].start() if index + 1 < len(matches) else len(job_block) - name = match.group(1).strip() - blocks[name] = job_block[match.start():end] - return blocks - - -def workflow_job_needs(blocks: dict[str, str], job: str) -> set[str]: - block = blocks.get(job) - if block is None: - fail(f"CI workflow is missing job {job}") - match = re.search(r"(?ms)^ needs:\n(?P(?: - [A-Za-z0-9_-]+\n)+)", block) - if match is None: - return set() - return { - line.removeprefix(" - ").strip() - for line in match.group("body").splitlines() - if line.strip() - } - - -def assert_job_contains(blocks: dict[str, str], job: str, snippet: str, message: str) -> None: - block = blocks.get(job) - if block is None: - fail(f"CI workflow is missing job {job}") - if snippet not in block: - fail(message) - - -def assert_step_contains(steps: dict[str, str], step: str, snippet: str, message: str) -> None: - block = steps.get(step) - if block is None: - fail(f"workflow is missing step {step!r}") - if snippet not in block: - fail(message) - - -def assert_step_if_contains_publish_guard(steps: dict[str, str], step: str) -> None: - block = steps.get(step) - if block is None: - fail(f"workflow is missing step {step!r}") - if "inputs.operation == 'publish'" not in block: - fail(f"{step!r} must be guarded by inputs.operation == 'publish'") - - -def normalized_shell(text: str) -> str: - return re.sub(r"\s+", " ", text).strip() - - -def assert_text_order(text: str, snippets: list[str], message: str) -> None: - index = -1 - for snippet in snippets: - next_index = text.find(snippet, index + 1) - if next_index == -1: - fail(f"{message}: missing {snippet!r}") - index = next_index - - -def check_release_metadata(graph: dict) -> None: - products = release_product_configs() - if set(products) != expected_products(): - fail(f"release product set mismatch: expected {sorted(expected_products())}, got {sorted(products)}") - extension_metadata = extension_metadata_rows() - modeled_extension_products = set(extension_product_ids()) - expected_extension_products = expected_extension_products_from_sdk_catalog() - if modeled_extension_products != expected_extension_products: - fail( - "exact-extension release products must match the public generated extension catalog: " - f"expected {sorted(expected_extension_products)}, got {sorted(modeled_extension_products)}" - ) - - projects = moon_project_rows() - product_projects = release_product_projects() - for product, config in products.items(): - release_path = ROOT / config["path"] / "release.toml" - raw = read_toml(release_path) - for forbidden in ("depends_on", "source_globs", "package_visible_globs"): - if forbidden in raw: - fail(f"{release_path.relative_to(ROOT)} must not declare {forbidden}; Moon owns graph shape") - for key in ("id", "owner", "kind", "publish_targets", "release_artifacts"): - if key not in raw: - fail(f"{release_path.relative_to(ROOT)} must declare {key}") - 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 = product_projects[product] - project = projects.get(project_id) - if project is None: - fail(f"{product} has no owning Moon project") - tags = set(project.get("tags", [])) - if "release-product" not in tags: - fail(f"{project_id} must be tagged release-product") - release = project_release_metadata(project) - if release is None: - fail(f"{project_id} must declare project.release metadata") - if release.get("component") != product: - fail(f"{project_id} release component expected {product}, got {release.get('component')}") - if release.get("packagePath") != config.get("path"): - fail(f"{project_id} packagePath expected {config.get('path')}, got {release.get('packagePath')}") - if config.get("kind") == "exact-extension-artifact": - if product not in extension_metadata: - fail(f"{product} exact-extension product is missing release graph extension metadata") - layer = project.get("layer") - if layer != "library": - fail(f"{project_id} must be a library layer project; exact extension artifacts are publishable runtime-compatible products") - scopes = project_dependency_scopes(project) - for dependency in ("extension-runtime-contract", "liboliphaunt-native", "liboliphaunt-wasix"): - if scopes.get(dependency) != "production": - fail(f"{project_id} must declare a production Moon dependency on {dependency}") - - extension_model = projects.get("extension-model") - if extension_model is None: - fail("extension-model project is missing") - if "extensions" in project_dependency_scopes(extension_model): - fail("extension-model must not depend on the aggregate extensions project; exact extension runtime deps must remain acyclic") - - -def check_release_planning(graph: dict) -> None: - all_extension_products = expected_extension_products_from_sdk_catalog() - contrib_extension_products = expected_contrib_extension_products_from_manifest() - contains_cases = { - "src/shared/js-core/src/query.ts": {"oliphaunt-js", "oliphaunt-react-native"}, - "src/postgres/versions/18/source.toml": { - "liboliphaunt-native", - "liboliphaunt-wasix", - "oliphaunt-rust", - "oliphaunt-swift", - "oliphaunt-kotlin", - "oliphaunt-react-native", - "oliphaunt-js", - "oliphaunt-wasix-rust", - } - | contrib_extension_products, - "src/extensions/contrib/postgres18.toml": contrib_extension_products, - "src/shared/extension-runtime-contract/contract.toml": { - "liboliphaunt-native", - "liboliphaunt-wasix", - } - | all_extension_products, - "src/runtimes/liboliphaunt/native/VERSION": { - "liboliphaunt-native", - } - | all_extension_products, - "src/runtimes/liboliphaunt/wasix/VERSION": { - "liboliphaunt-wasix", - } - | all_extension_products, - } - 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 = plans[path] - actual = set(plan.get("releaseProducts", [])) - if actual != expected: - fail(f"{path} release plan expected exactly {sorted(expected)}, got {sorted(actual)}") - - -def check_ci_policy() -> None: - assert_no_file("tools/graph/jobs.toml") - assert_no_file("tools/release/release-inputs.toml") - ci = read_text(".github/workflows/ci.yml") - for forbidden in ("targets=(", "tools/graph/jobs.toml", "tools/release/release-inputs.toml"): - if forbidden in ci: - fail(f"CI workflow must not contain {forbidden}") - assert_contains("tools/graph/ci_plan.mjs", "moon([\"query\", \"tasks\"])", "CI planner must read Moon task tags") - assert_contains("tools/graph/ci_plan.mjs", "ci-", "CI planner must document ci-* task tags") - assert_contains( - "tools/graph/ci_plan.mjs", - "extension_package_products_csv", - "CI planner must emit selected exact-extension products for artifact package builders", - ) - assert_contains( - ".github/workflows/ci.yml", - "OLIPHAUNT_EXTENSION_PACKAGE_PRODUCTS", - "CI extension package builders must consume selected exact-extension products from the affected plan", - ) - assert_contains( - "tools/release/build-extension-ci-artifacts.mjs", - "OLIPHAUNT_EXTENSION_PACKAGE_PRODUCTS", - "exact-extension package builder must support selected product subsets", - ) - assert_contains( - ".github/scripts/select-planned-moon-targets.mjs", - "OLIPHAUNT_CI_JOB_TARGETS_JSON", - "CI product jobs must consume planned Moon targets through the Bun selector", - ) - if not ci_plan.CI_JOB_TARGETS: - fail("CI planner found no Moon ci-* task tags") - if "liboliphaunt-wasix-aot-targets" in ci_plan.BUILDER_JOBS: - fail("builder_jobs must contain artifact-producing jobs, not the WASIX AOT target planner") - - workflow_blocks = workflow_job_blocks(".github/workflows/ci.yml") - workflow_jobs = set(workflow_blocks) - if not workflow_jobs: - fail("CI workflow parser found no jobs") - moon_jobs = set(ci_plan.CI_JOB_TARGETS) - builder_moon_jobs = moon_jobs & ci_plan.BUILDER_JOBS - no_moon_target_jobs = { - "affected", - "check-targets", - "policy-targets", - "release-intent", - "checks", - "test-targets", - "tests", - "builds", - "mobile-e2e-android", - "mobile-e2e-ios", - "e2e", - "required", - } - allowed_workflow_jobs = builder_moon_jobs | no_moon_target_jobs - missing_workflow_jobs = sorted(ci_plan.BUILDER_JOBS - workflow_jobs) - if missing_workflow_jobs: - fail(f"builder Moon ci-* tags have no CI workflow job: {missing_workflow_jobs}") - untagged_workflow_jobs = sorted(workflow_jobs - allowed_workflow_jobs) - if untagged_workflow_jobs: - fail(f"CI workflow must only define phase gates, builder jobs, and aggregate exceptions: {untagged_workflow_jobs}") - non_builder_workflow_jobs = sorted((moon_jobs - ci_plan.BUILDER_JOBS) & workflow_jobs) - if non_builder_workflow_jobs: - fail(f"CI workflow must not define non-builder Moon jobs as dedicated artifact build jobs: {non_builder_workflow_jobs}") - - required_match = re.search(r"(?ms)^ required:\n.*?^ needs:\n(?P(?: - [A-Za-z0-9_-]+\n)+)", ci) - if required_match is None: - fail("CI workflow required job must declare a static needs list") - required_needs = { - line.removeprefix(" - ").strip() - for line in required_match.group("body").splitlines() - if line.strip() - } - if required_needs != {"affected", "release-intent", "checks", "tests", "builds", "e2e"}: - fail( - "required.needs must be the CI phase gates only: " - f"['affected', 'release-intent', 'checks', 'tests', 'builds', 'e2e']; got {sorted(required_needs)}" - ) - - builds_match = re.search(r"(?ms)^ builds:\n.*?^ needs:\n(?P(?: - [A-Za-z0-9_-]+\n)+)", ci) - if builds_match is None: - fail("CI workflow builds job must declare a static needs list") - builds_needs = { - line.removeprefix(" - ").strip() - for line in builds_match.group("body").splitlines() - if line.strip() - } - missing_builders = sorted(ci_plan.BUILDER_JOBS - builds_needs) - if missing_builders: - fail(f"builds.needs is missing builder jobs: {missing_builders}") - if "tests" in builds_needs: - fail("builds.needs must not include the global Tests job; artifact builders must only wait on real artifact producers") - - planned_job_invocations = set( - match.group(1) - for match in re.finditer(r"run-planned-moon-job[.]sh ([A-Za-z0-9_-]+)", ci) - ) - missing_planned_invocations = sorted(builder_moon_jobs - planned_job_invocations) - if missing_planned_invocations: - fail(f"builder workflow jobs do not consume planned Moon targets: {missing_planned_invocations}") - for line_number, line in enumerate(ci.splitlines(), start=1): - match = re.search(r"run-planned-moon-job[.]sh ([A-Za-z0-9_-]+)", line) - if match is None: - continue - job = match.group(1) - if job in ci_plan.BUILDER_JOBS and "MOON_CACHE=off" not in line: - fail(f"builder job {job} must disable Moon cache in CI at .github/workflows/ci.yml:{line_number}") - artifact_consumer_jobs = { - "extension-artifacts-wasix", - "extension-packages", - "mobile-extension-packages", - "liboliphaunt-native-release-assets", - "liboliphaunt-wasix-aot", - "liboliphaunt-wasix-release-assets", - "mobile-build-android", - "mobile-build-ios", - } - if job in artifact_consumer_jobs and "OLIPHAUNT_MOON_UPSTREAM=none" not in line: - fail( - f"artifact consumer job {job} must not re-run upstream Moon artifact producers in CI " - f"at .github/workflows/ci.yml:{line_number}" - ) - if job in ci_plan.BUILDER_JOBS - artifact_consumer_jobs and "OLIPHAUNT_MOON_UPSTREAM=none" in line: - fail( - f"builder job {job} must allow Moon upstream task inheritance in CI " - f"at .github/workflows/ci.yml:{line_number}" - ) - - expected_mobile_build_needs = { - "mobile-build-android": { - "affected", - "mobile-extension-packages", - "liboliphaunt-native-android", - "kotlin-sdk-package", - "react-native-sdk-package", - }, - "mobile-build-ios": { - "affected", - "mobile-extension-packages", - "liboliphaunt-native-ios", - "react-native-sdk-package", - "swift-sdk-package", - }, - } - for job, expected in expected_mobile_build_needs.items(): - actual = workflow_job_needs(workflow_blocks, job) - if actual != expected: - fail(f"{job}.needs must consume staged runtime, SDK, and exact-extension builders: expected {sorted(expected)}, got {sorted(actual)}") - for snippet in ( - "OLIPHAUNT_EXPO_ALLOW_NATIVE_BUILDS: \"0\"", - "OLIPHAUNT_EXPO_REQUIRE_SDK_ARTIFACTS: \"1\"", - "OLIPHAUNT_EXPO_REQUIRE_PREBUILT_EXTENSIONS: \"1\"", - "OLIPHAUNT_EXPO_EXTENSION_ARTIFACT_ROOT:", - "oliphaunt-mobile-extension-package-artifacts", - "--require-mobile-prebuilt-extensions", - ): - assert_job_contains(workflow_blocks, job, snippet, f"{job} must use staged SDK/runtime/exact-extension artifacts and reject source-build fallbacks") - assert_job_contains( - workflow_blocks, - "mobile-build-android", - "OLIPHAUNT_EXPO_ANDROID_BUILD_TYPE: release", - "Android mobile app builder must publish the same release-mode artifact that installed-app E2E consumes", - ) - assert_job_contains( - workflow_blocks, - "mobile-build-ios", - "OLIPHAUNT_EXPO_IOS_CONFIGURATION: Release", - "iOS mobile app builder must publish the same release-mode artifact that installed-app E2E consumes", - ) - assert_job_contains( - workflow_blocks, - "mobile-build-ios", - "OLIPHAUNT_EXPO_IOS_SDK: iphonesimulator", - "iOS mobile app builder must publish a simulator artifact for free installed-app E2E", - ) - assert_job_contains( - workflow_blocks, - "mobile-e2e-ios", - 'MAESTRO_DRIVER_STARTUP_TIMEOUT: "300000"', - "iOS installed-app E2E must give Maestro's XCTest driver enough startup time on macOS runners", - ) - - android_build = workflow_blocks["mobile-build-android"] - for snippet in ( - "matrix: ${{ fromJson(needs.affected.outputs.react_native_android_mobile_app_matrix) }}", - "liboliphaunt-native-target-${{ matrix.target }}", - "OLIPHAUNT_EXPO_ANDROID_ABI: ${{ matrix.abi }}", - "oliphaunt-kotlin-sdk-package-artifacts", - "oliphaunt-react-native-sdk-package-artifacts", - "react-native-mobile-android-app-${{ matrix.target }}", - ): - if snippet not in android_build: - fail(f"mobile-build-android must download/upload {snippet}") - for path, snippet, message in ( - ( - "src/sdks/react-native/android/build.gradle", - "OLIPHAUNT_ANDROID_LINK_EVIDENCE_FILE", - "React Native Android Gradle packaging must pass static-extension link evidence into CMake", - ), - ( - "src/sdks/react-native/android/src/main/cpp/CMakeLists.txt", - "oliphaunt-android-static-extension-link-v1", - "React Native Android CMake packaging must emit deterministic static-extension link evidence", - ), - ( - "src/sdks/react-native/tools/expo-android-runner.sh", - "androidLinkEvidence", - "React Native Android mobile build reports must include static-extension link evidence", - ), - ( - "tools/release/check-staged-artifacts.mjs", - "checkAndroidPrebuiltExtensionLinkage", - "staged mobile artifact checks must validate Android static-extension link evidence", - ), - ): - if snippet not in read_text(path): - fail(message) - - ios_build = workflow_blocks["mobile-build-ios"] - for snippet in ( - "liboliphaunt-native-target-ios-xcframework", - "oliphaunt-swift-sdk-package-artifacts", - "oliphaunt-react-native-sdk-package-artifacts", - "react-native-mobile-ios-app", - ): - if snippet not in ios_build: - fail(f"mobile-build-ios must download/upload {snippet}") - - wasix_extension_packager = read_text("src/extensions/artifacts/wasix/tools/package-release-assets.sh") - if "--strict-generated" in wasix_extension_packager: - fail("WASIX exact-extension packaging must consume portable runtime outputs; strict generation checks belong to the portable runtime builder") - - mobile_e2e = read_text(".github/workflows/mobile-e2e.yml") - for snippet in ( - "name: E2E", - 'workflows: ["CI"]', - "BUILD_GATE_JOB: Builds", - 'bun .github/scripts/resolve-mobile-e2e.mjs', - 'bun .github/scripts/check-ci-gate.mjs allow-skipped', - 'react-native-mobile-android-app-android-x86_64', - 'react-native-mobile-ios-app', - 'uses: ./.github/actions/setup-maestro', - 'tools/dev/start-android-emulator-ci.sh', - 'bash src/sdks/react-native/tools/mobile-e2e.sh android', - 'bash src/sdks/react-native/tools/mobile-e2e.sh ios', - 'OLIPHAUNT_EXPO_ANDROID_BUILD_TYPE: release', - 'OLIPHAUNT_EXPO_IOS_CONFIGURATION: Release', - 'OLIPHAUNT_EXPO_IOS_SDK: iphonesimulator', - 'MAESTRO_DRIVER_STARTUP_TIMEOUT: "300000"', - ): - if snippet not in mobile_e2e: - fail(f"E2E workflow must consume built app artifacts with pinned installed-app tooling: missing {snippet}") - for forbidden in ( - "run-planned-moon-job.sh", - "mobile-build:android", - "mobile-build:ios", - "tools/mobile-build.sh", - "OLIPHAUNT_EXPO_ALLOW_NATIVE_BUILDS", - ): - if forbidden in mobile_e2e: - 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.mjs") - missing_moon_setup = sorted( - job - for job, block in release_workflow_blocks.items() - if any(pattern in block for pattern in release_tool_patterns) - and "./.github/actions/setup-moon" not in block - ) - if missing_moon_setup: - fail(f"release workflow jobs invoke release metadata without setup-moon: {missing_moon_setup}") - - if not (ROOT / CONSUMER_SHAPE_PRODUCTS_FIXTURE).is_file(): - fail(f"missing consumer shape fixture: {CONSUMER_SHAPE_PRODUCTS_FIXTURE}") - assert_contains( - "tools/release/release.py", - "check_release_pr_coverage.mjs", - "release checks must verify release-please version bumps cover Moon-selected products", - ) - for path in ( - ".github/workflows/release.yml", - "tools/release/release.py", - "tools/release/upload_github_release_assets.mjs", - ): - assert_not_contains( - path, - "replace_conflicting_assets", - "GitHub release asset replacement must stay a manual repair, not a release workflow switch", - ) - assert_not_contains( - path, - "replace-conflicting-assets", - "GitHub release asset replacement must stay a manual repair, not a release CLI switch", - ) - assert_not_contains( - "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.mjs", - "delete the conflicting GitHub release asset manually", - "GitHub release asset byte conflicts must fail with manual repair guidance", - ) - - -def check_release_workflow_policy() -> None: - release_blocks = workflow_job_blocks(".github/workflows/release.yml") - publish_block = release_blocks.get("publish") - if publish_block is None: - fail("Release workflow must define a publish job") - publish_steps = workflow_step_blocks(publish_block) - - for permission in ( - "actions: read", - "attestations: write", - "contents: write", - "id-token: write", - ): - if permission not in publish_block: - fail(f"Release publish job must declare {permission}") - release_workflow = read_text(".github/workflows/release.yml") - for snippet in ( - "release_commit:", - ".github/scripts/resolve-release-head.sh", - "id: release_head", - "RELEASE_HEAD_SHA", - "Create release-please target branch", - "target-branch: ${{ steps.release_head.outputs.target_branch }}", - "Remove release-please target branch", - 'tools/dev/bun.sh tools/release/release_plan.mjs --from-product-tags --include-current-tags --head-ref "$RELEASE_HEAD_SHA" --format github-output', - ): - if snippet not in release_workflow: - fail(f"Release workflow must resolve and publish from an explicit release commit: missing {snippet!r}") - if "tools/release/release.py plan" in release_workflow: - fail("Release workflow must call the Bun release plan entrypoint directly") - for legacy_release_query in ( - "tools/release/release.py ci-" + "products", - "tools/release/release.py ci-" + "artifacts", - ): - if legacy_release_query in release_workflow: - fail("Release workflow must call Bun release graph queries for CI artifact handoffs") - - assert_text_order( - publish_block, - [ - "Resolve release commit", - "Plan product releases", - "Require release-commit CI build gate", - "Download WASIX runtime build artifacts", - "Download WASIX release assets", - "Download exact-extension package artifacts", - "Download SDK package artifacts", - "Download liboliphaunt release assets", - "Install TypeScript release tooling", - "Download native helper release assets", - "Download Node direct optional npm packages", - "Validate selected release product dry-runs", - "Create release-please target branch", - "Create release-please GitHub releases", - "Remove release-please target branch", - "Publish liboliphaunt GitHub release assets", - ], - "Release publish must validate release-commit builder outputs before creating release tags", - ) - - for snippet in ( - "id: ci_build_gate", - 'require-workflow-success.sh CI "$RELEASE_HEAD_SHA" 7200 --job Builds', - "CI_RUN_ID: ${{ steps.ci_build_gate.outputs.run_id }}", - "--run-id \"$CI_RUN_ID\"", - "--run-id \"${CI_RUN_ID}\"", - "--job Builds", - "--artifact liboliphaunt-wasix-release-assets", - "--artifact oliphaunt-extension-package-artifacts", - "--artifact liboliphaunt-native-release-assets", - "--artifact \"$artifact\"", - "PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }}", - "tools/dev/bun.sh tools/release/release_graph_query.mjs ci-products --family sdk-package --products-json \"$PRODUCTS_JSON\" --format lines", - "tools/dev/bun.sh tools/release/release_graph_query.mjs ci-artifact-names --product \"$product\" --family sdk-package --format lines", - "tools/dev/bun.sh tools/release/release_graph_query.mjs ci-artifact-names --product \"$product\" --kind \"$kind\" --family release-assets --format lines", - "tools/dev/bun.sh tools/release/release_graph_query.mjs ci-artifact-names --product oliphaunt-node-direct --kind node-direct-addon --family npm-package --format lines", - "pnpm install --frozen-lockfile", - "target/oliphaunt-broker/release-assets", - "target/oliphaunt-node-direct/release-assets", - "tools/release/release.py publish-dry-run --products-json", - '--head-ref "$RELEASE_HEAD_SHA"', - ): - 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") - - download_calls = list(re.finditer(r"bun [.]github/scripts/download-build-artifacts[.]mjs", publish_block)) - if not download_calls: - fail("Release workflow must download staged builder artifacts from the CI workflow") - for index, call in enumerate(download_calls): - next_call = download_calls[index + 1].start() if index + 1 < len(download_calls) else -1 - next_step = publish_block.find("\n - name:", call.end()) - end_candidates = [candidate for candidate in (next_call, next_step) if candidate != -1] - end = min(end_candidates) if end_candidates else len(publish_block) - call_text = normalized_shell(publish_block[call.start():end]) - # 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"): - 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.mjs") - for snippet in ( - "--run-id", - "selectedRunId", - "requiredJobSuccess(repo, runId", - "artifactPresent(repo, runId, artifact)", - "actions/runs/${runId}/artifacts?per_page=100", - '"run", "view", runId, "--repo", repo, "--json", "jobs"', - "Bun.argv", - "mergeDownloadedArtifact", - "mergeChecksumManifest", - '-release-assets.sha256"', - "would overwrite", - ): - if snippet not in build_artifact_script: - fail(f"shared CI artifact downloader must support and verify pinned run ids: missing {snippet!r}") - if "GH_RUN_JSON=" in build_artifact_script: - fail("shared CI artifact downloader must not pass full workflow job JSON through the environment") - - require_workflow_script = read_text(".github/scripts/require-workflow-success.sh") - for snippet in ( - "--run-id", - "GITHUB_OUTPUT", - "run_id=", - 'emit_run_id "$run_id"', - 'actions/runs/$run_id/artifacts?per_page=100', - 'gh run view "$run_id" --repo "$GH_REPO" --json jobs > "$jobs_file"', - "Bun.argv", - ): - if snippet not in require_workflow_script: - fail(f"CI build gate must emit and validate selected run ids: missing {snippet!r}") - if "GH_RUN_JSON=" in require_workflow_script: - fail("CI build gate must not pass full workflow job JSON through the environment") - - release_script = read_text("tools/release/release.py") - assert_direct_release_python_tools_are_executable(release_script) - for forbidden in ( - "validate_wasix_runtime_inputs", - "materialized_wasix_runtime_crate_payloads", - "materialize_core_wasix_asset_payload", - "materialize_core_wasix_aot_payload", - "wasm_aot_target_triples", - 'xtask(["assets", "check"])', - 'xtask(["assets", "check-aot"', - '"assets", "aot-targets"', - ): - if forbidden in release_script: - fail( - "release CLI must validate staged liboliphaunt-wasix release archives, " - f"not raw WASIX build inputs or private crate payloads: found {forbidden!r}" - ) - for snippet in ( - "validate_wasix_release_assets", - "expected_assets(product, version, surface=\"github-release\")", - "parse_local_checksum_manifest", - "target/oliphaunt-wasix/release-assets", - "validate_wasix_release_asset_contents", - ): - if snippet not in release_script: - fail(f"release-staged WASIX assets must validate staged GitHub release assets: missing {snippet!r}") - for forbidden in ( - 'liboliphaunt-wasix:crates-io', - "publish_wasix_runtime_staged_crates", - "publish_wasix_runtime_crates_io", - "package_check.extend([\"--package\", package])", - ): - if forbidden in release_script: - fail(f"liboliphaunt-wasix must not publish private WASIX runtime crates to crates.io: found {forbidden!r}") - for snippet in ( - '["pnpm", "exec", "jsr", "publish", "--dry-run"]', - 'command.append("--allow-dirty")', - 'run(command, cwd=jsr_source)', - '"--product",\n "oliphaunt-node-direct",\n "--require-published"', - ): - 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") - crate_package_helper = read_text("tools/policy/list-publishable-cargo-packages.mjs") - for snippet in ( - "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', - ): - 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}" - ) - 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 ( - "INPUT_RELEASE_COMMIT", - "40-character commit SHA", - "git merge-base --is-ancestor", - "release-target/", - "release-tooling changes", - ".github/workflows/*", - "tools/release/*", - "tools/xtask/*", - "RELEASE_HEAD_SHA", - ): - if snippet not in release_head_script: - fail(f"release commit resolver must pin safe publish-from-commit behavior: missing {snippet!r}") - - wasix_download_script = read_text(".github/scripts/download-wasix-runtime-build-artifacts.mjs") - for snippet in ("RELEASE_HEAD_SHA", "CI_RUN_ID", 'args.push("--run-id", process.env.CI_RUN_ID)', "--required-job", "Builds"): - if snippet not in wasix_download_script: - fail(f"WASIX runtime artifact handoff must consume the selected CI run id: missing {snippet!r}") - if "bun .github/scripts/download-wasix-runtime-build-artifacts.mjs" not in publish_block: - fail("Release workflow must run WASIX runtime artifact handoff through the Bun wrapper") - - guarded_publish_steps = { - "Create release-please target branch", - "Create release-please GitHub releases", - "Remove release-please target branch", - "Publish liboliphaunt GitHub release assets", - "Publish selected extension GitHub release assets", - "Attest selected extension release assets", - "Attest liboliphaunt release assets", - "Publish Swift SDK GitHub release and SwiftPM tags", - "Publish Kotlin SDK to Maven Central", - "Publish React Native package to npm", - "Publish WASIX Rust binding to crates.io", - "Publish Rust SDK to crates.io", - "Publish broker GitHub release assets", - "Attest broker release assets", - "Publish Node direct GitHub release assets", - "Attest Node direct release assets", - "Publish Node direct optional packages to npm", - "Publish TypeScript packages to npm and JSR", - "Upload WASIX GitHub release assets", - "Attest WASIX release assets", - "Verify published release", - "Run consumer shape gates", - } - for step in guarded_publish_steps: - assert_step_if_contains_publish_guard(publish_steps, step) - - attestation_requirements = { - "Attest selected extension release assets": [ - "actions/attest-build-provenance@", - "target/extension-artifacts/*/release-assets/*.tar.gz", - "target/extension-artifacts/*/release-assets/*.tar.zst", - "target/extension-artifacts/*/release-assets/*.zip", - "target/extension-artifacts/*/release-assets/*.json", - "target/extension-artifacts/*/release-assets/*.properties", - "target/extension-artifacts/*/release-assets/*.sha256", - ], - "Attest liboliphaunt release assets": [ - "actions/attest-build-provenance@", - "target/liboliphaunt/release-assets/*.tar.gz", - "target/liboliphaunt/release-assets/*.tar.zst", - "target/liboliphaunt/release-assets/*.zip", - "target/liboliphaunt/release-assets/*.tsv", - "target/liboliphaunt/release-assets/*.sha256", - ], - "Attest broker release assets": [ - "actions/attest-build-provenance@", - "target/oliphaunt-broker/release-assets/*.tar.gz", - "target/oliphaunt-broker/release-assets/*.zip", - "target/oliphaunt-broker/release-assets/*.sha256", - ], - "Attest Node direct release assets": [ - "actions/attest-build-provenance@", - "target/oliphaunt-node-direct/release-assets/*.tar.gz", - "target/oliphaunt-node-direct/release-assets/*.zip", - "target/oliphaunt-node-direct/release-assets/*.sha256", - ], - "Attest WASIX release assets": [ - "actions/attest-build-provenance@", - "target/oliphaunt-wasix/release-assets/*.tar.zst", - "target/oliphaunt-wasix/release-assets/*.sha256", - ], - } - for step, snippets in attestation_requirements.items(): - for snippet in snippets: - assert_step_contains(publish_steps, step, snippet, f"{step} must attest {snippet}") - - assert_step_contains( - publish_steps, - "Verify published release", - "tools/release/release.py verify-release --products-json", - "Release workflow must verify published products through the release CLI", - ) - assert_contains( - "tools/release/release.py", - "tools/release/verify_github_release_attestations.mjs", - "release.py verify-release must verify GitHub artifact attestations", - ) - for snippet in ( - "--signer-workflow", - ".github/workflows/release.yml", - "--source-ref", - "refs/heads/main", - "--deny-self-hosted-runners", - ): - assert_contains( - "tools/release/verify_github_release_attestations.mjs", - snippet, - "Release attestation verification must pin signer workflow, source ref, and runner trust", - ) - - -def extension_native_targets(jobs: set[str], tasks: set[str]) -> set[str]: - selected_targets = ci_plan.native_target_subset_for_jobs(jobs, tasks) - matrix = ci_plan.extension_artifacts_native_matrix("all", selected_targets) - include = matrix.get("include") - if not isinstance(include, list): - fail("native extension artifact matrix must declare include rows") - targets = {row.get("target") for row in include if isinstance(row, dict)} - if not all(isinstance(target, str) for target in targets): - fail("native extension artifact matrix rows must declare string target") - return set(targets) - - -def assert_single_extension_matrix_selection(product: str) -> None: - jobs = ci_plan.plan_jobs_for_affected( - {product}, - {f"{product}:assemble-release"}, - ) - selection = ci_plan.selected_extension_products_for_plan( - {product}, - {f"{product}:assemble-release"}, - jobs, - ) - if selection != {product}: - fail(f"single exact-extension changes must narrow extension artifact matrices, got {sorted(selection or [])}") - native_matrix = ci_plan.extension_artifacts_native_matrix( - "all", - None, - selection, - ) - matrix_products = { - item - for row in native_matrix.get("include", []) - if isinstance(row, dict) - for item in str(row.get("extensions_csv", "")).split(",") - if item - } - if matrix_products != {product}: - fail(f"single exact-extension native matrix must include only {product}, got {sorted(matrix_products)}") - - aggregate_tasks = { - f"{product}:assemble-release", - "extension-artifacts-native:build-target", - "extension-artifacts-wasix:build-target", - "extension-packages:assemble-release", - } - aggregate_jobs = ci_plan.plan_jobs_for_affected({product}, aggregate_tasks) - aggregate_selection = ci_plan.selected_extension_products_for_plan( - {product}, - aggregate_tasks, - aggregate_jobs, - ) - if aggregate_selection != {product}: - fail( - "single exact-extension changes must stay product-scoped even when aggregate artifact/package tasks are selected, " - f"got {sorted(aggregate_selection or [])}" - ) - aggregate_native_products = { - item - for row in ci_plan.extension_artifacts_native_matrix("all", None, aggregate_selection).get("include", []) - if isinstance(row, dict) - for item in str(row.get("extensions_csv", "")).split(",") - if item - } - if aggregate_native_products != {product}: - fail( - f"single exact-extension aggregate native matrix must include only {product}, got {sorted(aggregate_native_products)}" - ) - aggregate_wasix_products = { - item - for row in ci_plan.extension_artifacts_wasix_matrix("all", aggregate_selection).get("include", []) - if isinstance(row, dict) - for item in str(row.get("extensions_csv", "")).split(",") - if item - } - if aggregate_wasix_products != {product}: - fail( - f"single exact-extension aggregate WASIX matrix must include only {product}, got {sorted(aggregate_wasix_products)}" - ) - - -def check_ci_builder_planning() -> None: - full_jobs, _projects, _tasks, _reason, _selected_targets = ci_plan.plan_for_full_run() - allowed_full_non_builders = ci_plan.BASE_JOBS - unexpected_full_jobs = sorted(full_jobs - ci_plan.BUILDER_JOBS - allowed_full_non_builders) - if unexpected_full_jobs: - fail( - "full non-PR CI runs must select artifact-producing builder jobs only; " - f"unexpected jobs: {unexpected_full_jobs}" - ) - forbidden_full_jobs = sorted( - full_jobs - & { - "coverage-summary", - "docs", - "js-regression", - "mobile-e2e-android", - "mobile-e2e-ios", - "release-intent", - "release-readiness", - "repo", - "rust-regression", - "wasm-regression", - } - ) - if forbidden_full_jobs: - fail(f"full non-PR CI runs must not select check/regression/policy jobs: {forbidden_full_jobs}") - - focused_wasix_jobs, _projects, _tasks, _reason, _targets = ci_plan.plan_for_full_run( - wasm_target="linux-x64-gnu", - ) - expected_focused_wasix_jobs = { - "affected", - "liboliphaunt-wasix-runtime", - "liboliphaunt-wasix-aot", - } - if focused_wasix_jobs != expected_focused_wasix_jobs: - fail( - "focused WASIX target CI runs must build only the portable runtime and requested AOT target, " - f"got {sorted(focused_wasix_jobs)}" - ) - - focused_mobile_expectations = { - "android": { - "affected", - "extension-artifacts-native", - "kotlin-sdk-package", - "liboliphaunt-native-android", - "mobile-build-android", - "mobile-extension-packages", - "react-native-sdk-package", - }, - "ios": { - "affected", - "extension-artifacts-native", - "liboliphaunt-native-ios", - "mobile-build-ios", - "mobile-extension-packages", - "react-native-sdk-package", - "swift-sdk-package", - }, - } - for target, expected_jobs in focused_mobile_expectations.items(): - focused_jobs, *_ = ci_plan.plan_for_full_run(mobile_target=target) - if not expected_jobs <= focused_jobs: - fail( - f"focused {target} CI run is missing builder jobs: " - f"expected at least {sorted(expected_jobs)}, got {sorted(focused_jobs)}" - ) - focused_forbidden = focused_jobs & {"mobile-e2e-android", "mobile-e2e-ios"} - if focused_forbidden: - fail( - f"focused {target} CI run must build app artifacts only, not E2E jobs: " - f"{sorted(focused_forbidden)}" - ) - - android_arm_jobs, _projects, _tasks, _reason, android_arm_targets = ci_plan.plan_for_full_run( - native_target="android-arm64-v8a", - mobile_target="android", - ) - if android_arm_targets != {"android-arm64-v8a"}: - fail( - "focused Android mobile CI run with native_target=android-arm64-v8a must narrow every " - f"target-scoped builder to android-arm64-v8a, got {sorted(android_arm_targets or [])}" - ) - if ci_plan.mobile_extension_package_native_targets(android_arm_jobs, android_arm_targets) != ["android-arm64-v8a"]: - fail("focused Android mobile extension package targets must match the selected Android native target") - - ios_focused_jobs, _projects, _tasks, _reason, ios_focused_targets = ci_plan.plan_for_full_run( - native_target="ios-xcframework", - mobile_target="ios", - ) - if ios_focused_targets != {"ios-xcframework"}: - fail( - "focused iOS mobile CI run with native_target=ios-xcframework must narrow every " - f"target-scoped builder to ios-xcframework, got {sorted(ios_focused_targets or [])}" - ) - if ci_plan.mobile_extension_package_native_targets(ios_focused_jobs, ios_focused_targets) != ["ios-xcframework"]: - fail("focused iOS mobile extension package targets must match the selected iOS native target") - - try: - ci_plan.plan_for_full_run(native_target="ios-xcframework", mobile_target="android") - except RuntimeError as error: - if "not valid for mobile_target=android" not in str(error): - fail(f"focused Android/iOS target mismatch failed with an unclear error: {error}") - else: - fail("focused Android mobile CI run must reject native_target=ios-xcframework") - - try: - ci_plan.plan_for_full_run(native_target="android-arm64-v8a", mobile_target="both") - except RuntimeError as error: - if "mobile_target=both requires native_target=all" not in str(error): - fail(f"focused mobile_target=both mismatch failed with an unclear error: {error}") - else: - fail("focused mobile_target=both must reject a single native target") - - react_native_jobs = ci_plan.plan_jobs_for_affected( - set(), - {"oliphaunt-react-native:package-artifacts"}, - ) - react_native_expected_jobs = { - "extension-artifacts-native", - "kotlin-sdk-package", - "liboliphaunt-native-android", - "liboliphaunt-native-ios", - "mobile-build-android", - "mobile-build-ios", - "mobile-extension-packages", - "react-native-sdk-package", - "swift-sdk-package", - } - if not react_native_expected_jobs <= react_native_jobs: - fail( - "React Native SDK package changes must build both mobile app artifacts from staged SDK/runtime/extension inputs; " - f"missing {sorted(react_native_expected_jobs - react_native_jobs)} from {sorted(react_native_jobs)}" - ) - react_native_targets = ci_plan.native_target_subset_for_jobs( - react_native_jobs, - {"oliphaunt-react-native:package-artifacts"}, - ) - expected_react_native_targets = {"android-arm64-v8a", "android-x86_64", "ios-xcframework"} - if react_native_targets != expected_react_native_targets: - fail( - "React Native SDK package changes must request Android and iOS native runtime targets, " - f"got {sorted(react_native_targets or [])}" - ) - - assert_single_extension_matrix_selection("oliphaunt-extension-vector") - assert_single_extension_matrix_selection("oliphaunt-extension-amcheck") - broad_selection = ci_plan.selected_extension_products_for_plan( - {"extensions"}, - {"extension-packages:assemble-release"}, - {"extension-packages", "extension-artifacts-native", "extension-artifacts-wasix"}, - ) - all_extension_products = expected_extension_products_from_sdk_catalog() - if broad_selection != all_extension_products: - fail( - "broad extension catalog changes must select the full exact-extension product set, " - f"got {sorted(broad_selection or [])}" - ) - - full_builder_selection = ci_plan.selected_extension_products_for_plan( - set(), - { - "extension-packages:assemble-release", - "extension-packages:assemble-mobile", - "oliphaunt-react-native:mobile-build-android", - "oliphaunt-react-native:mobile-build-ios", - }, - { - "extension-artifacts-native", - "extension-artifacts-wasix", - "extension-packages", - "mobile-build-android", - "mobile-build-ios", - "mobile-extension-packages", - }, - ) - if full_builder_selection != all_extension_products: - fail( - "full builder runs must select the full exact-extension product set, " - f"got {sorted(full_builder_selection or [])}" - ) - - mobile_focused_selection = ci_plan.selected_extension_products_for_plan( - set(), - {"oliphaunt-react-native:mobile-build-android"}, - {"mobile-build-android", "mobile-extension-packages", "extension-artifacts-native"}, - ) - if mobile_focused_selection != {"oliphaunt-extension-vector"}: - fail( - "focused mobile builder runs must build only the selected smoke extension, " - f"got {sorted(mobile_focused_selection or [])}" - ) - - android_tasks = {"oliphaunt-react-native:mobile-build-android"} - android_jobs = ci_plan.plan_jobs_for_affected(set(), android_tasks) - if "extension-artifacts-native" not in android_jobs: - fail("Android mobile build must build selected native extension artifacts") - android_targets = extension_native_targets(android_jobs, android_tasks) - if android_targets != {"android-arm64-v8a", "android-x86_64"}: - fail(f"Android mobile build must only request Android extension artifacts, got {sorted(android_targets)}") - - android_e2e_jobs = ci_plan.plan_jobs_for_affected(set(), {"oliphaunt-react-native:mobile-e2e-android"}) - if android_e2e_jobs != ci_plan.BASE_JOBS: - fail(f"CI must not select Android E2E jobs; got {sorted(android_e2e_jobs)}") - - ios_tasks = {"oliphaunt-react-native:mobile-build-ios"} - ios_jobs = ci_plan.plan_jobs_for_affected(set(), ios_tasks) - if "extension-artifacts-native" not in ios_jobs: - fail("iOS mobile build must build selected native extension artifacts") - ios_targets = extension_native_targets(ios_jobs, ios_tasks) - if ios_targets != {"ios-xcframework"}: - fail(f"iOS mobile build must only request iOS extension artifacts, got {sorted(ios_targets)}") - - ios_e2e_jobs = ci_plan.plan_jobs_for_affected(set(), {"oliphaunt-react-native:mobile-e2e-ios"}) - if ios_e2e_jobs != ci_plan.BASE_JOBS: - fail(f"CI must not select iOS E2E jobs; got {sorted(ios_e2e_jobs)}") - - extension_tasks = {"extension-packages:assemble-release"} - extension_jobs = ci_plan.plan_jobs_for_affected(set(), extension_tasks) - full_targets = extension_native_targets(extension_jobs, extension_tasks) - expected_full_targets = { - target["target"] - for target in artifact_target_rows( - product="liboliphaunt-native", - kind="native-runtime", - published_only=True, - ) - if target.get("extension_artifacts", True) - } - if full_targets != expected_full_targets: - fail(f"extension package build must request all supported native extension artifacts, got {sorted(full_targets)}") - - swift_jobs = ci_plan.plan_jobs_for_affected(set(), {"oliphaunt-swift:package-artifacts"}) - if "liboliphaunt-native-ios" not in swift_jobs: - fail("Swift SDK package build must build the Apple liboliphaunt XCFramework") - swift_targets = ci_plan.native_target_subset_for_jobs(swift_jobs, {"oliphaunt-swift:package-artifacts"}) - if swift_targets != {"ios-xcframework"}: - fail(f"Swift SDK package build must only request the Apple XCFramework runtime target, got {sorted(swift_targets or [])}") - - kotlin_jobs = ci_plan.plan_jobs_for_affected(set(), {"oliphaunt-kotlin:package-artifacts"}) - if kotlin_jobs != ci_plan.BASE_JOBS | {"kotlin-sdk-package"}: - fail(f"Kotlin SDK package build must only package the Kotlin SDK, got {sorted(kotlin_jobs)}") - - rust_jobs = ci_plan.plan_jobs_for_affected(set(), {"oliphaunt-rust:package-artifacts"}) - if rust_jobs != ci_plan.BASE_JOBS | {"rust-sdk-package"}: - fail(f"Rust SDK package build must only package the Rust SDK, got {sorted(rust_jobs)}") - - js_jobs = ci_plan.plan_jobs_for_affected(set(), {"oliphaunt-js:package-artifacts"}) - if js_jobs != ci_plan.BASE_JOBS | {"js-sdk-package"}: - fail(f"TypeScript SDK package build must only package the TypeScript SDK, got {sorted(js_jobs)}") - - wasix_rust_jobs = ci_plan.plan_jobs_for_affected(set(), {"oliphaunt-wasix-rust:package-artifacts"}) - if wasix_rust_jobs != ci_plan.BASE_JOBS | {"wasix-rust-package"}: - fail(f"WASIX Rust binding package build must only package the binding crate, got {sorted(wasix_rust_jobs)}") - - -def main() -> int: - graph = release_graph() - policy = graph.get("policy") - if not isinstance(policy, dict): - fail("release metadata must define policy") - if policy.get("repository") != "f0rr0/oliphaunt": - fail("release policy repository must be f0rr0/oliphaunt") - if policy.get("versioning") != "independent": - fail("release policy must use independent versioning") - - check_release_metadata(graph) - check_release_planning(graph) - check_ci_policy() - check_release_workflow_policy() - check_ci_builder_planning() - print("release policy checks passed") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index 903d7171..3070484c 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -2,7 +2,6 @@ # Format: pathdomainmigration-decisionrationale # New Python files should be ported to Bun or deliberately added here with a specific migration decision. src/extensions/tools/check-extension-model.py extensions defer-extension-model-port generates and validates multi-language extension catalog, SDK metadata, docs, and evidence from one model -tools/policy/check-release-policy.py release-policy defer-release-graph-port guards CI and release policy against product metadata and the Bun CI planner during release-graph migration tools/release/check_artifact_targets.py release-metadata defer-release-graph-port validates release target coverage across workflow producers, product metadata, and package artifact handlers tools/release/check_consumer_shape.py release-consumer-shape defer-release-graph-port validates cross-SDK package/runtime/install shape from generated release fixtures and source invariants tools/release/check_release_metadata.py release-metadata defer-release-graph-port validates release metadata and publish-step wiring through cached Bun release graph query rows diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 8a9261eb..7cb1987f 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -625,7 +625,7 @@ def validate_graph_files() -> None: extension_model_moon = read_text("src/extensions/model/moon.yml") extension_artifacts_native_moon = read_text("src/extensions/artifacts/native/moon.yml") extension_artifacts_wasix_moon = read_text("src/extensions/artifacts/wasix/moon.yml") - release_policy = read_text("tools/policy/check-release-policy.py") + release_policy = read_text("tools/policy/check-release-policy.mjs") check_release_metadata_source = read_text("tools/release/check_release_metadata.py") if re.search(r"(?m)^import product_metadata$", check_release_metadata_source): fail("check_release_metadata.py must consume Bun release graph rows instead of importing product_metadata.py") @@ -641,7 +641,7 @@ def validate_graph_files() -> None: or "exactExtensionProducts(TOOL)" not in release_graph_query or "extension_products = extension_product_ids()" not in check_artifact_targets or "return set(extension_product_ids())" not in check_consumer_shape - or "modeled_extension_products = set(extension_product_ids())" not in release_policy + or "const modeledExtensionProducts = new Set(extensionProductIds());" not in release_policy or "import product_metadata" in release_policy or "import product_metadata" in check_artifact_targets or "import product_metadata" in check_consumer_shape @@ -698,7 +698,7 @@ def validate_graph_files() -> None: if ( "moon-projects [--project PROJECT]" not in release_graph_query or "export function moonProjectRows(" not in release_graph_source - or 'bun_json(["tools/release/release_graph_query.mjs", "moon-projects"])' not in release_policy + or 'bunJson(["tools/release/release_graph_query.mjs", "moon-projects"])' not in release_policy or "def moon_projects(" in release_policy or "moon query projects" in release_policy or 'graph.get("products")' in release_policy diff --git a/tools/release/release.py b/tools/release/release.py index e76b52eb..b50b7fe4 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -2193,7 +2193,7 @@ def run_product_publish_dry_runs(products: list[str], *, allow_dirty: bool, head def command_check(args: list[str]) -> None: - run(["python3", "tools/policy/check-release-policy.py"]) + run(["tools/dev/bun.sh", "tools/policy/check-release-policy.mjs"]) run(["tools/release/check_release_please_config.mjs"]) run(["python3", "tools/release/check_artifact_targets.py"]) run(["tools/dev/bun.sh", "tools/release/sync-release-pr.mjs", "--check"]) From 3222cbe802d26d4bc78709c8e3a58101435a2e9a Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 22:34:28 +0000 Subject: [PATCH 231/308] chore: port artifact target check to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 7 +- .../examples-ci-release-validation.md | 4 +- src/runtimes/node-direct/moon.yml | 2 +- tools/policy/python-entrypoints.allowlist | 1 - tools/release/check_artifact_targets.mjs | 1664 +++++++++++++++++ tools/release/check_artifact_targets.py | 1600 ---------------- tools/release/check_release_metadata.py | 6 +- tools/release/release.py | 2 +- 8 files changed, 1674 insertions(+), 1612 deletions(-) create mode 100644 tools/release/check_artifact_targets.mjs delete mode 100644 tools/release/check_artifact_targets.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index bc8e24f7..7bc2a679 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -2182,14 +2182,13 @@ until the current-state gates here are checked with fresh local evidence. - 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`. - The current inventory contains 6 tracked Python files: release orchestration, + The current inventory contains 5 tracked Python files: release orchestration, release/package validators, local registry publishing, and the extension model generator. New Python files must either be intentionally allowlisted or ported to Bun. The current migration order is: 1. port the remaining release checkers in the release-graph cluster - (`check_artifact_targets.py`, `check_release_metadata.py`, - `check_consumer_shape.py`) behind parity smokes and then remove their - Python compatibility imports; + (`check_release_metadata.py`, `check_consumer_shape.py`) behind parity + smokes and then remove their Python compatibility imports; 2. port `local_registry_publish.py` after artifact package generation and release metadata are Bun-native, preserving the local registry e2e path; 3. port `release.py` last, when the underlying validators and registry helpers diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index abb13254..ddbddbbc 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -100,7 +100,7 @@ the release/tooling surface after the runtime tool crate split. - On 2026-06-27, the open release DRY and SDK consistency tracker items were rechecked against current source. Fresh checks passed: `bash tools/policy/check-sdk-parity.sh`, - `python3 tools/release/check_artifact_targets.py`, + `tools/dev/bun.sh tools/release/check_artifact_targets.mjs`, `python3 tools/release/check_release_metadata.py`, `tools/dev/bun.sh tools/policy/assertions/assert-ci-workflows.mjs`, and `tools/dev/bun.sh examples/tools/check-examples.mjs`. The SDK parity gate @@ -285,7 +285,7 @@ the release/tooling surface after the runtime tool crate split. 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 + `check_consumer_shape.py`, and `check_artifact_targets.mjs`. 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 diff --git a/src/runtimes/node-direct/moon.yml b/src/runtimes/node-direct/moon.yml index 2300053e..f45be25c 100644 --- a/src/runtimes/node-direct/moon.yml +++ b/src/runtimes/node-direct/moon.yml @@ -38,7 +38,7 @@ tasks: - "liboliphaunt-native:check" inputs: - "/src/runtimes/node-direct/**/*" - - "/tools/release/check_artifact_targets.py" + - "/tools/release/check_artifact_targets.mjs" - "/tools/release/check-node-direct-release-assets.mjs" - "/tools/release/release-asset-validation.mjs" - "/tools/release/release-artifact-targets.mjs" diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index 3070484c..686a24d3 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -2,7 +2,6 @@ # Format: pathdomainmigration-decisionrationale # New Python files should be ported to Bun or deliberately added here with a specific migration decision. src/extensions/tools/check-extension-model.py extensions defer-extension-model-port generates and validates multi-language extension catalog, SDK metadata, docs, and evidence from one model -tools/release/check_artifact_targets.py release-metadata defer-release-graph-port validates release target coverage across workflow producers, product metadata, and package artifact handlers tools/release/check_consumer_shape.py release-consumer-shape defer-release-graph-port validates cross-SDK package/runtime/install shape from generated release fixtures and source invariants tools/release/check_release_metadata.py release-metadata defer-release-graph-port validates release metadata and publish-step wiring through cached Bun release graph query rows tools/release/local_registry_publish.py local-registry defer-local-registry-port publishes local Cargo, npm, Maven, and Swift registries from current release artifacts for e2e example validation diff --git a/tools/release/check_artifact_targets.mjs b/tools/release/check_artifact_targets.mjs new file mode 100644 index 00000000..c202688e --- /dev/null +++ b/tools/release/check_artifact_targets.mjs @@ -0,0 +1,1664 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; +import { existsSync, readFileSync, statSync } from "node:fs"; +import path from "node:path"; + +const ROOT = path.resolve(import.meta.dir, "../.."); +const PREFIX = "check_artifact_targets.mjs"; +const graphCache = new Map(); + +function fail(message) { + console.error(`${PREFIX}: ${message}`); + process.exit(1); +} + +function isObject(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function sorted(values) { + return [...values].sort(); +} + +function sameSet(left, right) { + if (left.size !== right.size) { + return false; + } + for (const value of left) { + if (!right.has(value)) { + return false; + } + } + return true; +} + +function isSubset(left, right) { + for (const value of left) { + if (!right.has(value)) { + return false; + } + } + return true; +} + +function formatList(values) { + return JSON.stringify(sorted(values)); +} + +function readText(repoPath) { + return readFileSync(path.join(ROOT, repoPath), "utf8"); +} + +function readToml(repoPath) { + const file = path.isAbsolute(repoPath) ? repoPath : path.join(ROOT, repoPath); + try { + const data = Bun.TOML.parse(readFileSync(file, "utf8")); + if (!isObject(data)) { + fail(`${path.relative(ROOT, file)} must contain a TOML table`); + } + return data; + } catch (error) { + fail(`${path.relative(ROOT, file)} is invalid TOML: ${error.message}`); + } +} + +function bunJson(args) { + const result = spawnSync("tools/dev/bun.sh", args, { + cwd: ROOT, + encoding: "utf8", + maxBuffer: 100 * 1024 * 1024, + }); + if (result.status !== 0) { + const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim(); + fail(output || `tools/dev/bun.sh ${args.join(" ")} failed`); + } + return JSON.parse(result.stdout); +} + +function releaseGraphRows(command, args = []) { + const cacheKey = JSON.stringify([command, args]); + if (!graphCache.has(cacheKey)) { + const value = bunJson(["tools/release/release_graph_query.mjs", command, ...args]); + if (!Array.isArray(value) || !value.every(isObject)) { + fail(`release graph ${command} query did not return an object list`); + } + graphCache.set(cacheKey, value); + } + return graphCache.get(cacheKey); +} + +function objectRow(row) { + return { + triple: null, + runner: null, + library_relative_path: null, + executable_relative_path: null, + npm_package: null, + npm_os: null, + npm_cpu: null, + npm_libc: null, + llvm_url: null, + extension_artifacts: true, + ...row, + }; +} + +function artifactTargetArgs({ product = null, kind = null, surface = null, publishedOnly = false } = {}) { + const args = []; + if (product !== null) { + args.push("--product", product); + } + if (kind !== null) { + args.push("--kind", kind); + } + if (surface !== null) { + args.push("--surface", surface); + } + if (publishedOnly) { + args.push("--published-only"); + } + return args; +} + +function artifactTargets({ product = null, kind = null, surface = null, publishedOnly = false } = {}) { + return releaseGraphRows( + "artifact-targets", + artifactTargetArgs({ product, kind, surface, publishedOnly }), + ).map(objectRow); +} + +function rawArtifactTargetTables() { + return releaseGraphRows("raw-artifact-targets").map((row) => ({ ...row })); +} + +function legacyCentralArtifactTargetRows() { + return releaseGraphRows("legacy-central-artifact-targets"); +} + +function moonReleaseMetadata(product) { + const rows = releaseGraphRows("moon-release-metadata", ["--product", product]); + if (rows.length !== 1) { + fail(`release graph moon-release-metadata returned ${rows.length} rows for ${product}`); + } + const row = { ...rows[0] }; + delete row.product; + return row; +} + +function extensionProductIds() { + const products = []; + for (const row of releaseGraphRows("extension-metadata")) { + const product = row.product; + if (typeof product !== "string" || product.length === 0) { + fail("release graph extension-metadata rows must declare non-empty products"); + } + products.push(product); + } + if (products.length !== new Set(products).size) { + fail("release graph extension-metadata query returned duplicate products"); + } + return products.sort(); +} + +function extensionArtifactTargets({ product = null, family = null, publishedOnly = false } = {}) { + const args = []; + if (product !== null) { + args.push("--product", product); + } + if (family !== null) { + args.push("--family", family); + } + if (publishedOnly) { + args.push("--published-only"); + } + return releaseGraphRows("extension-targets", args).map(objectRow); +} + +function productConfig(product) { + const rows = releaseGraphRows("product-configs", ["--product", product]); + if (rows.length !== 1) { + fail(`release graph product-configs returned ${rows.length} rows for ${product}`); + } + return { ...rows[0] }; +} + +function packagePath(product) { + const productPath = productConfig(product).path; + if (typeof productPath !== "string" || productPath.length === 0) { + fail(`release graph product-configs ${product}.path must be a non-empty string`); + } + return path.join(ROOT, productPath); +} + +function sdkPackageProducts() { + const products = []; + for (const row of releaseGraphRows("sdk-package-products")) { + const product = row.product; + if (typeof product !== "string" || product.length === 0) { + fail("release graph sdk-package-products rows must declare non-empty products"); + } + products.push(product); + } + if (products.length !== new Set(products).size) { + fail("release graph sdk-package-products query returned duplicate products"); + } + return products; +} + +function ciSdkPackageArtifactNames() { + const artifacts = []; + for (const row of releaseGraphRows("sdk-package-products")) { + const artifact = row.artifactName; + if (typeof artifact !== "string" || artifact.length === 0) { + fail("release graph sdk-package-products rows must declare non-empty artifactName"); + } + artifacts.push(artifact); + } + if (artifacts.length !== new Set(artifacts).size) { + fail("release graph sdk-package-products query returned duplicate artifacts"); + } + return artifacts; +} + +function readCurrentVersion(product) { + const rows = releaseGraphRows("product-versions", ["--product", product]); + if (rows.length !== 1) { + fail(`release graph product-versions returned ${rows.length} rows for ${product}`); + } + const version = rows[0].version; + if (typeof version !== "string" || version.length === 0) { + fail(`release graph product-versions ${product}.version must be a non-empty string`); + } + return version; +} + +function artifactTargetMatrix(matrix) { + const value = bunJson(["tools/release/artifact_target_matrix.mjs", matrix]); + if (!isObject(value) || !Array.isArray(value.include)) { + fail(`${matrix} matrix query did not return a matrix object`); + } + return value; +} + +function ciPlanFullRun({ wasmTarget = "all", nativeTarget = "all", mobileTarget = "all" } = {}) { + const value = bunJson([ + "tools/graph/ci_plan.mjs", + "plan-full", + "--wasm-target", + wasmTarget, + "--native-target", + nativeTarget, + "--mobile-target", + mobileTarget, + ]); + if (!isObject(value)) { + fail("CI planner full-run query did not return an object"); + } + return value; +} + +function tsTemplate(asset) { + return asset.replaceAll("{version}", "${version}"); +} + +function requireText(repoPath, text, message) { + if (!readText(repoPath).includes(text)) { + fail(message); + } +} + +function rejectText(repoPath, text, message) { + if (readText(repoPath).includes(text)) { + fail(message); + } +} + +function validateTargetShape() { + const targets = artifactTargets(); + if (targets.length === 0) { + fail("artifact target metadata must define targets"); + } + const rawTargets = new Map( + rawArtifactTargetTables() + .filter((raw) => isObject(raw) && typeof raw.id === "string") + .map((raw) => [raw.id, raw]), + ); + + const seenAssets = new Map(); + for (const target of targets) { + const rawTarget = rawTargets.get(target.id) ?? {}; + if (!target.asset.includes("{version}")) { + fail(`${target.id} asset template must contain {version}`); + } + if (target.published && !target.surfaces.includes("github-release") && !new Set(["native-tools"]).has(target.kind)) { + fail(`${target.id} is published but is not a GitHub release asset`); + } + if (!target.published) { + if (rawTarget.tier !== "planned") { + fail(`${target.id} is unpublished and must declare tier = "planned"`); + } + const reason = rawTarget.unsupported_reason; + if (typeof reason !== "string" || reason.trim().length < 40) { + fail(`${target.id} is unpublished and must declare a concrete unsupported_reason`); + } + } + if (["native-runtime", "broker-helper", "node-direct-addon"].includes(target.kind)) { + if (target.triple === null) { + fail(`${target.id} must declare a target triple`); + } + if (target.runner === null) { + fail(`${target.id} must declare the CI/release runner`); + } + } + if (target.kind === "wasix-aot-runtime") { + if (target.triple === null) { + fail(`${target.id} must declare a target triple`); + } + if (target.runner === null) { + fail(`${target.id} must declare the CI/release runner`); + } + if (target.llvm_url === null) { + fail(`${target.id} must declare llvm_url for AOT generation`); + } + } + if (["native-runtime", "node-direct-addon"].includes(target.kind) && target.library_relative_path === null) { + fail(`${target.id} must declare library_relative_path`); + } + if (target.kind === "native-runtime" && target.target.startsWith("android-")) { + const expectedPrefix = `jni/${target.target.replace(/^android-/u, "")}/`; + if (target.library_relative_path === null || !target.library_relative_path.startsWith(expectedPrefix)) { + fail( + `${target.id} library_relative_path must describe the Android release archive layout under ` + + `${expectedPrefix}, got ${target.library_relative_path}`, + ); + } + } + if (target.kind === "broker-helper" && target.executable_relative_path === null) { + fail(`${target.id} must declare executable_relative_path`); + } + if (target.surfaces.includes("github-release")) { + const dedupeKey = `${target.product}\0${target.asset}`; + const previous = seenAssets.get(dedupeKey); + if (previous !== undefined) { + fail(`${target.id} and ${previous} use the same asset template ${target.asset}`); + } + seenAssets.set(dedupeKey, target.id); + } + } +} + +function validateMoonRuntimeTargets() { + const centralTargets = legacyCentralArtifactTargetRows().map((raw) => raw.id); + if (centralTargets.length > 0) { + fail( + "artifact targets must be derived from Moon release metadata, " + + `not central release metadata: ${JSON.stringify(centralTargets)}`, + ); + } + + const runtimeTargetDirs = { + "liboliphaunt-native": "src/runtimes/liboliphaunt/native/targets", + "liboliphaunt-wasix": "src/runtimes/liboliphaunt/wasix/targets", + "oliphaunt-broker": "src/runtimes/broker/targets", + "oliphaunt-node-direct": "src/runtimes/node-direct/targets", + }; + for (const [product, directory] of Object.entries(runtimeTargetDirs)) { + const dir = path.join(ROOT, directory); + const files = existsSync(dir) + ? Array.from(new Bun.Glob("*.toml").scanSync({ cwd: dir })).sort() + : []; + if (files.length > 0) { + fail( + `${product} runtime artifact targets must be derived from Moon release metadata, ` + + "not product-local target TOML files: " + + files.map((file) => path.posix.join(directory, file)).join(", "), + ); + } + } + + const expectedPresets = { + "liboliphaunt-native": "liboliphaunt-native", + "liboliphaunt-wasix": "liboliphaunt-wasix", + "oliphaunt-broker": "broker-helper", + "oliphaunt-node-direct": "node-direct-addon", + }; + for (const [product, preset] of Object.entries(expectedPresets)) { + const release = moonReleaseMetadata(product); + const targets = release.artifactTargets; + if (!isObject(targets)) { + fail(`${product} Moon release metadata must declare artifactTargets`); + } + if (targets.preset !== preset) { + fail(`${product} Moon artifactTargets.preset must be ${JSON.stringify(preset)}`); + } + const published = targets.publishedTargets; + if (!Array.isArray(published) || published.length === 0 || !published.every((item) => typeof item === "string")) { + fail(`${product} Moon artifactTargets.publishedTargets must be a non-empty string list`); + } + } +} + +function wasmExtensionTargetId(runtimeTarget) { + return runtimeTarget === "portable" ? "wasix-portable" : runtimeTarget; +} + +function validateExtensionArtifactTargets() { + const extensionProducts = extensionProductIds(); + if (extensionProducts.length === 0) { + fail("exact-extension release products must be modeled as release products"); + } + + const expectedNativeTargets = new Set( + artifactTargets({ product: "liboliphaunt-native", kind: "native-runtime", publishedOnly: true }) + .filter((target) => target.extension_artifacts) + .map((target) => target.target), + ); + const expectedWasixTargets = new Set( + artifactTargets({ product: "liboliphaunt-wasix", publishedOnly: true }) + .filter((target) => target.kind === "wasix-runtime") + .map((target) => wasmExtensionTargetId(target.target)), + ); + if (expectedNativeTargets.size === 0) { + fail("published native runtime targets are required before extension artifacts can be published"); + } + if (expectedWasixTargets.size === 0) { + fail("published WASIX runtime targets are required before extension artifacts can be published"); + } + + for (const product of extensionProducts) { + const rows = extensionArtifactTargets({ product }); + const publishedNativeTargets = new Set(rows.filter((target) => target.family === "native" && target.published).map((target) => target.target)); + const declaredNativeTargets = new Set(rows.filter((target) => target.family === "native").map((target) => target.target)); + const publishedWasixTargets = new Set(rows.filter((target) => target.family === "wasix" && target.published).map((target) => target.target)); + if (!sameSet(declaredNativeTargets, expectedNativeTargets)) { + fail( + `${product} native extension target rows must cover published liboliphaunt native runtimes, ` + + `including explicit unpublished opt-outs: ${formatList(declaredNativeTargets)} vs ${formatList(expectedNativeTargets)}`, + ); + } + if (publishedNativeTargets.size === 0) { + fail(`${product} must publish at least one native extension artifact target`); + } + if (!isSubset(publishedNativeTargets, expectedNativeTargets)) { + fail( + `${product} published native extension targets must be published liboliphaunt native runtimes: ` + + `${formatList(publishedNativeTargets)} vs ${formatList(expectedNativeTargets)}`, + ); + } + if (!sameSet(publishedWasixTargets, expectedWasixTargets)) { + fail( + `${product} published WASIX extension targets must match published liboliphaunt WASIX runtimes: ` + + `${formatList(publishedWasixTargets)} vs ${formatList(expectedWasixTargets)}`, + ); + } + for (const row of rows) { + if (row.family === "native") { + const expectedKind = row.target === "ios-xcframework" || row.target.startsWith("android-") + ? "native-static-registry" + : "native-dynamic"; + if (row.kind !== expectedKind) { + fail(`${product} ${row.target} must use extension artifact kind ${expectedKind}, got ${row.kind}`); + } + if (row.published && row.kind === "native-static-registry") { + const staticRecipe = path.join(packagePath(product), "targets", "native-static-registry.toml"); + if (existsSync(staticRecipe) && statSync(staticRecipe).isFile()) { + const staticData = readToml(staticRecipe); + const status = staticData.status; + if (status !== "supported") { + fail( + `${product} publishes ${row.target} native static-registry artifacts, ` + + `but ${path.relative(ROOT, staticRecipe)} declares status=${JSON.stringify(status)}`, + ); + } + } + } + } + if (row.family === "wasix" && row.kind !== "wasix-runtime") { + fail(`${product} ${row.target} must use wasix-runtime extension artifacts`); + } + } + } +} + +function validateGithubAssetHelpers() { + requireText( + "tools/release/package-liboliphaunt-macos-assets.sh", + "liboliphaunt-${version}-${target_id}.tar.gz", + "macOS liboliphaunt target packager must emit the release-shaped macOS archive", + ); + requireText( + "tools/release/package-liboliphaunt-macos-assets.sh", + "target/liboliphaunt/release-assets", + "macOS liboliphaunt target packager must write into the release asset directory", + ); + requireText( + "tools/release/check_github_release_assets.mjs", + "expectedAssets", + "GitHub release asset checks must derive product assets from product-local artifact targets", + ); + requireText( + "tools/release/check-liboliphaunt-release-assets.mjs", + "allArtifactTargets", + "liboliphaunt release asset checks must derive required assets from product-local artifact targets", + ); + requireText( + "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", + ); + requireText( + "src/runtimes/liboliphaunt/native/tools/run-host-c-smoke.mjs", + "OLIPHAUNT_SMOKE_BIN_DIR", + "liboliphaunt C ABI smoke runner must support staged-release smoke binaries outside release layouts", + ); + for (const packager of [ + "tools/release/package-liboliphaunt-macos-assets.sh", + "tools/release/package-liboliphaunt-linux-assets.sh", + "tools/release/package-liboliphaunt-windows-assets.ps1", + ]) { + requireText( + packager, + "OLIPHAUNT_SMOKE_BIN_DIR", + `${packager} must smoke the staged release layout without writing smoke binaries into the archive`, + ); + requireText( + packager, + "run-host-c-smoke.mjs", + `${packager} must run the liboliphaunt C ABI smoke against the staged release layout`, + ); + requireText( + packager, + "plpgsql", + `${packager} must include embedded core PostgreSQL modules for native SDK materialization`, + ); + } +} + +function validateCiReleaseArtifacts() { + const ci = readText(".github/workflows/ci.yml"); + const release = readText(".github/workflows/release.yml"); + const requiredCiSnippets = new Map([ + ["Package liboliphaunt macOS release asset", "CI must build a release-shaped liboliphaunt macOS target archive"], + ["tools/release/package-liboliphaunt-macos-assets.sh", "CI must use the macOS liboliphaunt target packager"], + ["Package liboliphaunt Linux release asset", "CI must build release-shaped liboliphaunt Linux target archives"], + ["tools/release/package-liboliphaunt-linux-assets.sh", "CI must use the Linux liboliphaunt target packager"], + ["Package liboliphaunt Windows release asset", "CI must build a release-shaped liboliphaunt Windows target archive"], + ["package-liboliphaunt-windows-assets.ps1", "CI must use the Windows liboliphaunt target packager"], + ["Package liboliphaunt Android release asset", "CI must package release-shaped liboliphaunt Android target archives"], + ["Package liboliphaunt iOS release asset", "CI must package release-shaped liboliphaunt iOS target archives"], + ["tools/release/package-liboliphaunt-mobile-assets.sh", "CI must use the mobile liboliphaunt target packager"], + ["liboliphaunt-native-release-assets-${{ matrix.target }}", "CI must upload liboliphaunt release-shaped artifacts per target"], + ["liboliphaunt-native-release-assets:", "CI must aggregate complete public liboliphaunt release assets"], + ["Download liboliphaunt target release assets", "CI must aggregate liboliphaunt target archive outputs"], + [ + ".github/scripts/run-planned-moon-job.sh liboliphaunt-native-release-assets", + "CI must aggregate liboliphaunt native release assets through the Moon-modeled builder", + ], + ["Upload aggregate liboliphaunt release assets", "CI must upload complete liboliphaunt release assets for release consumption"], + ["Download Apple liboliphaunt release assets", "Swift SDK package artifacts must consume the Apple SwiftPM liboliphaunt release asset"], + ["liboliphaunt-native-release-assets-ios-xcframework", "Swift SDK package artifacts must download the Apple target release asset directly"], + ["OLIPHAUNT_SWIFT_RELEASE_ASSET_DIR", "Swift SDK package artifacts must render Package.swift.release from real liboliphaunt release assets in CI"], + [".github/scripts/run-planned-moon-job.sh broker-runtime", "CI must invoke the planned broker Moon job that includes release-shaped helper artifacts"], + ["oliphaunt-broker-release-assets-${{ matrix.target }}", "CI must upload broker helper release-shaped artifacts per target"], + [".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-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/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"], + ["Download exact-extension package artifacts", "Mobile build jobs must consume package-shaped exact-extension artifacts"], + ["Download WASIX exact-extension artifacts", "CI exact-extension package assembly must consume WASIX extension artifact builder outputs"], + ["pattern: liboliphaunt-wasix-extension-artifacts-*", "CI exact-extension package assembly must download every WASIX extension artifact target output"], + ["target/extensions/wasix/release-assets", "CI must use the shared WASIX exact-extension release asset staging layout"], + [ + "extension-artifacts-native:\n name: Builds / extension-native (${{ matrix.target }})\n needs:\n - affected", + "Native exact-extension artifact builders must be grouped by target", + ], + [ + "OLIPHAUNT_EXTENSION_PRODUCTS: ${{ matrix.extensions_csv }}", + "Exact-extension artifact builder jobs must pass the selected extension product set into the producer", + ], + [ + "liboliphaunt-native-extension-artifacts-${{ matrix.target }}", + "Native exact-extension artifact uploads must be addressable by target", + ], + [ + "liboliphaunt-native-extension-ccache-${{ matrix.target }}", + "Native exact-extension artifact builders must restore target-scoped compiler/build caches", + ], + [ + "liboliphaunt-wasix-extension-artifacts-${{ matrix.target }}", + "WASIX exact-extension artifact uploads must be addressable by target", + ], + [ + "MOON_CACHE=off .github/scripts/run-planned-moon-job.sh extension-artifacts-native", + "Native exact-extension artifact builders must inherit Moon source/check prerequisites inside the job", + ], + [ + "OLIPHAUNT_MOON_UPSTREAM=none MOON_CACHE=off .github/scripts/run-planned-moon-job.sh extension-artifacts-wasix", + "WASIX exact-extension artifact builders must consume downloaded runtime outputs, not re-run upstream producers", + ], + ["OLIPHAUNT_EXPO_REQUIRE_PREBUILT_EXTENSIONS", "Mobile build jobs must require prebuilt exact-extension artifacts instead of source-built extension fallbacks"], + ["OLIPHAUNT_EXPO_REQUIRE_SDK_ARTIFACTS", "Mobile build jobs must require staged SDK package artifacts instead of silent source fallbacks"], + ["OLIPHAUNT_EXPO_SDK_ARTIFACT_ROOT", "Mobile build jobs must resolve SDK artifacts from the staged package artifact root"], + ["OLIPHAUNT_EXPO_EXTENSION_ARTIFACT_ROOT", "Mobile build jobs must resolve exact-extension artifacts from the staged package artifact root"], + ["Validate Android mobile app artifacts", "Android mobile build jobs must inspect the built app for exact selected-extension contents"], + ["Validate iOS mobile app artifacts", "iOS mobile build jobs must inspect the built app for exact selected-extension contents"], + [ + "check-staged-artifacts.mjs --require-mobile android --require-mobile-prebuilt-extensions", + "Android mobile artifact validation must require prebuilt exact-extension package inputs", + ], + [ + "check-staged-artifacts.mjs --require-mobile ios --require-mobile-prebuilt-extensions", + "iOS mobile artifact validation must require prebuilt exact-extension package inputs", + ], + ["OLIPHAUNT_EXPO_IOS_OLIPHAUNT_XCFRAMEWORK", "iOS mobile build jobs must consume the linked liboliphaunt XCFramework artifact"], + ["liboliphaunt-wasix-release-assets:", "CI must aggregate WASIX portable and AOT outputs into public release assets"], + [ + "liboliphaunt_wasix_aot_runtime_matrix: ${{ steps.plan.outputs.liboliphaunt_wasix_aot_runtime_matrix }}", + "CI affected planning must emit the WASIX AOT target matrix without a separate planning job", + ], + [ + "matrix: ${{ fromJson(needs.affected.outputs.liboliphaunt_wasix_aot_runtime_matrix", + "WASIX AOT builders must consume the affected-plan target matrix directly", + ], + [ + "contains(fromJson(needs.affected.outputs.jobs), 'liboliphaunt-wasix-aot')", + "CI must only build WASIX AOT artifacts when the affected planner selected AOT work", + ], + [ + "contains(fromJson(needs.affected.outputs.jobs), 'liboliphaunt-wasix-release-assets')", + "CI must only aggregate WASIX release assets when the affected planner selected release aggregation", + ], + [".github/scripts/run-planned-moon-job.sh liboliphaunt-wasix-release-assets", "CI must package WASIX public release assets through the planned Moon task"], + [ + "target/oliphaunt-wasix/wasix-build/work/icu-wasix/share/icu/**", + "CI must pass the WASIX ICU sidecar produced by the portable runtime job into release asset packaging", + ], + ["target/oliphaunt-wasix/release-assets", "CI must upload WASIX public release assets"], + ["Stage target AOT artifact envelope", "WASIX AOT builders must upload a deterministic artifact envelope"], + ["target-triple.txt", "WASIX AOT artifact envelopes must identify their target triple explicitly"], + ["target/oliphaunt-wasix/aot-upload/**", "WASIX AOT upload must use the staged artifact envelope, not an implicit target path"], + ["Invalid WASIX AOT artifact envelope", "WASIX AOT consumers must validate the downloaded artifact envelope before restoring it"], + ]); + for (const [snippet, message] of requiredCiSnippets.entries()) { + if (!ci.includes(snippet)) { + fail(message); + } + } + for (const artifact of ciSdkPackageArtifactNames()) { + if (!ci.includes(artifact)) { + fail(`CI must upload SDK package artifact ${artifact}`); + } + } + for (const product of sdkPackageProducts()) { + if (!ci.includes(`target/sdk-artifacts/${product}`)) { + fail(`CI must use the shared SDK artifact staging layout for ${product}`); + } + } + requireText( + ".github/workflows/release.yml", + 'tools/dev/bun.sh tools/release/release_graph_query.mjs ci-artifact-names --product "$product" --family sdk-package --format lines', + "release workflow must derive SDK package artifact names from release metadata", + ); + requireText( + ".github/workflows/release.yml", + 'tools/dev/bun.sh tools/release/release_graph_query.mjs ci-products --family sdk-package --products-json "$PRODUCTS_JSON" --format lines', + "release workflow must derive selected SDK package products from release metadata", + ); + for (const legacyEnv of [ + "PRODUCT_OLIPHAUNT_RUST", + "PRODUCT_OLIPHAUNT_SWIFT", + "PRODUCT_OLIPHAUNT_KOTLIN", + "PRODUCT_OLIPHAUNT_REACT_NATIVE", + "PRODUCT_OLIPHAUNT_JS", + "PRODUCT_OLIPHAUNT_WASIX_RUST", + ]) { + rejectText( + ".github/workflows/release.yml", + legacyEnv, + `release workflow must not hard-code SDK product selection with ${legacyEnv}`, + ); + } + requireText( + "src/runtimes/broker/moon.yml", + 'tags: ["release", "artifact", "ci-broker-runtime"]', + "Broker release-assets must be selected by the ci-broker-runtime Moon tag", + ); + requireText( + "src/runtimes/node-direct/moon.yml", + 'tags: ["release", "artifact", "ci-node-direct"]', + "Node direct release-assets must be selected by the ci-node-direct Moon tag", + ); + requireText( + "src/runtimes/node-direct/moon.yml", + "/target/oliphaunt-node-direct/npm-packages/**/*", + "Node direct Moon release-assets task must declare optional npm tarballs as outputs", + ); + requireText( + "src/runtimes/node-direct/tools/build-node-addon.sh", + "Node direct optional npm package staged", + "Node direct CI builder must stage optional npm tarballs for release publishing", + ); + requireText( + ".github/workflows/release.yml", + "Download Node direct optional npm packages", + "release workflow must download Node direct optional npm package artifacts from CI", + ); + requireText( + "tools/release/release.py", + "node_direct_optional_npm_tarballs", + "Node direct release publish must validate staged optional npm tarballs", + ); + requireText( + "tools/release/release.py", + 'run(["npm", "publish", str(tarball), "--access", "public", "--provenance"])', + "Node direct optional npm publish must publish CI-built tarballs directly", + ); + for (const projectId of sdkPackageProducts()) { + const moonFile = projectId === "oliphaunt-wasix-rust" + ? "src/bindings/wasix-rust/moon.yml" + : `src/sdks/${projectId === "oliphaunt-js" ? "js" : projectId.replace(/^oliphaunt-/u, "")}/moon.yml`; + requireText( + moonFile, + `tools/release/build-sdk-ci-artifacts.mjs ${projectId}`, + `${projectId} package task must stage publishable SDK artifacts`, + ); + requireText( + moonFile, + `/target/sdk-artifacts/${projectId}/**/*`, + `${projectId} package task must declare staged SDK package artifacts as Moon outputs`, + ); + } + const focusedWasixJobs = new Set(ciPlanFullRun({ wasmTarget: "linux-x64-gnu" }).jobs ?? []); + if (!sameSet(focusedWasixJobs, new Set(["affected", "liboliphaunt-wasix-runtime", "liboliphaunt-wasix-aot"]))) { + fail( + "focused WASIX target runs must build only the portable runtime and requested AOT producer, " + + `got ${formatList(focusedWasixJobs)}`, + ); + } + requireText( + "tools/graph/ci_plan.mjs", + "extension_artifacts_wasix_matrix:", + "CI planner must model WASIX exact-extension artifact matrix output", + ); + requireText( + "tools/graph/ci_plan.mjs", + 'jobs.has("extension-artifacts-wasix")', + "CI planner must emit WASIX exact-extension rows only when the WASIX extension builder is selected", + ); + requireText( + "tools/graph/ci_plan.mjs", + 'extensionArtifactsWasixMatrix("all", selectedExtensionProducts', + "WASIX extension artifacts are portable and must use the portable selector, not the AOT target selector", + ); + const wasixReleaseNeeds = [ + "liboliphaunt-wasix-release-assets:", + " name: Builds / liboliphaunt-wasix-release-assets", + " needs:", + " - affected", + " - liboliphaunt-wasix-runtime", + " - liboliphaunt-wasix-aot", + ].join("\n"); + if (!ci.includes(wasixReleaseNeeds)) { + fail("WASIX release asset builder must consume portable and AOT runtime builders"); + } + if (ci.includes('OLIPHAUNT_EXPO_MOBILE_EXTENSIONS: ""')) { + fail('mobile build jobs must not disable selected extensions with OLIPHAUNT_EXPO_MOBILE_EXTENSIONS=""'); + } + if (ci.includes("run: cargo run -p xtask -- release package-assets")) { + fail("CI must not bypass Moon for WASIX release asset packaging"); + } + if (ci.includes("run: src/runtimes/liboliphaunt/wasix/tools/build-runtime-portable.sh")) { + fail("CI must not bypass Moon for portable WASIX runtime builds"); + } + if (ci.includes("target/oliphaunt-wasix/aot/${{ matrix.target }}/**")) { + fail("WASIX AOT uploads must use the explicit target-triple artifact envelope"); + } + if (ci.includes("run: src/runtimes/liboliphaunt/wasix/tools/build-aot-target.sh")) { + fail("CI must not bypass Moon for WASIX AOT builds"); + } + if (ci.indexOf("mobile-build-android:") < ci.indexOf("mobile-extension-packages:")) { + fail("mobile exact-extension package producer must be declared before mobile Android build consumers"); + } + if (!ci.includes("mobile-build-android:\n name: Builds / mobile-android (${{ matrix.target }})\n needs:\n - affected\n - mobile-extension-packages\n - liboliphaunt-native-android")) { + fail("Android mobile build must depend on mobile-extension-packages and the Android liboliphaunt target builder"); + } + if (!ci.includes("mobile-build-ios:\n name: Builds / mobile-ios\n needs:\n - affected\n - mobile-extension-packages\n - liboliphaunt-native-ios")) { + fail("iOS mobile build must depend on mobile-extension-packages and the iOS liboliphaunt target builder"); + } + if (!ci.includes("mobile-build-android:\n name: Builds / mobile-android (${{ matrix.target }})\n needs:\n - affected\n - mobile-extension-packages\n - liboliphaunt-native-android\n - kotlin-sdk-package\n - react-native-sdk-package")) { + fail("Android mobile build must depend on Android runtime, Kotlin, and React Native package artifacts"); + } + requireText( + ".github/workflows/ci.yml", + "matrix: ${{ fromJson(needs.affected.outputs.react_native_android_mobile_app_matrix) }}", + "Android mobile build must use the React Native Android runtime target matrix", + ); + requireText( + ".github/workflows/ci.yml", + "react-native-mobile-android-app-${{ matrix.target }}", + "Android mobile build artifacts must be target-specific", + ); + if (!ci.includes("mobile-build-ios:\n name: Builds / mobile-ios\n needs:\n - affected\n - mobile-extension-packages\n - liboliphaunt-native-ios\n - react-native-sdk-package\n - swift-sdk-package")) { + fail("iOS mobile build must depend on iOS runtime, React Native, and Swift package artifacts"); + } + if (!ci.includes("swift-sdk-package:\n name: Builds / swift-sdk\n needs:\n - affected\n - liboliphaunt-native-ios")) { + fail("Swift SDK package artifacts must depend on the iOS native target builder that produces the Apple release asset"); + } + requireText( + "tools/graph/ci_plan.mjs", + 'jobs.has("swift-sdk-package")', + "CI affected planner must make Swift SDK package builds imply liboliphaunt target asset producers", + ); + requireText( + "tools/graph/ci_plan.mjs", + 'targets.add("ios-xcframework")', + "CI affected planner must narrow Swift SDK liboliphaunt target builds to the Apple SwiftPM target when possible", + ); + requireText( + "src/sdks/react-native/tools/expo-runner-common.sh", + "expo_single_sdk_artifact_file", + "React Native mobile runners must have a shared required-SDK-artifact resolver", + ); + requireText( + "src/sdks/react-native/tools/expo-android-runner.sh", + "install_kotlin_sdk_maven_artifacts_if_required", + "Android mobile runner must consume staged Kotlin Maven artifacts when CI requires SDK artifacts", + ); + requireText( + "src/sdks/react-native/tools/expo-ios-runner.sh", + "prepare_swift_sdk_artifact_git_repo_if_required", + "iOS mobile runner must consume the staged Swift source artifact when CI requires SDK artifacts", + ); + requireText( + "tools/release/build-sdk-ci-artifacts.mjs", + "publishAndroidReleasePublicationToMavenLocal", + "Kotlin SDK package builder must stage a Maven repository layout for Android consumers", + ); + requireText( + "tools/release/build-sdk-ci-artifacts.mjs", + 'path.join(artifactRoot, "maven")', + "Kotlin SDK package builder must stage Maven artifacts under target/sdk-artifacts/oliphaunt-kotlin/maven", + ); + requireText( + "tools/release/build-sdk-ci-artifacts.mjs", + '"tools/release/check-staged-artifacts.mjs", "--require-sdk-product", product', + "SDK package builders must validate staged package artifacts for runtime/extension payload leaks", + ); + rejectText( + "tools/release/build-sdk-ci-artifacts.mjs", + "outputs/aar/*-release.aar", + "Kotlin SDK package staging must not copy loose AARs; the staged Maven repository is the package boundary", + ); + requireText( + "tools/release/build-sdk-ci-artifacts.mjs", + "oliphaunt-android-gradle-plugin:publishToMavenLocal", + "Kotlin SDK package builder must stage the Android Gradle plugin Maven artifact", + ); + requireText( + "src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh", + 'check-staged-artifacts.mjs "${validation_args[@]}"', + "mobile exact-extension package assembly must validate the staged package manifests and checksums it selected", + ); + requireText( + "src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh", + "OLIPHAUNT_EXTENSION_PACKAGE_PRODUCTS must list selected exact-extension products for mobile packaging", + "mobile exact-extension package assembly must fail closed without an explicit selected product list", + ); + rejectText( + "src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh", + "args+=(--all)", + "mobile exact-extension package assembly must not fall back to all extension products", + ); + requireText( + "src/runtimes/liboliphaunt/native/moon.yml", + "tools/release/package-liboliphaunt-aggregate-assets.sh", + "liboliphaunt native aggregate assets must have one Moon-modeled packager/checker entrypoint", + ); + requireText( + "tools/release/check-staged-artifacts.mjs", + "validateReleaseArchivePayload(assetPath)", + "staged exact-extension artifact checks must reject placeholder files that are not readable release archives", + ); + requireText( + "tools/graph/ci_plan.mjs", + 'jobs.add("mobile-extension-packages")', + "affected planner must select target-scoped exact-extension packages whenever mobile jobs are selected", + ); + rejectText( + "tools/graph/ci_plan.mjs", + 'if "extension-artifacts-native" in jobs:\n jobs.add("liboliphaunt-native")', + "affected planner must not create a coarse native-runtime waterfall for exact-extension artifact builds", + ); + rejectText( + ".github/workflows/release.yml", + "product_liboliphaunt_native == 'true' || steps.release_plan.outputs.product_oliphaunt_swift == 'true'", + "Swift SDK releases must consume staged Swift package artifacts, not force aggregate liboliphaunt asset downloads", + ); + requireText( + ".github/workflows/release.yml", + "steps.release_plan.outputs.product_liboliphaunt_native == 'true' }}", + "release workflow must still download aggregate liboliphaunt assets for liboliphaunt-native releases", + ); + requireText( + "tools/release/release.py", + "prepare_staged_swift_release_manifest", + "Swift SDK release must use the Package.swift.release produced by the SDK package builder", + ); + requireText( + "tools/release/release.py", + "def validate_staged_sdk_package", + "release dry-runs must validate staged SDK package artifacts before publish checks", + ); + for (const productId of sdkPackageProducts()) { + requireText( + "tools/release/release.py", + `validate_staged_sdk_package("${productId}")`, + `${productId} release dry-run must validate the staged SDK package artifact`, + ); + } + requireText( + ".github/scripts/run-planned-moon-job.sh", + "OLIPHAUNT_MOON_UPSTREAM", + "CI must be able to run downloaded-artifact consumer jobs without re-running Moon upstream producer tasks", + ); + for (const consumerJob of [ + "extension-packages", + "mobile-extension-packages", + "liboliphaunt-native-release-assets", + "liboliphaunt-wasix-aot", + "liboliphaunt-wasix-release-assets", + "mobile-build-android", + "mobile-build-ios", + ]) { + requireText( + ".github/workflows/ci.yml", + `OLIPHAUNT_MOON_UPSTREAM=none MOON_CACHE=off .github/scripts/run-planned-moon-job.sh ${consumerJob}`, + `${consumerJob} must consume downloaded builder artifacts without re-running upstream producer tasks`, + ); + } + if (ci.includes("Stage mobile exact-extension packages")) { + fail("mobile build jobs must not locally stage extension packages; they must consume extension-package builder artifacts"); + } + if (ci.includes("extension-packages-native")) { + fail("CI must not keep a native-only extension package shortcut; mobile must consume target-scoped exact-extension packages"); + } + if (ci.includes("oliphaunt-extension-native-package-artifacts")) { + fail("CI must not publish native-only exact-extension package artifacts"); + } + if (ci.includes("target/extension-artifacts-native")) { + fail("CI must not use a separate native-only extension package staging layout"); + } + requireText( + "tools/release/release.py", + "requires staged exact-extension package artifacts", + "release CLI must fail closed when extension releases lack staged CI-built package artifacts", + ); + requireText( + "tools/release/release.py", + "validate_extension_release_package", + "release CLI must validate staged exact-extension package manifests before dry-run or publish", + ); + requireText( + "tools/release/release.py", + "staged_native_targets != declared_native_targets", + "release CLI must reject partial native exact-extension package artifacts", + ); + requireText( + "tools/release/release.py", + "staged_wasix_targets != declared_wasix_targets", + "release CLI must reject partial WASIX exact-extension package artifacts", + ); + requireText( + "tools/release/release.py", + "sha256_file(asset_path) != sha_value", + "release CLI must verify staged exact-extension artifact checksums", + ); + requireText( + "tools/release/release.py", + "validate_checksum_manifest(checksum_manifest, asset_dir)", + "release CLI must verify staged exact-extension checksum manifests exactly", + ); + requireText( + "tools/release/build-extension-ci-artifacts.mjs", + "nativeAssetName(product, version", + "exact-extension package artifacts must be named by extension product version", + ); + requireText( + "src/extensions/artifacts/native/tools/package-release-assets.sh", + "native-extension-assets.tsv", + "native exact-extension artifact producers must emit a target-addressed native asset index", + ); + requireText( + "src/extensions/artifacts/native/tools/package-release-assets.sh", + "OLIPHAUNT_EXTENSION_PRODUCT", + "native exact-extension artifact producers must support product-scoped builds", + ); + requireText( + "src/extensions/artifacts/wasix/tools/package-release-assets.sh", + "OLIPHAUNT_EXTENSION_PRODUCT", + "WASIX exact-extension artifact producers must support product-scoped builds", + ); + requireText( + "tools/release/build-extension-ci-artifacts.mjs", + "nativeAssetsFromTargetIndexes", + "exact-extension package staging must consume target-addressed native asset indexes", + ); + requireText( + "tools/release/build-extension-ci-artifacts.mjs", + 'publishedTargetIds("native")', + "exact-extension package staging must only read declared published native target artifact indexes", + ); + requireText( + "tools/release/build-extension-ci-artifacts.mjs", + 'publishedTargetIds("wasix")', + "exact-extension package staging must only read declared published WASIX target artifact indexes", + ); + requireText( + "tools/release/build-extension-ci-artifacts.mjs", + "if (requireNativeTargets.size > 0 && !requireNativeTargets.has(target))", + "mobile exact-extension package staging must filter out native targets that the mobile build did not request", + ); + requireText( + "tools/release/build-extension-ci-artifacts.mjs", + "indexContainsSqlName(productIndex, sqlName)", + "exact-extension package staging must not let stale empty product-scoped native indexes shadow target-level indexes", + ); + requireText( + "tools/release/build-extension-ci-artifacts.mjs", + "-manifest.json", + "exact-extension package artifacts must publish a machine-readable release manifest", + ); + requireText( + "tools/release/check_github_release_assets.mjs", + "verifyReleaseAssets", + "GitHub release verification must derive exact-extension asset expectations from staged extension package manifests", + ); + requireText( + "tools/release/verify_github_release_attestations.mjs", + "exact-extension-artifact", + "Release attestation verification must include exact-extension artifact products", + ); + requireText( + "tools/release/release.py", + "liboliphaunt-native requires staged release assets", + "release CLI must fail closed when liboliphaunt releases lack staged CI-built runtime artifacts", + ); + requireText( + "tools/release/release.py", + "liboliphaunt-wasix requires staged release assets", + "release CLI must fail closed when WASIX releases lack staged CI-built runtime artifacts", + ); + requireText( + "tools/release/release.py", + "requires staged JSR source", + "release CLI must fail closed when TypeScript JSR release artifacts are not staged", + ); + requireText( + ".github/workflows/release.yml", + "Download SDK package artifacts", + "release workflow must download SDK package artifacts from the CI workflow before publishing", + ); + requireText( + ".github/workflows/release.yml", + "Download liboliphaunt release assets", + "release workflow must download complete liboliphaunt assets from the CI workflow before publishing", + ); + requireText( + ".github/workflows/release.yml", + "Download native helper release assets", + "release workflow must download broker and Node direct helper assets from the CI workflow before publishing those helper products", + ); + requireText( + ".github/workflows/release.yml", + "Download WASIX release assets", + "release workflow must download complete WASIX runtime release assets from the CI workflow before publishing", + ); + requireText( + ".github/workflows/release.yml", + "Upload WASIX GitHub release assets", + "release workflow must publish WASIX GitHub assets through the liboliphaunt-wasix runtime product", + ); + requireText( + ".github/workflows/release.yml", + "--product liboliphaunt-wasix --step github-release-assets", + "release workflow must publish WASIX GitHub assets through the liboliphaunt-wasix runtime product", + ); + requireText( + ".github/workflows/release.yml", + "--product liboliphaunt-wasix --step crates-io", + "release workflow must publish liboliphaunt-wasix Cargo artifact packages before the WASIX Rust binding", + ); + requireText( + ".github/workflows/release.yml", + 'tools/dev/bun.sh tools/release/release_graph_query.mjs ci-artifact-names --product "$product" --kind "$kind" --family release-assets --format lines', + "release workflow must derive native helper release artifact names from target metadata", + ); + requireText( + ".github/workflows/release.yml", + '[ "$PRODUCT_OLIPHAUNT_BROKER" = "true" ]', + "broker helper releases must download broker artifacts from CI", + ); + requireText( + ".github/workflows/release.yml", + '[ "$PRODUCT_OLIPHAUNT_NODE_DIRECT" = "true" ]', + "Node direct helper releases must download Node direct artifacts from CI", + ); + requireText( + ".github/workflows/release.yml", + "tools/dev/bun.sh tools/release/release_graph_query.mjs ci-artifact-names --product oliphaunt-node-direct --kind node-direct-addon --family npm-package --format lines", + "release workflow must derive Node direct npm package artifact names from target metadata", + ); + requireText( + ".github/workflows/release.yml", + "target/oliphaunt-broker/release-assets", + "release workflow must download broker artifacts into the canonical broker release asset root", + ); + requireText( + ".github/workflows/release.yml", + "target/oliphaunt-node-direct/release-assets", + "release workflow must download Node direct artifacts into the canonical Node direct release asset root", + ); + requireText( + ".github/workflows/release.yml", + "--product liboliphaunt-native --step npm", + "release workflow must publish liboliphaunt artifact packages to npm before dependent SDK packages", + ); + requireText( + ".github/workflows/release.yml", + "--product oliphaunt-broker --step npm", + "release workflow must publish broker artifact packages to npm before dependent SDK packages", + ); + requireText( + ".github/workflows/release.yml", + "--product liboliphaunt-native --step crates-io", + "release workflow must publish liboliphaunt native Cargo artifact packages before dependent Rust SDK packages", + ); + requireText( + ".github/workflows/release.yml", + "--product oliphaunt-broker --step crates-io", + "release workflow must publish broker artifact packages to crates.io before dependent Rust SDK packages", + ); + requireText( + "tools/release/release.py", + "npm-package-sources", + "npm artifact packages must be assembled from staged package sources instead of mutating checked-in package directories", + ); + requireText( + "tools/release/release.py", + "package-liboliphaunt-cargo-artifacts.mjs", + "liboliphaunt native Cargo artifact packages must be generated from staged native release assets", + ); + requireText( + "tools/release/release.py", + "package_broker_cargo_artifacts.mjs", + "broker Cargo artifact packages must be generated from staged broker release assets", + ); + requireText( + "tools/release/release.py", + "package_liboliphaunt_wasix_cargo_artifacts.mjs", + "liboliphaunt-wasix Cargo artifact packages must be generated from staged WASIX release assets", + ); + requireText( + "tools/release/release.py", + "liboliphaunt_wasix_cargo_artifact_crates", + "release CLI must package and validate direct WASIX Cargo artifact crates", + ); + requireText( + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", + "CRATES_IO_MAX_BYTES", + "WASIX Cargo artifact packager must enforce the crates.io package size limit", + ); + requireText( + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", + "validateCrateSize", + "WASIX Cargo artifact packager must validate direct artifact crate sizes", + ); + rejectText( + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", + "DEFAULT_PART_COUNT", + "WASIX Cargo artifact packager must not generate reserved part crates", + ); + requireText( + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", + "wasixExtensionAotPartPackageName", + "WASIX Cargo artifact packager may only generate named part crates for oversized extension AOT artifacts", + ); + requireText( + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", + "EXTENSION_AOT_SPLIT_THRESHOLD_BYTES", + "WASIX Cargo artifact packager must keep extension AOT part splitting behind an explicit size threshold", + ); + requireText( + "tools/release/release.py", + "artifact_npm_package_targets", + "liboliphaunt and broker npm artifact packages must derive package targets from artifact target metadata", + ); + rejectText( + "tools/release/release.py", + "LIBOLIPHAUNT_NPM_PACKAGE_DIRS", + "liboliphaunt npm package target mapping must not be duplicated outside artifact target metadata", + ); + rejectText( + "tools/release/release.py", + "BROKER_NPM_PACKAGE_DIRS", + "broker npm package target mapping must not be duplicated outside artifact target metadata", + ); + requireText( + "tools/release/release.py", + "required_runtime_member_paths", + "liboliphaunt npm artifact packages must include the selected platform runtime tree", + ); + requireText( + "tools/release/package-liboliphaunt-cargo-artifacts.mjs", + "optimizeNativePayload(", + "liboliphaunt Cargo artifact packages must prune and validate native runtime payloads before splitting", + ); + rejectText( + ".github/workflows/release.yml", + "target/release-assets/native", + "release workflow must not stage native helper artifacts in a generic release-assets/native bucket", + ); + requireText( + "tools/release/build-sdk-ci-artifacts.mjs", + 'stageJsrSourceWorkspace(packageShapeDir, path.join(artifactRoot, "jsr-source"))', + "TypeScript SDK builder must stage source for JSR publishing in addition to the npm tarball", + ); + requireText( + "tools/release/release.py", + 'staged_jsr_source_dir("oliphaunt-js")', + "TypeScript SDK release must publish JSR from staged CI-built source artifacts", + ); + requireText( + "tools/release/release.py", + "validate_staged_npm_package_tarball", + "npm SDK release steps must validate CI-built package tarballs before dry-run or publish", + ); + requireText( + "tools/release/release.py", + "must not contain workspace: dependency specifiers", + "staged npm SDK package validation must reject unpublished workspace protocol specs", + ); + requireText( + "tools/release/release.py", + "verify_staged_cargo_crate_identity", + "Cargo SDK release steps must verify staged CI-built .crate identity before dry-run or publish", + ); + for (const forbidden of [ + "tools/release/package-liboliphaunt-assets.sh", + "tools/release/package-broker-assets.sh", + "src/runtimes/node-direct/tools/build-node-addon.sh", + "src/extensions/artifacts/native/tools/package-release-assets.sh", + "src/extensions/artifacts/wasix/tools/package-release-assets.sh", + "tools/release/build-extension-ci-artifacts.mjs", + "src/sdks/kotlin/tools/check-sdk.sh", + "src/sdks/react-native/tools/check-sdk.sh", + "src/sdks/js/tools/check-sdk.sh", + 'xtask(["release", "stage"])', + '"--staged-wasm"', + '"--staged-wasix-runtime"', + "OLIPHAUNT_RELEASE_REQUIRE_STAGED_", + "OLIPHAUNT_WASM_RELEASE_STAGED", + ]) { + rejectText( + "tools/release/release.py", + forbidden, + `release CLI must consume staged CI artifacts, not retain local fallback path ${forbidden}`, + ); + } + for (const forbidden of ["OLIPHAUNT_RELEASE_REQUIRE_STAGED_", "OLIPHAUNT_WASM_RELEASE_STAGED"]) { + rejectText( + ".github/workflows/release.yml", + forbidden, + `release workflow must not rely on staged-mode env flag ${forbidden}; release CLI is staged-artifact-only`, + ); + } + rejectText( + ".github/workflows/release.yml", + "Build liboliphaunt Linux asset", + "release workflow must not rebuild liboliphaunt Linux assets; it must consume CI artifacts", + ); + rejectText( + ".github/workflows/release.yml", + "Build liboliphaunt Windows asset", + "release workflow must not rebuild liboliphaunt Windows assets; it must consume CI artifacts", + ); + rejectText( + ".github/workflows/release.yml", + "Build broker Linux asset", + "release workflow must not rebuild broker Linux assets; it must consume CI artifacts", + ); + rejectText( + ".github/workflows/release.yml", + "Build Node direct native asset", + "release workflow must not rebuild Node direct assets; it must consume CI artifacts", + ); + requireText( + ".github/scripts/download-build-artifacts.mjs", + "artifactPresent", + "shared artifact downloader must select a successful CI run containing every requested artifact", + ); + requireText( + ".github/scripts/download-build-artifacts.mjs", + "requiredJobSuccess", + "shared artifact downloader must support the builder-gate handoff when non-builder checks fail", + ); + requireText( + ".github/workflows/release.yml", + 'require-workflow-success.sh CI "$RELEASE_HEAD_SHA" 7200 --job Builds', + "release workflow must require the selected release commit CI artifact builder gate instead of the whole workflow conclusion", + ); + requireText( + ".github/workflows/release.yml", + "--job Builds", + "release workflow artifact downloads must select artifacts from a run whose builds job succeeded", + ); + requireText( + ".github/scripts/download-wasix-runtime-build-artifacts.mjs", + 'args.push("--required-job", "Builds", "--all-targets")', + "WASIX runtime artifact handoff must download from a CI run whose builds job succeeded", + ); + requireText( + "tools/xtask/src/asset_io.rs", + "run_has_required_job_success", + "xtask WASIX artifact downloads must support filtering selected release runs by required builder job", + ); + if (release.indexOf("Download SDK package artifacts") > release.indexOf("Validate selected release product dry-runs")) { + fail("release workflow must stage SDK artifacts before selected release product dry-runs"); + } + if (release.indexOf("Download liboliphaunt release assets") > release.indexOf("Validate selected release product dry-runs")) { + fail("release workflow must stage liboliphaunt runtime artifacts before selected release product dry-runs"); + } + if (release.indexOf("Download native helper release assets") > release.indexOf("Validate selected release product dry-runs")) { + fail("release workflow must stage native helper artifacts before selected release product dry-runs"); + } + if (release.indexOf("Download WASIX release assets") > release.indexOf("Validate selected release product dry-runs")) { + fail("release workflow must stage WASIX runtime release assets before selected release product dry-runs"); + } + if (release.indexOf("--product liboliphaunt-wasix --step crates-io") > release.indexOf("--product oliphaunt-wasix-rust --step crates-io")) { + fail("release workflow must publish liboliphaunt-wasix Cargo artifact crates before oliphaunt-wasix"); + } + const extensionPackagesBlock = ci.slice(ci.indexOf("extension-packages:"), ci.indexOf(" liboliphaunt-native-desktop:")); + if (extensionPackagesBlock.includes("Download portable WASIX runtime outputs")) { + fail("extension-packages must consume WASIX extension artifact outputs, not raw portable runtime outputs"); + } +} + +function validateTargetMatrices() { + const ci = readText(".github/workflows/ci.yml"); + const release = readText(".github/workflows/release.yml"); + const planner = readText("tools/graph/ci_plan.mjs"); + for (const outputName of [ + "liboliphaunt_native_desktop_runtime_matrix", + "liboliphaunt_native_android_runtime_matrix", + "liboliphaunt_native_ios_runtime_matrix", + ]) { + if (!ci.includes(outputName) || !ci.includes(`fromJson(needs.affected.outputs.${outputName})`)) { + fail(`CI ${outputName} matrix must come from affected planner output`); + } + } + for (const [outputName, helper] of [ + ["liboliphaunt_native_desktop_runtime_matrix", "liboliphauntNativeDesktopRuntimeMatrix"], + ["liboliphaunt_native_android_runtime_matrix", "liboliphauntNativeAndroidRuntimeMatrix"], + ["liboliphaunt_native_ios_runtime_matrix", "liboliphauntNativeIosRuntimeMatrix"], + ]) { + requireText( + "tools/graph/ci_plan.mjs", + helper, + `CI affected planner must derive ${outputName} from release metadata artifact targets`, + ); + } + if (!ci.includes("broker_runtime_matrix") || !ci.includes("fromJson(needs.affected.outputs.broker_runtime_matrix)")) { + fail("CI broker matrix must come from affected planner output"); + } + if (!ci.includes("node_direct_runtime_matrix") || !ci.includes("fromJson(needs.affected.outputs.node_direct_runtime_matrix)")) { + fail("CI Node direct matrix must come from affected planner output"); + } + if (!ci.includes("extension_artifacts_wasix_matrix") || !ci.includes("fromJson(needs.affected.outputs.extension_artifacts_wasix_matrix)")) { + fail("CI WASIX extension artifact matrix must come from affected planner output"); + } + requireText( + ".github/workflows/ci.yml", + "Build native exact-extension artifacts", + "CI must build native exact-extension artifacts in their own producer job", + ); + if (!ci.includes("extension_artifacts_native_matrix") || !ci.includes("fromJson(needs.affected.outputs.extension_artifacts_native_matrix)")) { + fail("CI native extension artifact matrix must come from affected planner output"); + } + requireText( + "src/extensions/artifacts/native/moon.yml", + "src/extensions/artifacts/native/tools/package-release-assets.sh", + "CI native exact-extension artifact producer must use the release-shaped native extension packager", + ); + requireText( + "src/extensions/artifacts/packages/moon.yml", + "tools/release/build-extension-ci-artifacts.mjs --all --require-native --require-wasix", + "CI exact-extension package producer must use the shared product artifact builder", + ); + requireText( + "src/extensions/artifacts/packages/moon.yml", + "/target/extensions/wasix/aot-artifacts/**/*", + "CI exact-extension package producer must consume WASIX extension AOT artifacts", + ); + requireText( + "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", + ); + requireText( + "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", + ); + requireText( + "src/runtimes/liboliphaunt/wasix/tools/build-aot-target.sh", + 'cargo run -p xtask -- assets check-aot --target-triple "$target"', + "WASIX AOT target build must validate target AOT artifacts", + ); + if (release.includes("native-release-targets:") || release.includes("native-release-assets:")) { + fail("release workflow must not define separate native asset builder jobs; CI owns runtime/helper artifacts"); + } + if (release.includes("artifact_target_matrix.py native-release-hosts")) { + fail("release workflow must not use the removed native-release-hosts matrix"); + } + if (!planner.includes("../release/artifact_target_matrix.mjs")) { + fail("shared affected planner must query the release artifact target matrix helper"); + } + + const liboliphauntMatrix = artifactTargetMatrix("liboliphaunt-native-runtime"); + const liboliphauntTargets = new Set(liboliphauntMatrix.include.map((item) => item.target)); + const expectedLiboliphauntTargets = new Set( + artifactTargets({ product: "liboliphaunt-native", kind: "native-runtime", publishedOnly: true }).map((target) => target.target), + ); + if (!sameSet(liboliphauntTargets, expectedLiboliphauntTargets)) { + fail( + "liboliphaunt CI matrix does not match published native runtime targets: " + + `${formatList(liboliphauntTargets)} vs ${formatList(expectedLiboliphauntTargets)}`, + ); + } + + const extensionNativeMatrix = artifactTargetMatrix("extension-artifacts-native"); + const extensionNativePairs = new Set(); + for (const item of extensionNativeMatrix.include) { + for (const product of item.extensions_csv.split(",")) { + if (product) { + extensionNativePairs.add(`${product}\0${item.target}`); + } + } + } + const expectedExtensionNativePairs = new Set( + extensionArtifactTargets({ family: "native", publishedOnly: true }).map((target) => `${target.product}\0${target.target}`), + ); + if (!sameSet(extensionNativePairs, expectedExtensionNativePairs)) { + fail( + "native extension artifact CI matrix does not match published exact-extension native product/target pairs: " + + `${formatList([...extensionNativePairs].map((item) => item.split("\0")))} vs ${formatList([...expectedExtensionNativePairs].map((item) => item.split("\0")))}`, + ); + } + + const brokerMatrix = artifactTargetMatrix("broker-runtime"); + const brokerTargets = new Set(brokerMatrix.include.map((item) => item.target)); + const expectedBrokerTargets = new Set( + artifactTargets({ product: "oliphaunt-broker", kind: "broker-helper", publishedOnly: true }).map((target) => target.target), + ); + if (!sameSet(brokerTargets, expectedBrokerTargets)) { + fail(`broker CI matrix does not match published broker helper targets: ${formatList(brokerTargets)} vs ${formatList(expectedBrokerTargets)}`); + } + + const nodeDirectMatrix = artifactTargetMatrix("node-direct-runtime"); + const nodeDirectTargets = new Set(nodeDirectMatrix.include.map((item) => item.target)); + const expectedNodeDirectTargets = new Set( + artifactTargets({ product: "oliphaunt-node-direct", kind: "node-direct-addon", publishedOnly: true }).map((target) => target.target), + ); + if (!sameSet(nodeDirectTargets, expectedNodeDirectTargets)) { + fail(`Node direct CI matrix does not match published Node direct targets: ${formatList(nodeDirectTargets)} vs ${formatList(expectedNodeDirectTargets)}`); + } + + const extensionWasixMatrix = artifactTargetMatrix("extension-artifacts-wasix"); + const extensionWasixPairs = new Set(); + for (const item of extensionWasixMatrix.include) { + for (const product of item.extensions_csv.split(",")) { + if (product) { + extensionWasixPairs.add(`${product}\0${item.target}`); + } + } + } + const expectedExtensionWasixPairs = new Set( + extensionArtifactTargets({ family: "wasix", publishedOnly: true }).map((target) => `${target.product}\0${target.target}`), + ); + if (!sameSet(extensionWasixPairs, expectedExtensionWasixPairs)) { + fail( + "WASIX extension artifact CI matrix does not match published exact-extension WASIX product/target pairs: " + + `${formatList([...extensionWasixPairs].map((item) => item.split("\0")))} vs ${formatList([...expectedExtensionWasixPairs].map((item) => item.split("\0")))}`, + ); + } +} + +function validateTypescriptRuntimeTargets() { + for (const target of artifactTargets({ product: "liboliphaunt-native", kind: "native-runtime", surface: "typescript-native-direct" })) { + const source = "src/sdks/js/src/native/common.ts"; + if (target.published) { + if (target.npm_package === null) { + fail(`${target.id} must declare npm_package for TypeScript native resolution`); + } + if (target.library_relative_path === null) { + fail(`${target.id} must declare library_relative_path for TypeScript native resolution`); + } + requireText(source, target.npm_package, `TypeScript native resolver must advertise ${target.id}`); + requireText(source, target.target, `TypeScript native resolver must expose target id ${target.target}`); + requireText(source, target.library_relative_path, `TypeScript native resolver must expose library path for ${target.id}`); + requireText(source, "runtimeRelativePath", `TypeScript native resolver must expose runtime package path for ${target.id}`); + } else { + if (target.npm_package !== null) { + rejectText(source, target.npm_package, `TypeScript native resolver must not advertise unpublished target ${target.id}`); + } + rejectText(source, target.target, `TypeScript native resolver must not expose unpublished target id ${target.target}`); + } + } + + for (const target of artifactTargets({ product: "oliphaunt-broker", kind: "broker-helper", surface: "typescript-broker" })) { + const source = "src/sdks/js/src/runtime/broker.ts"; + if (target.published) { + if (target.npm_package === null) { + fail(`${target.id} must declare npm_package for TypeScript broker resolution`); + } + if (target.executable_relative_path === null) { + fail(`${target.id} must declare executable_relative_path for TypeScript broker resolution`); + } + requireText(source, target.npm_package, `TypeScript broker resolver must advertise ${target.id}`); + requireText(source, target.target, `TypeScript broker resolver must expose target id ${target.target}`); + requireText(source, target.executable_relative_path, `TypeScript broker resolver must expose executable path for ${target.id}`); + } else { + if (target.npm_package !== null) { + rejectText(source, target.npm_package, `TypeScript broker resolver must not advertise unpublished target ${target.id}`); + } + rejectText(source, target.target, `TypeScript broker resolver must not expose unpublished target id ${target.target}`); + } + } + + for (const target of artifactTargets({ product: "oliphaunt-node-direct", kind: "node-direct-addon", surface: "npm-optional" })) { + const source = "src/sdks/js/src/native/node-addon.ts"; + if (target.published) { + if (target.npm_package === null) { + fail(`${target.id} must declare npm_package for TypeScript Node direct resolution`); + } + requireText(source, target.npm_package, `TypeScript Node direct resolver must advertise ${target.id}`); + requireText(source, target.target, `TypeScript Node direct resolver must expose target id ${target.target}`); + requireText(source, "ADDON_STEM", `TypeScript Node direct resolver must expose addon path for ${target.id}`); + } else { + if (target.npm_package !== null) { + rejectText(source, target.npm_package, `TypeScript Node direct resolver must not advertise unpublished target ${target.id}`); + } + rejectText(source, target.target, `TypeScript Node direct resolver must not expose unpublished target id ${target.target}`); + } + } +} + +function validateRustBrokerTargets() { + const manifest = "src/sdks/rust/Cargo.toml"; + const source = "src/sdks/rust/src/broker.rs"; + requireText( + manifest, + 'broker-helper = "oliphaunt-broker"', + "Rust SDK package metadata must identify the broker helper runtime it consumes", + ); + requireText( + manifest, + `broker-version = "${readCurrentVersion("oliphaunt-broker")}"`, + "Rust SDK package metadata must pin the compatible broker helper version", + ); + requireText( + source, + "OLIPHAUNT_BROKER_ASSET_DIR", + "Rust broker resolver must support package-shaped broker artifact fixtures", + ); + for (const target of artifactTargets({ product: "oliphaunt-broker", kind: "broker-helper", surface: "rust-broker" })) { + if (target.published) { + requireText(source, target.asset, `Rust broker resolver must advertise ${target.id}`); + requireText(source, target.target, `Rust broker resolver must expose target id ${target.target}`); + if (target.executable_relative_path !== null) { + requireText(source, target.executable_relative_path, `Rust broker resolver must expose helper path for ${target.id}`); + } + } else { + rejectText(source, target.asset, `Rust broker resolver must not advertise unpublished target ${target.id}`); + rejectText(source, target.target, `Rust broker resolver must not expose unpublished target id ${target.target}`); + } + } +} + +function validateExpectedProductAssets() { + const expected = { + "liboliphaunt-native": new Set([ + "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", + "liboliphaunt-{version}-android-x86_64.tar.gz", + "liboliphaunt-{version}-runtime-resources.tar.gz", + "liboliphaunt-{version}-icu-data.tar.gz", + "liboliphaunt-{version}-package-size.tsv", + "liboliphaunt-{version}-release-assets.sha256", + ]), + "oliphaunt-broker": new Set([ + "oliphaunt-broker-{version}-macos-arm64.tar.gz", + "oliphaunt-broker-{version}-linux-x64-gnu.tar.gz", + "oliphaunt-broker-{version}-linux-arm64-gnu.tar.gz", + "oliphaunt-broker-{version}-windows-x64-msvc.zip", + "oliphaunt-broker-{version}-release-assets.sha256", + ]), + "oliphaunt-node-direct": new Set([ + "oliphaunt-node-direct-{version}-macos-arm64.tar.gz", + "oliphaunt-node-direct-{version}-linux-x64-gnu.tar.gz", + "oliphaunt-node-direct-{version}-linux-arm64-gnu.tar.gz", + "oliphaunt-node-direct-{version}-windows-x64-msvc.zip", + "oliphaunt-node-direct-{version}-release-assets.sha256", + ]), + "liboliphaunt-wasix": new Set([ + "liboliphaunt-wasix-{version}-runtime-portable.tar.zst", + "liboliphaunt-wasix-{version}-icu-data.tar.zst", + "liboliphaunt-wasix-{version}-runtime-aot-macos-arm64.tar.zst", + "liboliphaunt-wasix-{version}-runtime-aot-linux-x64-gnu.tar.zst", + "liboliphaunt-wasix-{version}-runtime-aot-linux-arm64-gnu.tar.zst", + "liboliphaunt-wasix-{version}-runtime-aot-windows-x64-msvc.tar.zst", + "liboliphaunt-wasix-{version}-release-assets.sha256", + ]), + }; + for (const [product, assets] of Object.entries(expected)) { + const actual = new Set( + artifactTargets({ product, surface: "github-release", publishedOnly: true }).map((target) => target.asset), + ); + if (!sameSet(actual, assets)) { + fail(`${product} published artifact targets expected ${formatList(assets)}, got ${formatList(actual)}`); + } + } +} + +function main() { + validateTargetShape(); + validateMoonRuntimeTargets(); + validateExtensionArtifactTargets(); + validateGithubAssetHelpers(); + validateCiReleaseArtifacts(); + validateTargetMatrices(); + validateTypescriptRuntimeTargets(); + validateRustBrokerTargets(); + validateExpectedProductAssets(); + console.log("artifact target checks passed"); + return 0; +} + +try { + process.exit(main()); +} catch (error) { + fail(error?.message ?? String(error)); +} diff --git a/tools/release/check_artifact_targets.py b/tools/release/check_artifact_targets.py deleted file mode 100644 index eb40ddfd..00000000 --- a/tools/release/check_artifact_targets.py +++ /dev/null @@ -1,1600 +0,0 @@ -#!/usr/bin/env python3 -"""Validate native and helper artifact target metadata.""" - -from __future__ import annotations - -import json -import subprocess -import sys -import tomllib -from functools import lru_cache -from pathlib import Path -from types import SimpleNamespace -from typing import NoReturn - - -ROOT = Path(__file__).resolve().parents[2] - - -def fail(message: str) -> NoReturn: - print(f"check_artifact_targets.py: {message}", file=sys.stderr) - raise SystemExit(1) - - -def read_text(path: str) -> str: - return (ROOT / path).read_text(encoding="utf-8") - - -def read_toml(path: Path) -> dict: - try: - with path.open("rb") as handle: - data = tomllib.load(handle) - except tomllib.TOMLDecodeError as error: - fail(f"{path.relative_to(ROOT)} is invalid TOML: {error}") - if not isinstance(data, dict): - fail(f"{path.relative_to(ROOT)} must contain a TOML table") - 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) - - -@lru_cache(maxsize=None) -def release_graph_rows(command: str, args: tuple[str, ...] = ()) -> tuple[dict, ...]: - value = bun_json(["tools/release/release_graph_query.mjs", command, *args]) - if not isinstance(value, list) or not all(isinstance(row, dict) for row in value): - fail(f"release graph {command} query did not return an object list") - return tuple(value) - - -def object_row(row: dict) -> SimpleNamespace: - normalized = dict(row) - for key in ( - "triple", - "runner", - "library_relative_path", - "executable_relative_path", - "npm_package", - "npm_os", - "npm_cpu", - "npm_libc", - "llvm_url", - ): - normalized.setdefault(key, None) - normalized.setdefault("extension_artifacts", True) - return SimpleNamespace(**normalized) - - -def artifact_target_args( - *, - product: str | None = None, - kind: str | None = None, - surface: str | None = None, - published_only: bool = False, -) -> tuple[str, ...]: - args: list[str] = [] - if product is not None: - args.extend(["--product", product]) - if kind is not None: - args.extend(["--kind", kind]) - if surface is not None: - args.extend(["--surface", surface]) - if published_only: - args.append("--published-only") - return tuple(args) - - -def artifact_targets( - *, - product: str | None = None, - kind: str | None = None, - surface: str | None = None, - published_only: bool = False, -) -> list[SimpleNamespace]: - return [ - object_row(row) - for row in release_graph_rows( - "artifact-targets", - artifact_target_args( - product=product, - kind=kind, - surface=surface, - published_only=published_only, - ), - ) - ] - - -def raw_artifact_target_tables() -> list[dict]: - return [dict(row) for row in release_graph_rows("raw-artifact-targets")] - - -def legacy_central_artifact_target_rows() -> tuple[dict, ...]: - return release_graph_rows("legacy-central-artifact-targets") - - -def moon_release_metadata(product: str) -> dict: - rows = release_graph_rows("moon-release-metadata", ("--product", product)) - if len(rows) != 1: - fail(f"release graph moon-release-metadata returned {len(rows)} rows for {product}") - row = dict(rows[0]) - row.pop("product", None) - return row - - -def extension_product_ids() -> list[str]: - rows = release_graph_rows("extension-metadata") - products = [] - for row in rows: - product = row.get("product") - if not isinstance(product, str) or not product: - fail("release graph extension-metadata rows must declare non-empty products") - products.append(product) - if len(products) != len(set(products)): - fail("release graph extension-metadata query returned duplicate products") - return sorted(products) - - -def extension_artifact_targets( - *, - product: str | None = None, - family: str | None = None, - published_only: bool = False, -) -> list[SimpleNamespace]: - args: list[str] = [] - if product is not None: - args.extend(["--product", product]) - if family is not None: - args.extend(["--family", family]) - if published_only: - args.append("--published-only") - return [ - object_row(row) - for row in release_graph_rows("extension-targets", tuple(args)) - ] - - -def product_config(product: str) -> dict: - rows = release_graph_rows("product-configs", ("--product", product)) - if len(rows) != 1: - fail(f"release graph product-configs returned {len(rows)} rows for {product}") - return dict(rows[0]) - - -def package_path(product: str) -> Path: - path = product_config(product).get("path") - if not isinstance(path, str) or not path: - fail(f"release graph product-configs {product}.path must be a non-empty string") - return ROOT / path - - -def sdk_package_products() -> list[str]: - products = [] - for row in release_graph_rows("sdk-package-products"): - product = row.get("product") - if not isinstance(product, str) or not product: - fail("release graph sdk-package-products rows must declare non-empty products") - products.append(product) - if len(products) != len(set(products)): - fail("release graph sdk-package-products query returned duplicate products") - return products - - -def ci_sdk_package_artifact_names() -> list[str]: - artifacts = [] - for row in release_graph_rows("sdk-package-products"): - artifact = row.get("artifactName") - if not isinstance(artifact, str) or not artifact: - fail("release graph sdk-package-products rows must declare non-empty artifactName") - artifacts.append(artifact) - if len(artifacts) != len(set(artifacts)): - fail("release graph sdk-package-products query returned duplicate artifacts") - return artifacts - - -def read_current_version(product: str) -> str: - rows = release_graph_rows("product-versions", ("--product", product)) - if len(rows) != 1: - fail(f"release graph product-versions returned {len(rows)} rows for {product}") - version = rows[0].get("version") - if not isinstance(version, str) or not version: - fail(f"release graph product-versions {product}.version must be a non-empty string") - return version - - -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 ci_plan_full_run(*, wasm_target: str = "all", native_target: str = "all", mobile_target: str = "all") -> dict: - value = bun_json( - [ - "tools/graph/ci_plan.mjs", - "plan-full", - "--wasm-target", - wasm_target, - "--native-target", - native_target, - "--mobile-target", - mobile_target, - ] - ) - if not isinstance(value, dict): - fail("CI planner full-run query did not return an object") - return value - - -def ts_template(asset: str) -> str: - return asset.replace("{version}", "${version}") - - -def require_text(path: str, text: str, message: str) -> None: - if text not in read_text(path): - fail(message) - - -def reject_text(path: str, text: str, message: str) -> None: - if text in read_text(path): - fail(message) - - -def validate_target_shape() -> None: - targets = artifact_targets() - if not targets: - fail("artifact target metadata must define targets") - raw_targets = { - raw.get("id"): raw - for raw in raw_artifact_target_tables() - if isinstance(raw, dict) and isinstance(raw.get("id"), str) - } - - seen_assets: dict[tuple[str, str], str] = {} - for target in targets: - 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 - 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": - fail(f"{target.id} is unpublished and must declare tier = \"planned\"") - reason = raw_target.get("unsupported_reason") - if not isinstance(reason, str) or len(reason.strip()) < 40: - fail(f"{target.id} is unpublished and must declare a concrete unsupported_reason") - if target.kind in {"native-runtime", "broker-helper", "node-direct-addon"}: - if target.triple is None: - fail(f"{target.id} must declare a target triple") - if target.runner is None: - fail(f"{target.id} must declare the CI/release runner") - if target.kind == "wasix-aot-runtime": - if target.triple is None: - fail(f"{target.id} must declare a target triple") - if target.runner is None: - fail(f"{target.id} must declare the CI/release runner") - if target.llvm_url is None: - fail(f"{target.id} must declare llvm_url for AOT generation") - if target.kind in {"native-runtime", "node-direct-addon"}: - if target.library_relative_path is None: - fail(f"{target.id} must declare library_relative_path") - if target.kind == "native-runtime" and target.target.startswith("android-"): - expected_prefix = f"jni/{target.target.removeprefix('android-')}/" - if target.library_relative_path is None or not target.library_relative_path.startswith(expected_prefix): - fail( - f"{target.id} library_relative_path must describe the Android release archive " - f"layout under {expected_prefix}, got {target.library_relative_path}" - ) - if target.kind == "broker-helper" and target.executable_relative_path is None: - fail(f"{target.id} must declare executable_relative_path") - 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: - graph_targets = legacy_central_artifact_target_rows() - central_targets = [ - raw.get("id") - for raw in graph_targets - ] - if central_targets: - fail( - "artifact targets must be derived from Moon release metadata, " - f"not central release metadata: {central_targets}" - ) - - runtime_target_dirs = { - "liboliphaunt-native": "src/runtimes/liboliphaunt/native/targets", - "liboliphaunt-wasix": "src/runtimes/liboliphaunt/wasix/targets", - "oliphaunt-broker": "src/runtimes/broker/targets", - "oliphaunt-node-direct": "src/runtimes/node-direct/targets", - } - for product, directory in runtime_target_dirs.items(): - files = sorted((ROOT / directory).glob("*.toml")) - if files: - fail( - f"{product} runtime artifact targets must be derived from Moon release metadata, " - "not product-local target TOML files: " - + ", ".join(path.relative_to(ROOT).as_posix() for path in files) - ) - - expected_presets = { - "liboliphaunt-native": "liboliphaunt-native", - "liboliphaunt-wasix": "liboliphaunt-wasix", - "oliphaunt-broker": "broker-helper", - "oliphaunt-node-direct": "node-direct-addon", - } - for product, preset in expected_presets.items(): - release = moon_release_metadata(product) - targets = release.get("artifactTargets") - if not isinstance(targets, dict): - fail(f"{product} Moon release metadata must declare artifactTargets") - if targets.get("preset") != preset: - fail(f"{product} Moon artifactTargets.preset must be {preset!r}") - published = targets.get("publishedTargets") - if not isinstance(published, list) or not published or not all(isinstance(item, str) for item in published): - fail(f"{product} Moon artifactTargets.publishedTargets must be a non-empty string list") - - -def wasm_extension_target_id(runtime_target: str) -> str: - if runtime_target == "portable": - return "wasix-portable" - return runtime_target - - -def validate_extension_artifact_targets() -> None: - extension_products = extension_product_ids() - if not extension_products: - fail("exact-extension release products must be modeled as release products") - - expected_native_targets = { - target.target - for target in artifact_targets( - product="liboliphaunt-native", - kind="native-runtime", - published_only=True, - ) - if target.extension_artifacts - } - expected_wasix_targets = { - wasm_extension_target_id(target.target) - for target in artifact_targets( - product="liboliphaunt-wasix", - published_only=True, - ) - if target.kind == "wasix-runtime" - } - if not expected_native_targets: - fail("published native runtime targets are required before extension artifacts can be published") - if not expected_wasix_targets: - fail("published WASIX runtime targets are required before extension artifacts can be published") - - for product in extension_products: - rows = extension_artifact_targets(product=product) - published_native_targets = { - target.target for target in rows if target.family == "native" and target.published - } - declared_native_targets = { - target.target for target in rows if target.family == "native" - } - published_wasix_targets = { - target.target for target in rows if target.family == "wasix" and target.published - } - if declared_native_targets != expected_native_targets: - fail( - f"{product} native extension target rows must cover published liboliphaunt native runtimes, " - f"including explicit unpublished opt-outs: {sorted(declared_native_targets)} vs {sorted(expected_native_targets)}" - ) - if not published_native_targets: - fail(f"{product} must publish at least one native extension artifact target") - if not published_native_targets <= expected_native_targets: - fail( - f"{product} published native extension targets must be published liboliphaunt native runtimes: " - f"{sorted(published_native_targets)} vs {sorted(expected_native_targets)}" - ) - if published_wasix_targets != expected_wasix_targets: - fail( - f"{product} published WASIX extension targets must match published liboliphaunt WASIX runtimes: " - f"{sorted(published_wasix_targets)} vs {sorted(expected_wasix_targets)}" - ) - for row in rows: - if row.family == "native": - expected_kind = ( - "native-static-registry" - if row.target == "ios-xcframework" or row.target.startswith("android-") - else "native-dynamic" - ) - if row.kind != expected_kind: - fail(f"{product} {row.target} must use extension artifact kind {expected_kind}, got {row.kind}") - if row.published and row.kind == "native-static-registry": - static_recipe = package_path(product) / "targets" / "native-static-registry.toml" - if static_recipe.is_file(): - static_data = read_toml(static_recipe) - status = static_data.get("status") - if status != "supported": - fail( - f"{product} publishes {row.target} native static-registry artifacts, " - f"but {static_recipe.relative_to(ROOT)} declares status={status!r}" - ) - if row.family == "wasix" and row.kind != "wasix-runtime": - fail(f"{product} {row.target} must use wasix-runtime extension artifacts") - - -def validate_github_asset_helpers() -> None: - require_text( - "tools/release/package-liboliphaunt-macos-assets.sh", - "liboliphaunt-${version}-${target_id}.tar.gz", - "macOS liboliphaunt target packager must emit the release-shaped macOS archive", - ) - require_text( - "tools/release/package-liboliphaunt-macos-assets.sh", - "target/liboliphaunt/release-assets", - "macOS liboliphaunt target packager must write into the release asset directory", - ) - require_text( - "tools/release/check_github_release_assets.mjs", - "expectedAssets", - "GitHub release asset checks must derive product assets from product-local artifact targets", - ) - require_text( - "tools/release/check-liboliphaunt-release-assets.mjs", - "allArtifactTargets", - "liboliphaunt release asset checks must derive required assets from product-local artifact targets", - ) - require_text( - "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( - "src/runtimes/liboliphaunt/native/tools/run-host-c-smoke.mjs", - "OLIPHAUNT_SMOKE_BIN_DIR", - "liboliphaunt C ABI smoke runner must support staged-release smoke binaries outside release layouts", - ) - for packager in ( - "tools/release/package-liboliphaunt-macos-assets.sh", - "tools/release/package-liboliphaunt-linux-assets.sh", - "tools/release/package-liboliphaunt-windows-assets.ps1", - ): - require_text( - packager, - "OLIPHAUNT_SMOKE_BIN_DIR", - f"{packager} must smoke the staged release layout without writing smoke binaries into the archive", - ) - require_text( - packager, - "run-host-c-smoke.mjs", - f"{packager} must run the liboliphaunt C ABI smoke against the staged release layout", - ) - require_text( - packager, - "plpgsql", - f"{packager} must include embedded core PostgreSQL modules for native SDK materialization", - ) - - -def validate_ci_release_artifacts() -> None: - ci = read_text(".github/workflows/ci.yml") - release = read_text(".github/workflows/release.yml") - required_ci_snippets = { - "Package liboliphaunt macOS release asset": "CI must build a release-shaped liboliphaunt macOS target archive", - "tools/release/package-liboliphaunt-macos-assets.sh": "CI must use the macOS liboliphaunt target packager", - "Package liboliphaunt Linux release asset": "CI must build release-shaped liboliphaunt Linux target archives", - "tools/release/package-liboliphaunt-linux-assets.sh": "CI must use the Linux liboliphaunt target packager", - "Package liboliphaunt Windows release asset": "CI must build a release-shaped liboliphaunt Windows target archive", - "package-liboliphaunt-windows-assets.ps1": "CI must use the Windows liboliphaunt target packager", - "Package liboliphaunt Android release asset": "CI must package release-shaped liboliphaunt Android target archives", - "Package liboliphaunt iOS release asset": "CI must package release-shaped liboliphaunt iOS target archives", - "tools/release/package-liboliphaunt-mobile-assets.sh": "CI must use the mobile liboliphaunt target packager", - "liboliphaunt-native-release-assets-${{ matrix.target }}": "CI must upload liboliphaunt release-shaped artifacts per target", - "liboliphaunt-native-release-assets:": "CI must aggregate complete public liboliphaunt release assets", - "Download liboliphaunt target release assets": "CI must aggregate liboliphaunt target archive outputs", - ".github/scripts/run-planned-moon-job.sh liboliphaunt-native-release-assets": ( - "CI must aggregate liboliphaunt native release assets through the Moon-modeled builder" - ), - "Upload aggregate liboliphaunt release assets": "CI must upload complete liboliphaunt release assets for release consumption", - "Download Apple liboliphaunt release assets": "Swift SDK package artifacts must consume the Apple SwiftPM liboliphaunt release asset", - "liboliphaunt-native-release-assets-ios-xcframework": "Swift SDK package artifacts must download the Apple target release asset directly", - "OLIPHAUNT_SWIFT_RELEASE_ASSET_DIR": "Swift SDK package artifacts must render Package.swift.release from real liboliphaunt release assets in CI", - ".github/scripts/run-planned-moon-job.sh broker-runtime": "CI must invoke the planned broker Moon job that includes release-shaped helper artifacts", - "oliphaunt-broker-release-assets-${{ matrix.target }}": "CI must upload broker helper release-shaped artifacts per target", - ".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-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/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", - "Download exact-extension package artifacts": "Mobile build jobs must consume package-shaped exact-extension artifacts", - "Download WASIX exact-extension artifacts": "CI exact-extension package assembly must consume WASIX extension artifact builder outputs", - "pattern: liboliphaunt-wasix-extension-artifacts-*": "CI exact-extension package assembly must download every WASIX extension artifact target output", - "target/extensions/wasix/release-assets": "CI must use the shared WASIX exact-extension release asset staging layout", - "extension-artifacts-native:\n name: Builds / extension-native (${{ matrix.target }})\n needs:\n - affected": ( - "Native exact-extension artifact builders must be grouped by target" - ), - "OLIPHAUNT_EXTENSION_PRODUCTS: ${{ matrix.extensions_csv }}": ( - "Exact-extension artifact builder jobs must pass the selected extension product set into the producer" - ), - "liboliphaunt-native-extension-artifacts-${{ matrix.target }}": ( - "Native exact-extension artifact uploads must be addressable by target" - ), - "liboliphaunt-native-extension-ccache-${{ matrix.target }}": ( - "Native exact-extension artifact builders must restore target-scoped compiler/build caches" - ), - "liboliphaunt-wasix-extension-artifacts-${{ matrix.target }}": ( - "WASIX exact-extension artifact uploads must be addressable by target" - ), - "MOON_CACHE=off .github/scripts/run-planned-moon-job.sh extension-artifacts-native": ( - "Native exact-extension artifact builders must inherit Moon source/check prerequisites inside the job" - ), - "OLIPHAUNT_MOON_UPSTREAM=none MOON_CACHE=off .github/scripts/run-planned-moon-job.sh extension-artifacts-wasix": ( - "WASIX exact-extension artifact builders must consume downloaded runtime outputs, not re-run upstream producers" - ), - "OLIPHAUNT_EXPO_REQUIRE_PREBUILT_EXTENSIONS": "Mobile build jobs must require prebuilt exact-extension artifacts instead of source-built extension fallbacks", - "OLIPHAUNT_EXPO_REQUIRE_SDK_ARTIFACTS": "Mobile build jobs must require staged SDK package artifacts instead of silent source fallbacks", - "OLIPHAUNT_EXPO_SDK_ARTIFACT_ROOT": "Mobile build jobs must resolve SDK artifacts from the staged package artifact root", - "OLIPHAUNT_EXPO_EXTENSION_ARTIFACT_ROOT": "Mobile build jobs must resolve exact-extension artifacts from the staged package artifact root", - "Validate Android mobile app artifacts": "Android mobile build jobs must inspect the built app for exact selected-extension contents", - "Validate iOS mobile app artifacts": "iOS mobile build jobs must inspect the built app for exact selected-extension contents", - "check-staged-artifacts.mjs --require-mobile android --require-mobile-prebuilt-extensions": ( - "Android mobile artifact validation must require prebuilt exact-extension package inputs" - ), - "check-staged-artifacts.mjs --require-mobile ios --require-mobile-prebuilt-extensions": ( - "iOS mobile artifact validation must require prebuilt exact-extension package inputs" - ), - "OLIPHAUNT_EXPO_IOS_OLIPHAUNT_XCFRAMEWORK": "iOS mobile build jobs must consume the linked liboliphaunt XCFramework artifact", - "liboliphaunt-wasix-release-assets:": "CI must aggregate WASIX portable and AOT outputs into public release assets", - "liboliphaunt_wasix_aot_runtime_matrix: ${{ steps.plan.outputs.liboliphaunt_wasix_aot_runtime_matrix }}": ( - "CI affected planning must emit the WASIX AOT target matrix without a separate planning job" - ), - "matrix: ${{ fromJson(needs.affected.outputs.liboliphaunt_wasix_aot_runtime_matrix": ( - "WASIX AOT builders must consume the affected-plan target matrix directly" - ), - "contains(fromJson(needs.affected.outputs.jobs), 'liboliphaunt-wasix-aot')": ( - "CI must only build WASIX AOT artifacts when the affected planner selected AOT work" - ), - "contains(fromJson(needs.affected.outputs.jobs), 'liboliphaunt-wasix-release-assets')": ( - "CI must only aggregate WASIX release assets when the affected planner selected release aggregation" - ), - ".github/scripts/run-planned-moon-job.sh liboliphaunt-wasix-release-assets": ( - "CI must package WASIX public release assets through the planned Moon task" - ), - "target/oliphaunt-wasix/wasix-build/work/icu-wasix/share/icu/**": ( - "CI must pass the WASIX ICU sidecar produced by the portable runtime job into release asset packaging" - ), - "target/oliphaunt-wasix/release-assets": "CI must upload WASIX public release assets", - "Stage target AOT artifact envelope": "WASIX AOT builders must upload a deterministic artifact envelope", - "target-triple.txt": "WASIX AOT artifact envelopes must identify their target triple explicitly", - "target/oliphaunt-wasix/aot-upload/**": "WASIX AOT upload must use the staged artifact envelope, not an implicit target path", - "Invalid WASIX AOT artifact envelope": "WASIX AOT consumers must validate the downloaded artifact envelope before restoring it", - } - for snippet, message in required_ci_snippets.items(): - if snippet not in ci: - fail(message) - for artifact in ci_sdk_package_artifact_names(): - if artifact not in ci: - fail(f"CI must upload SDK package artifact {artifact}") - for product in 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/dev/bun.sh tools/release/release_graph_query.mjs ci-artifact-names --product "$product" --family sdk-package --format lines', - "release workflow must derive SDK package artifact names from release metadata", - ) - require_text( - ".github/workflows/release.yml", - 'tools/dev/bun.sh tools/release/release_graph_query.mjs ci-products --family sdk-package --products-json "$PRODUCTS_JSON" --format lines', - "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"]', - "Broker release-assets must be selected by the ci-broker-runtime Moon tag", - ) - require_text( - "src/runtimes/node-direct/moon.yml", - 'tags: ["release", "artifact", "ci-node-direct"]', - "Node direct release-assets must be selected by the ci-node-direct Moon tag", - ) - require_text( - "src/runtimes/node-direct/moon.yml", - "/target/oliphaunt-node-direct/npm-packages/**/*", - "Node direct Moon release-assets task must declare optional npm tarballs as outputs", - ) - require_text( - "src/runtimes/node-direct/tools/build-node-addon.sh", - "Node direct optional npm package staged", - "Node direct CI builder must stage optional npm tarballs for release publishing", - ) - require_text( - ".github/workflows/release.yml", - "Download Node direct optional npm packages", - "release workflow must download Node direct optional npm package artifacts from CI", - ) - require_text( - "tools/release/release.py", - "node_direct_optional_npm_tarballs", - "Node direct release publish must validate staged optional npm tarballs", - ) - require_text( - "tools/release/release.py", - 'run(["npm", "publish", str(tarball), "--access", "public", "--provenance"])', - "Node direct optional npm publish must publish CI-built tarballs directly", - ) - for project_id in sdk_package_products(): - moon_file = ( - "src/bindings/wasix-rust/moon.yml" - if project_id == "oliphaunt-wasix-rust" - else f"src/sdks/{'js' if project_id == 'oliphaunt-js' else project_id.removeprefix('oliphaunt-')}/moon.yml" - ) - require_text( - moon_file, - f"tools/release/build-sdk-ci-artifacts.mjs {project_id}", - f"{project_id} package task must stage publishable SDK artifacts", - ) - require_text( - moon_file, - f"/target/sdk-artifacts/{project_id}/**/*", - f"{project_id} package task must declare staged SDK package artifacts as Moon outputs", - ) - focused_wasix_jobs = set(ci_plan_full_run(wasm_target="linux-x64-gnu").get("jobs", [])) - if focused_wasix_jobs != {"affected", "liboliphaunt-wasix-runtime", "liboliphaunt-wasix-aot"}: - fail( - "focused WASIX target runs must build only the portable runtime and requested AOT producer, " - f"got {sorted(focused_wasix_jobs)}" - ) - require_text( - "tools/graph/ci_plan.mjs", - "extension_artifacts_wasix_matrix:", - "CI planner must model WASIX exact-extension artifact matrix output", - ) - require_text( - "tools/graph/ci_plan.mjs", - 'jobs.has("extension-artifacts-wasix")', - "CI planner must emit WASIX exact-extension rows only when the WASIX extension builder is selected", - ) - require_text( - "tools/graph/ci_plan.mjs", - 'extensionArtifactsWasixMatrix("all", selectedExtensionProducts', - "WASIX extension artifacts are portable and must use the portable selector, not the AOT target selector", - ) - wasix_release_needs = ( - "liboliphaunt-wasix-release-assets:\n" - " name: Builds / liboliphaunt-wasix-release-assets\n" - " needs:\n" - " - affected\n" - " - liboliphaunt-wasix-runtime\n" - " - liboliphaunt-wasix-aot" - ) - if wasix_release_needs not in ci: - fail("WASIX release asset builder must consume portable and AOT runtime builders") - if 'OLIPHAUNT_EXPO_MOBILE_EXTENSIONS: ""' in ci: - fail("mobile build jobs must not disable selected extensions with OLIPHAUNT_EXPO_MOBILE_EXTENSIONS=\"\"") - if "run: cargo run -p xtask -- release package-assets" in ci: - fail("CI must not bypass Moon for WASIX release asset packaging") - if "run: src/runtimes/liboliphaunt/wasix/tools/build-runtime-portable.sh" in ci: - fail("CI must not bypass Moon for portable WASIX runtime builds") - if "target/oliphaunt-wasix/aot/${{ matrix.target }}/**" in ci: - fail("WASIX AOT uploads must use the explicit target-triple artifact envelope") - if "run: src/runtimes/liboliphaunt/wasix/tools/build-aot-target.sh" in ci: - fail("CI must not bypass Moon for WASIX AOT builds") - if ci.index("mobile-build-android:") < ci.index("mobile-extension-packages:"): - fail("mobile exact-extension package producer must be declared before mobile Android build consumers") - if "mobile-build-android:\n name: Builds / mobile-android (${{ matrix.target }})\n needs:\n - affected\n - mobile-extension-packages\n - liboliphaunt-native-android" not in ci: - fail("Android mobile build must depend on mobile-extension-packages and the Android liboliphaunt target builder") - if "mobile-build-ios:\n name: Builds / mobile-ios\n needs:\n - affected\n - mobile-extension-packages\n - liboliphaunt-native-ios" not in ci: - fail("iOS mobile build must depend on mobile-extension-packages and the iOS liboliphaunt target builder") - if "mobile-build-android:\n name: Builds / mobile-android (${{ matrix.target }})\n needs:\n - affected\n - mobile-extension-packages\n - liboliphaunt-native-android\n - kotlin-sdk-package\n - react-native-sdk-package" not in ci: - fail("Android mobile build must depend on Android runtime, Kotlin, and React Native package artifacts") - require_text( - ".github/workflows/ci.yml", - "matrix: ${{ fromJson(needs.affected.outputs.react_native_android_mobile_app_matrix) }}", - "Android mobile build must use the React Native Android runtime target matrix", - ) - require_text( - ".github/workflows/ci.yml", - "react-native-mobile-android-app-${{ matrix.target }}", - "Android mobile build artifacts must be target-specific", - ) - if "mobile-build-ios:\n name: Builds / mobile-ios\n needs:\n - affected\n - mobile-extension-packages\n - liboliphaunt-native-ios\n - react-native-sdk-package\n - swift-sdk-package" not in ci: - fail("iOS mobile build must depend on iOS runtime, React Native, and Swift package artifacts") - if "swift-sdk-package:\n name: Builds / swift-sdk\n needs:\n - affected\n - liboliphaunt-native-ios" not in ci: - fail("Swift SDK package artifacts must depend on the iOS native target builder that produces the Apple release asset") - require_text( - "tools/graph/ci_plan.mjs", - 'jobs.has("swift-sdk-package")', - "CI affected planner must make Swift SDK package builds imply liboliphaunt target asset producers", - ) - require_text( - "tools/graph/ci_plan.mjs", - 'targets.add("ios-xcframework")', - "CI affected planner must narrow Swift SDK liboliphaunt target builds to the Apple SwiftPM target when possible", - ) - require_text( - "src/sdks/react-native/tools/expo-runner-common.sh", - "expo_single_sdk_artifact_file", - "React Native mobile runners must have a shared required-SDK-artifact resolver", - ) - require_text( - "src/sdks/react-native/tools/expo-android-runner.sh", - "install_kotlin_sdk_maven_artifacts_if_required", - "Android mobile runner must consume staged Kotlin Maven artifacts when CI requires SDK artifacts", - ) - require_text( - "src/sdks/react-native/tools/expo-ios-runner.sh", - "prepare_swift_sdk_artifact_git_repo_if_required", - "iOS mobile runner must consume the staged Swift source artifact when CI requires SDK artifacts", - ) - require_text( - "tools/release/build-sdk-ci-artifacts.mjs", - "publishAndroidReleasePublicationToMavenLocal", - "Kotlin SDK package builder must stage a Maven repository layout for Android consumers", - ) - require_text( - "tools/release/build-sdk-ci-artifacts.mjs", - 'path.join(artifactRoot, "maven")', - "Kotlin SDK package builder must stage Maven artifacts under target/sdk-artifacts/oliphaunt-kotlin/maven", - ) - require_text( - "tools/release/build-sdk-ci-artifacts.mjs", - '"tools/release/check-staged-artifacts.mjs", "--require-sdk-product", product', - "SDK package builders must validate staged package artifacts for runtime/extension payload leaks", - ) - reject_text( - "tools/release/build-sdk-ci-artifacts.mjs", - "outputs/aar/*-release.aar", - "Kotlin SDK package staging must not copy loose AARs; the staged Maven repository is the package boundary", - ) - require_text( - "tools/release/build-sdk-ci-artifacts.mjs", - "oliphaunt-android-gradle-plugin:publishToMavenLocal", - "Kotlin SDK package builder must stage the Android Gradle plugin Maven artifact", - ) - require_text( - "src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh", - "check-staged-artifacts.mjs \"${validation_args[@]}\"", - "mobile exact-extension package assembly must validate the staged package manifests and checksums it selected", - ) - require_text( - "src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh", - "OLIPHAUNT_EXTENSION_PACKAGE_PRODUCTS must list selected exact-extension products for mobile packaging", - "mobile exact-extension package assembly must fail closed without an explicit selected product list", - ) - reject_text( - "src/extensions/artifacts/packages/tools/package-mobile-release-assets.sh", - "args+=(--all)", - "mobile exact-extension package assembly must not fall back to all extension products", - ) - require_text( - "src/runtimes/liboliphaunt/native/moon.yml", - "tools/release/package-liboliphaunt-aggregate-assets.sh", - "liboliphaunt native aggregate assets must have one Moon-modeled packager/checker entrypoint", - ) - require_text( - "tools/release/check-staged-artifacts.mjs", - "validateReleaseArchivePayload(assetPath)", - "staged exact-extension artifact checks must reject placeholder files that are not readable release archives", - ) - require_text( - "tools/graph/ci_plan.mjs", - 'jobs.add("mobile-extension-packages")', - "affected planner must select target-scoped exact-extension packages whenever mobile jobs are selected", - ) - reject_text( - "tools/graph/ci_plan.mjs", - 'if "extension-artifacts-native" in jobs:\n jobs.add("liboliphaunt-native")', - "affected planner must not create a coarse native-runtime waterfall for exact-extension artifact builds", - ) - reject_text( - ".github/workflows/release.yml", - "product_liboliphaunt_native == 'true' || steps.release_plan.outputs.product_oliphaunt_swift == 'true'", - "Swift SDK releases must consume staged Swift package artifacts, not force aggregate liboliphaunt asset downloads", - ) - require_text( - ".github/workflows/release.yml", - "steps.release_plan.outputs.product_liboliphaunt_native == 'true' }}", - "release workflow must still download aggregate liboliphaunt assets for liboliphaunt-native releases", - ) - require_text( - "tools/release/release.py", - "prepare_staged_swift_release_manifest", - "Swift SDK release must use the Package.swift.release produced by the SDK package builder", - ) - require_text( - "tools/release/release.py", - "def validate_staged_sdk_package", - "release dry-runs must validate staged SDK package artifacts before publish checks", - ) - for product_id in sdk_package_products(): - require_text( - "tools/release/release.py", - f'validate_staged_sdk_package("{product_id}")', - f"{product_id} release dry-run must validate the staged SDK package artifact", - ) - require_text( - ".github/scripts/run-planned-moon-job.sh", - "OLIPHAUNT_MOON_UPSTREAM", - "CI must be able to run downloaded-artifact consumer jobs without re-running Moon upstream producer tasks", - ) - for consumer_job in ( - "extension-packages", - "mobile-extension-packages", - "liboliphaunt-native-release-assets", - "liboliphaunt-wasix-aot", - "liboliphaunt-wasix-release-assets", - "mobile-build-android", - "mobile-build-ios", - ): - require_text( - ".github/workflows/ci.yml", - f"OLIPHAUNT_MOON_UPSTREAM=none MOON_CACHE=off .github/scripts/run-planned-moon-job.sh {consumer_job}", - f"{consumer_job} must consume downloaded builder artifacts without re-running upstream producer tasks", - ) - if "Stage mobile exact-extension packages" in ci: - fail("mobile build jobs must not locally stage extension packages; they must consume extension-package builder artifacts") - if "extension-packages-native" in ci: - fail("CI must not keep a native-only extension package shortcut; mobile must consume target-scoped exact-extension packages") - if "oliphaunt-extension-native-package-artifacts" in ci: - fail("CI must not publish native-only exact-extension package artifacts") - if "target/extension-artifacts-native" in ci: - fail("CI must not use a separate native-only extension package staging layout") - require_text( - "tools/release/release.py", - "requires staged exact-extension package artifacts", - "release CLI must fail closed when extension releases lack staged CI-built package artifacts", - ) - require_text( - "tools/release/release.py", - "validate_extension_release_package", - "release CLI must validate staged exact-extension package manifests before dry-run or publish", - ) - require_text( - "tools/release/release.py", - "staged_native_targets != declared_native_targets", - "release CLI must reject partial native exact-extension package artifacts", - ) - require_text( - "tools/release/release.py", - "staged_wasix_targets != declared_wasix_targets", - "release CLI must reject partial WASIX exact-extension package artifacts", - ) - require_text( - "tools/release/release.py", - "sha256_file(asset_path) != sha_value", - "release CLI must verify staged exact-extension artifact checksums", - ) - require_text( - "tools/release/release.py", - "validate_checksum_manifest(checksum_manifest, asset_dir)", - "release CLI must verify staged exact-extension checksum manifests exactly", - ) - require_text( - "tools/release/build-extension-ci-artifacts.mjs", - "nativeAssetName(product, version", - "exact-extension package artifacts must be named by extension product version", - ) - require_text( - "src/extensions/artifacts/native/tools/package-release-assets.sh", - "native-extension-assets.tsv", - "native exact-extension artifact producers must emit a target-addressed native asset index", - ) - require_text( - "src/extensions/artifacts/native/tools/package-release-assets.sh", - "OLIPHAUNT_EXTENSION_PRODUCT", - "native exact-extension artifact producers must support product-scoped builds", - ) - require_text( - "src/extensions/artifacts/wasix/tools/package-release-assets.sh", - "OLIPHAUNT_EXTENSION_PRODUCT", - "WASIX exact-extension artifact producers must support product-scoped builds", - ) - require_text( - "tools/release/build-extension-ci-artifacts.mjs", - "nativeAssetsFromTargetIndexes", - "exact-extension package staging must consume target-addressed native asset indexes", - ) - require_text( - "tools/release/build-extension-ci-artifacts.mjs", - 'publishedTargetIds("native")', - "exact-extension package staging must only read declared published native target artifact indexes", - ) - require_text( - "tools/release/build-extension-ci-artifacts.mjs", - 'publishedTargetIds("wasix")', - "exact-extension package staging must only read declared published WASIX target artifact indexes", - ) - require_text( - "tools/release/build-extension-ci-artifacts.mjs", - "if (requireNativeTargets.size > 0 && !requireNativeTargets.has(target))", - "mobile exact-extension package staging must filter out native targets that the mobile build did not request", - ) - require_text( - "tools/release/build-extension-ci-artifacts.mjs", - "indexContainsSqlName(productIndex, sqlName)", - "exact-extension package staging must not let stale empty product-scoped native indexes shadow target-level indexes", - ) - require_text( - "tools/release/build-extension-ci-artifacts.mjs", - "-manifest.json", - "exact-extension package artifacts must publish a machine-readable release manifest", - ) - require_text( - "tools/release/check_github_release_assets.mjs", - "verifyReleaseAssets", - "GitHub release verification must derive exact-extension asset expectations from staged extension package manifests", - ) - require_text( - "tools/release/verify_github_release_attestations.mjs", - "exact-extension-artifact", - "Release attestation verification must include exact-extension artifact products", - ) - require_text( - "tools/release/release.py", - "liboliphaunt-native requires staged release assets", - "release CLI must fail closed when liboliphaunt releases lack staged CI-built runtime artifacts", - ) - require_text( - "tools/release/release.py", - "liboliphaunt-wasix requires staged release assets", - "release CLI must fail closed when WASIX releases lack staged CI-built runtime artifacts", - ) - require_text( - "tools/release/release.py", - "requires staged JSR source", - "release CLI must fail closed when TypeScript JSR release artifacts are not staged", - ) - require_text( - ".github/workflows/release.yml", - "Download SDK package artifacts", - "release workflow must download SDK package artifacts from the CI workflow before publishing", - ) - require_text( - ".github/workflows/release.yml", - "Download liboliphaunt release assets", - "release workflow must download complete liboliphaunt assets from the CI workflow before publishing", - ) - require_text( - ".github/workflows/release.yml", - "Download native helper release assets", - "release workflow must download broker and Node direct helper assets from the CI workflow before publishing those helper products", - ) - require_text( - ".github/workflows/release.yml", - "Download WASIX release assets", - "release workflow must download complete WASIX runtime release assets from the CI workflow before publishing", - ) - require_text( - ".github/workflows/release.yml", - "Upload WASIX GitHub release assets", - "release workflow must publish WASIX GitHub assets through the liboliphaunt-wasix runtime product", - ) - require_text( - ".github/workflows/release.yml", - "--product liboliphaunt-wasix --step github-release-assets", - "release workflow must publish WASIX GitHub assets through the liboliphaunt-wasix runtime product", - ) - require_text( - ".github/workflows/release.yml", - "--product liboliphaunt-wasix --step crates-io", - "release workflow must publish liboliphaunt-wasix Cargo artifact packages before the WASIX Rust binding", - ) - require_text( - ".github/workflows/release.yml", - "tools/dev/bun.sh tools/release/release_graph_query.mjs ci-artifact-names --product \"$product\" --kind \"$kind\" --family release-assets --format lines", - "release workflow must derive native helper release artifact names from target metadata", - ) - require_text( - ".github/workflows/release.yml", - '[ "$PRODUCT_OLIPHAUNT_BROKER" = "true" ]', - "broker helper releases must download broker artifacts from CI", - ) - require_text( - ".github/workflows/release.yml", - '[ "$PRODUCT_OLIPHAUNT_NODE_DIRECT" = "true" ]', - "Node direct helper releases must download Node direct artifacts from CI", - ) - require_text( - ".github/workflows/release.yml", - "tools/dev/bun.sh tools/release/release_graph_query.mjs ci-artifact-names --product oliphaunt-node-direct --kind node-direct-addon --family npm-package --format lines", - "release workflow must derive Node direct npm package artifact names from target metadata", - ) - require_text( - ".github/workflows/release.yml", - "target/oliphaunt-broker/release-assets", - "release workflow must download broker artifacts into the canonical broker release asset root", - ) - require_text( - ".github/workflows/release.yml", - "target/oliphaunt-node-direct/release-assets", - "release workflow must download Node direct artifacts into the canonical Node direct release asset root", - ) - require_text( - ".github/workflows/release.yml", - "--product liboliphaunt-native --step npm", - "release workflow must publish liboliphaunt artifact packages to npm before dependent SDK packages", - ) - require_text( - ".github/workflows/release.yml", - "--product oliphaunt-broker --step npm", - "release workflow must publish broker artifact packages to npm before dependent SDK packages", - ) - require_text( - ".github/workflows/release.yml", - "--product liboliphaunt-native --step crates-io", - "release workflow must publish liboliphaunt native Cargo artifact packages before dependent Rust SDK packages", - ) - require_text( - ".github/workflows/release.yml", - "--product oliphaunt-broker --step crates-io", - "release workflow must publish broker artifact packages to crates.io before dependent Rust SDK packages", - ) - require_text( - "tools/release/release.py", - "npm-package-sources", - "npm artifact packages must be assembled from staged package sources instead of mutating checked-in package directories", - ) - require_text( - "tools/release/release.py", - "package-liboliphaunt-cargo-artifacts.mjs", - "liboliphaunt native Cargo artifact packages must be generated from staged native release assets", - ) - require_text( - "tools/release/release.py", - "package_broker_cargo_artifacts.mjs", - "broker Cargo artifact packages must be generated from staged broker release assets", - ) - require_text( - "tools/release/release.py", - "package_liboliphaunt_wasix_cargo_artifacts.mjs", - "liboliphaunt-wasix Cargo artifact packages must be generated from staged WASIX release assets", - ) - require_text( - "tools/release/release.py", - "liboliphaunt_wasix_cargo_artifact_crates", - "release CLI must package and validate direct WASIX Cargo artifact crates", - ) - require_text( - "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", - "CRATES_IO_MAX_BYTES", - "WASIX Cargo artifact packager must enforce the crates.io package size limit", - ) - require_text( - "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", - "validateCrateSize", - "WASIX Cargo artifact packager must validate direct artifact crate sizes", - ) - reject_text( - "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", - "DEFAULT_PART_COUNT", - "WASIX Cargo artifact packager must not generate reserved part crates", - ) - require_text( - "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", - "wasixExtensionAotPartPackageName", - "WASIX Cargo artifact packager may only generate named part crates for oversized extension AOT artifacts", - ) - require_text( - "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", - "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", - "artifact_npm_package_targets", - "liboliphaunt and broker npm artifact packages must derive package targets from artifact target metadata", - ) - reject_text( - "tools/release/release.py", - "LIBOLIPHAUNT_NPM_PACKAGE_DIRS", - "liboliphaunt npm package target mapping must not be duplicated outside artifact target metadata", - ) - reject_text( - "tools/release/release.py", - "BROKER_NPM_PACKAGE_DIRS", - "broker npm package target mapping must not be duplicated outside artifact target metadata", - ) - require_text( - "tools/release/release.py", - "required_runtime_member_paths", - "liboliphaunt npm artifact packages must include the selected platform runtime tree", - ) - require_text( - "tools/release/package-liboliphaunt-cargo-artifacts.mjs", - "optimizeNativePayload(", - "liboliphaunt Cargo artifact packages must prune and validate native runtime payloads before splitting", - ) - reject_text( - ".github/workflows/release.yml", - "target/release-assets/native", - "release workflow must not stage native helper artifacts in a generic release-assets/native bucket", - ) - require_text( - "tools/release/build-sdk-ci-artifacts.mjs", - 'stageJsrSourceWorkspace(packageShapeDir, path.join(artifactRoot, "jsr-source"))', - "TypeScript SDK builder must stage source for JSR publishing in addition to the npm tarball", - ) - require_text( - "tools/release/release.py", - 'staged_jsr_source_dir("oliphaunt-js")', - "TypeScript SDK release must publish JSR from staged CI-built source artifacts", - ) - require_text( - "tools/release/release.py", - "validate_staged_npm_package_tarball", - "npm SDK release steps must validate CI-built package tarballs before dry-run or publish", - ) - require_text( - "tools/release/release.py", - "must not contain workspace: dependency specifiers", - "staged npm SDK package validation must reject unpublished workspace protocol specs", - ) - require_text( - "tools/release/release.py", - "verify_staged_cargo_crate_identity", - "Cargo SDK release steps must verify staged CI-built .crate identity before dry-run or publish", - ) - for forbidden in ( - "tools/release/package-liboliphaunt-assets.sh", - "tools/release/package-broker-assets.sh", - "src/runtimes/node-direct/tools/build-node-addon.sh", - "src/extensions/artifacts/native/tools/package-release-assets.sh", - "src/extensions/artifacts/wasix/tools/package-release-assets.sh", - "tools/release/build-extension-ci-artifacts.mjs", - "src/sdks/kotlin/tools/check-sdk.sh", - "src/sdks/react-native/tools/check-sdk.sh", - "src/sdks/js/tools/check-sdk.sh", - 'xtask(["release", "stage"])', - '"--staged-wasm"', - '"--staged-wasix-runtime"', - "OLIPHAUNT_RELEASE_REQUIRE_STAGED_", - "OLIPHAUNT_WASM_RELEASE_STAGED", - ): - reject_text( - "tools/release/release.py", - forbidden, - f"release CLI must consume staged CI artifacts, not retain local fallback path {forbidden}", - ) - for forbidden in ( - "OLIPHAUNT_RELEASE_REQUIRE_STAGED_", - "OLIPHAUNT_WASM_RELEASE_STAGED", - ): - reject_text( - ".github/workflows/release.yml", - forbidden, - f"release workflow must not rely on staged-mode env flag {forbidden}; release CLI is staged-artifact-only", - ) - reject_text( - ".github/workflows/release.yml", - "Build liboliphaunt Linux asset", - "release workflow must not rebuild liboliphaunt Linux assets; it must consume CI artifacts", - ) - reject_text( - ".github/workflows/release.yml", - "Build liboliphaunt Windows asset", - "release workflow must not rebuild liboliphaunt Windows assets; it must consume CI artifacts", - ) - reject_text( - ".github/workflows/release.yml", - "Build broker Linux asset", - "release workflow must not rebuild broker Linux assets; it must consume CI artifacts", - ) - reject_text( - ".github/workflows/release.yml", - "Build Node direct native asset", - "release workflow must not rebuild Node direct assets; it must consume CI artifacts", - ) - require_text( - ".github/scripts/download-build-artifacts.mjs", - "artifactPresent", - "shared artifact downloader must select a successful CI run containing every requested artifact", - ) - require_text( - ".github/scripts/download-build-artifacts.mjs", - "requiredJobSuccess", - "shared artifact downloader must support the builder-gate handoff when non-builder checks fail", - ) - require_text( - ".github/workflows/release.yml", - "require-workflow-success.sh CI \"$RELEASE_HEAD_SHA\" 7200 --job Builds", - "release workflow must require the selected release commit CI artifact builder gate instead of the whole workflow conclusion", - ) - require_text( - ".github/workflows/release.yml", - "--job Builds", - "release workflow artifact downloads must select artifacts from a run whose builds job succeeded", - ) - require_text( - ".github/scripts/download-wasix-runtime-build-artifacts.mjs", - 'args.push("--required-job", "Builds", "--all-targets")', - "WASIX runtime artifact handoff must download from a CI run whose builds job succeeded", - ) - require_text( - "tools/xtask/src/asset_io.rs", - "run_has_required_job_success", - "xtask WASIX artifact downloads must support filtering selected release runs by required builder job", - ) - if release.index("Download SDK package artifacts") > release.index("Validate selected release product dry-runs"): - fail("release workflow must stage SDK artifacts before selected release product dry-runs") - if release.index("Download liboliphaunt release assets") > release.index("Validate selected release product dry-runs"): - fail("release workflow must stage liboliphaunt runtime artifacts before selected release product dry-runs") - if release.index("Download native helper release assets") > release.index("Validate selected release product dry-runs"): - fail("release workflow must stage native helper artifacts before selected release product dry-runs") - if release.index("Download WASIX release assets") > release.index("Validate selected release product dry-runs"): - fail("release workflow must stage WASIX runtime release assets before selected release product dry-runs") - if release.index("--product liboliphaunt-wasix --step crates-io") > release.index("--product oliphaunt-wasix-rust --step crates-io"): - fail("release workflow must publish liboliphaunt-wasix Cargo artifact crates before oliphaunt-wasix") - extension_packages_block = ci[ci.index("extension-packages:") : ci.index(" liboliphaunt-native-desktop:")] - if "Download portable WASIX runtime outputs" in extension_packages_block: - fail("extension-packages must consume WASIX extension artifact outputs, not raw portable runtime outputs") - - -def validate_target_matrices() -> None: - ci = read_text(".github/workflows/ci.yml") - release = read_text(".github/workflows/release.yml") - planner = read_text("tools/graph/ci_plan.mjs") - for output_name in ( - "liboliphaunt_native_desktop_runtime_matrix", - "liboliphaunt_native_android_runtime_matrix", - "liboliphaunt_native_ios_runtime_matrix", - ): - if output_name not in ci or f"fromJson(needs.affected.outputs.{output_name})" not in ci: - fail(f"CI {output_name} matrix must come from affected planner output") - for output_name, helper in ( - ("liboliphaunt_native_desktop_runtime_matrix", "liboliphauntNativeDesktopRuntimeMatrix"), - ("liboliphaunt_native_android_runtime_matrix", "liboliphauntNativeAndroidRuntimeMatrix"), - ("liboliphaunt_native_ios_runtime_matrix", "liboliphauntNativeIosRuntimeMatrix"), - ): - require_text( - "tools/graph/ci_plan.mjs", - helper, - f"CI affected planner must derive {output_name} from release metadata artifact targets", - ) - if "broker_runtime_matrix" not in ci or "fromJson(needs.affected.outputs.broker_runtime_matrix)" not in ci: - fail("CI broker matrix must come from affected planner output") - if "node_direct_runtime_matrix" not in ci or "fromJson(needs.affected.outputs.node_direct_runtime_matrix)" not in ci: - fail("CI Node direct matrix must come from affected planner output") - if ( - "extension_artifacts_wasix_matrix" not in ci - or "fromJson(needs.affected.outputs.extension_artifacts_wasix_matrix)" not in ci - ): - fail("CI WASIX extension artifact matrix must come from affected planner output") - require_text( - ".github/workflows/ci.yml", - "Build native exact-extension artifacts", - "CI must build native exact-extension artifacts in their own producer job", - ) - if ( - "extension_artifacts_native_matrix" not in ci - or "fromJson(needs.affected.outputs.extension_artifacts_native_matrix)" not in ci - ): - fail("CI native extension artifact matrix must come from affected planner output") - require_text( - "src/extensions/artifacts/native/moon.yml", - "src/extensions/artifacts/native/tools/package-release-assets.sh", - "CI native exact-extension artifact producer must use the release-shaped native extension packager", - ) - require_text( - "src/extensions/artifacts/packages/moon.yml", - "tools/release/build-extension-ci-artifacts.mjs --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\"", - "WASIX AOT target build must validate target AOT artifacts", - ) - if "native-release-targets:" in release or "native-release-assets:" in release: - 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 "../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") - liboliphaunt_targets = {item["target"] for item in liboliphaunt_matrix["include"]} - expected_liboliphaunt_targets = { - target.target - for target in artifact_targets( - product="liboliphaunt-native", - kind="native-runtime", - published_only=True, - ) - } - if liboliphaunt_targets != expected_liboliphaunt_targets: - fail( - "liboliphaunt CI matrix does not match published native runtime targets: " - f"{sorted(liboliphaunt_targets)} vs {sorted(expected_liboliphaunt_targets)}" - ) - - extension_native_matrix = artifact_target_matrix("extension-artifacts-native") - extension_native_pairs = { - (product, item["target"]) - for item in extension_native_matrix["include"] - for product in item["extensions_csv"].split(",") - if product - } - expected_extension_native_pairs = { - (target.product, target.target) - for target in extension_artifact_targets(family="native", published_only=True) - } - if extension_native_pairs != expected_extension_native_pairs: - fail( - "native extension artifact CI matrix does not match published exact-extension native product/target pairs: " - f"{sorted(extension_native_pairs)} vs {sorted(expected_extension_native_pairs)}" - ) - - broker_matrix = artifact_target_matrix("broker-runtime") - broker_targets = {item["target"] for item in broker_matrix["include"]} - expected_broker_targets = { - target.target - for target in artifact_targets( - product="oliphaunt-broker", - kind="broker-helper", - published_only=True, - ) - } - if broker_targets != expected_broker_targets: - fail( - "broker CI matrix does not match published broker helper targets: " - f"{sorted(broker_targets)} vs {sorted(expected_broker_targets)}" - ) - - 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 - for target in artifact_targets( - product="oliphaunt-node-direct", - kind="node-direct-addon", - published_only=True, - ) - } - if node_direct_targets != expected_node_direct_targets: - fail( - "Node direct CI matrix does not match published Node direct targets: " - f"{sorted(node_direct_targets)} vs {sorted(expected_node_direct_targets)}" - ) - - extension_wasix_matrix = artifact_target_matrix("extension-artifacts-wasix") - extension_wasix_pairs = { - (product, item["target"]) - for item in extension_wasix_matrix["include"] - for product in item["extensions_csv"].split(",") - if product - } - expected_extension_wasix_pairs = { - (target.product, target.target) - for target in extension_artifact_targets(family="wasix", published_only=True) - } - if extension_wasix_pairs != expected_extension_wasix_pairs: - fail( - "WASIX extension artifact CI matrix does not match published exact-extension WASIX product/target pairs: " - f"{sorted(extension_wasix_pairs)} vs {sorted(expected_extension_wasix_pairs)}" - ) - - -def validate_typescript_runtime_targets() -> None: - for target in artifact_targets( - product="liboliphaunt-native", - kind="native-runtime", - surface="typescript-native-direct", - ): - path = "src/sdks/js/src/native/common.ts" - if target.published: - if target.npm_package is None: - fail(f"{target.id} must declare npm_package for TypeScript native resolution") - if target.library_relative_path is None: - fail(f"{target.id} must declare library_relative_path for TypeScript native resolution") - require_text(path, target.npm_package, f"TypeScript native resolver must advertise {target.id}") - require_text(path, target.target, f"TypeScript native resolver must expose target id {target.target}") - require_text( - path, - target.library_relative_path, - f"TypeScript native resolver must expose library path for {target.id}", - ) - require_text( - path, - "runtimeRelativePath", - f"TypeScript native resolver must expose runtime package path for {target.id}", - ) - else: - if target.npm_package is not None: - reject_text(path, target.npm_package, f"TypeScript native resolver must not advertise unpublished target {target.id}") - reject_text(path, target.target, f"TypeScript native resolver must not expose unpublished target id {target.target}") - - for target in artifact_targets( - product="oliphaunt-broker", - kind="broker-helper", - surface="typescript-broker", - ): - path = "src/sdks/js/src/runtime/broker.ts" - if target.published: - if target.npm_package is None: - fail(f"{target.id} must declare npm_package for TypeScript broker resolution") - if target.executable_relative_path is None: - fail(f"{target.id} must declare executable_relative_path for TypeScript broker resolution") - require_text(path, target.npm_package, f"TypeScript broker resolver must advertise {target.id}") - require_text(path, target.target, f"TypeScript broker resolver must expose target id {target.target}") - require_text( - path, - target.executable_relative_path, - f"TypeScript broker resolver must expose executable path for {target.id}", - ) - else: - if target.npm_package is not None: - reject_text(path, target.npm_package, f"TypeScript broker resolver must not advertise unpublished target {target.id}") - reject_text(path, target.target, f"TypeScript broker resolver must not expose unpublished target id {target.target}") - - for target in artifact_targets( - product="oliphaunt-node-direct", - kind="node-direct-addon", - surface="npm-optional", - ): - path = "src/sdks/js/src/native/node-addon.ts" - if target.published: - if target.npm_package is None: - fail(f"{target.id} must declare npm_package for TypeScript Node direct resolution") - require_text(path, target.npm_package, f"TypeScript Node direct resolver must advertise {target.id}") - require_text(path, target.target, f"TypeScript Node direct resolver must expose target id {target.target}") - require_text( - path, - "ADDON_STEM", - f"TypeScript Node direct resolver must expose addon path for {target.id}", - ) - else: - if target.npm_package is not None: - reject_text(path, target.npm_package, f"TypeScript Node direct resolver must not advertise unpublished target {target.id}") - reject_text(path, target.target, f"TypeScript Node direct resolver must not expose unpublished target id {target.target}") - - -def validate_rust_broker_targets() -> None: - manifest = "src/sdks/rust/Cargo.toml" - path = "src/sdks/rust/src/broker.rs" - require_text( - manifest, - 'broker-helper = "oliphaunt-broker"', - "Rust SDK package metadata must identify the broker helper runtime it consumes", - ) - require_text( - manifest, - f'broker-version = "{read_current_version("oliphaunt-broker")}"', - "Rust SDK package metadata must pin the compatible broker helper version", - ) - require_text( - path, - "OLIPHAUNT_BROKER_ASSET_DIR", - "Rust broker resolver must support package-shaped broker artifact fixtures", - ) - for target in artifact_targets( - product="oliphaunt-broker", - kind="broker-helper", - surface="rust-broker", - ): - if target.published: - require_text(path, target.asset, f"Rust broker resolver must advertise {target.id}") - require_text(path, target.target, f"Rust broker resolver must expose target id {target.target}") - if target.executable_relative_path is not None: - require_text( - path, - target.executable_relative_path, - f"Rust broker resolver must expose helper path for {target.id}", - ) - else: - reject_text(path, target.asset, f"Rust broker resolver must not advertise unpublished target {target.id}") - reject_text(path, target.target, f"Rust broker resolver must not expose unpublished target id {target.target}") - - -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", - "liboliphaunt-{version}-android-x86_64.tar.gz", - "liboliphaunt-{version}-runtime-resources.tar.gz", - "liboliphaunt-{version}-icu-data.tar.gz", - "liboliphaunt-{version}-package-size.tsv", - "liboliphaunt-{version}-release-assets.sha256", - }, - "oliphaunt-broker": { - "oliphaunt-broker-{version}-macos-arm64.tar.gz", - "oliphaunt-broker-{version}-linux-x64-gnu.tar.gz", - "oliphaunt-broker-{version}-linux-arm64-gnu.tar.gz", - "oliphaunt-broker-{version}-windows-x64-msvc.zip", - "oliphaunt-broker-{version}-release-assets.sha256", - }, - "oliphaunt-node-direct": { - "oliphaunt-node-direct-{version}-macos-arm64.tar.gz", - "oliphaunt-node-direct-{version}-linux-x64-gnu.tar.gz", - "oliphaunt-node-direct-{version}-linux-arm64-gnu.tar.gz", - "oliphaunt-node-direct-{version}-windows-x64-msvc.zip", - "oliphaunt-node-direct-{version}-release-assets.sha256", - }, - "liboliphaunt-wasix": { - "liboliphaunt-wasix-{version}-runtime-portable.tar.zst", - "liboliphaunt-wasix-{version}-icu-data.tar.zst", - "liboliphaunt-wasix-{version}-runtime-aot-macos-arm64.tar.zst", - "liboliphaunt-wasix-{version}-runtime-aot-linux-x64-gnu.tar.zst", - "liboliphaunt-wasix-{version}-runtime-aot-linux-arm64-gnu.tar.zst", - "liboliphaunt-wasix-{version}-runtime-aot-windows-x64-msvc.tar.zst", - "liboliphaunt-wasix-{version}-release-assets.sha256", - }, - } - for product, assets in expected.items(): - actual = { - target.asset - for target in artifact_targets( - product=product, - surface="github-release", - published_only=True, - ) - } - if actual != assets: - fail(f"{product} published artifact targets expected {sorted(assets)}, got {sorted(actual)}") - - -def main() -> int: - validate_target_shape() - validate_moon_runtime_targets() - validate_extension_artifact_targets() - validate_github_asset_helpers() - validate_ci_release_artifacts() - validate_target_matrices() - validate_typescript_runtime_targets() - validate_rust_broker_targets() - validate_expected_product_assets() - print("artifact target checks passed") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 7cb1987f..3fd7dbfb 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -619,7 +619,7 @@ def validate_graph_files() -> None: release_pr_coverage = read_text("tools/release/check_release_pr_coverage.mjs") build_extension_ci_artifacts = read_text("tools/release/build-extension-ci-artifacts.mjs") check_staged_artifacts = read_text("tools/release/check-staged-artifacts.mjs") - check_artifact_targets = read_text("tools/release/check_artifact_targets.py") + check_artifact_targets = read_text("tools/release/check_artifact_targets.mjs") check_consumer_shape = read_text("tools/release/check_consumer_shape.py") extension_model = read_text("src/extensions/tools/check-extension-model.py") extension_model_moon = read_text("src/extensions/model/moon.yml") @@ -639,7 +639,7 @@ def validate_graph_files() -> None: or "export function extensionMetadata(" not in release_artifact_targets or "export function extensionSourceIdentity(" not in release_artifact_targets or "exactExtensionProducts(TOOL)" not in release_graph_query - or "extension_products = extension_product_ids()" not in check_artifact_targets + or "const extensionProducts = extensionProductIds();" not in check_artifact_targets or "return set(extension_product_ids())" not in check_consumer_shape or "const modeledExtensionProducts = new Set(extensionProductIds());" not in release_policy or "import product_metadata" in release_policy @@ -707,7 +707,7 @@ def validate_graph_files() -> None: fail("release policy must consume normalized Bun Moon project rows and product-config metadata") if ( "legacy-central-artifact-targets" not in release_graph_query - or 'release_graph_rows("legacy-central-artifact-targets")' not in check_artifact_targets + or 'releaseGraphRows("legacy-central-artifact-targets")' not in check_artifact_targets or ("product_metadata." + "load_graph()") in check_artifact_targets or ("def " + "load_graph()") in check_release_metadata_source or ("product_metadata." + "load_graph()") in check_release_metadata_source diff --git a/tools/release/release.py b/tools/release/release.py index b50b7fe4..372875f4 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -2195,7 +2195,7 @@ def run_product_publish_dry_runs(products: list[str], *, allow_dirty: bool, head def command_check(args: list[str]) -> None: run(["tools/dev/bun.sh", "tools/policy/check-release-policy.mjs"]) run(["tools/release/check_release_please_config.mjs"]) - run(["python3", "tools/release/check_artifact_targets.py"]) + run(["tools/dev/bun.sh", "tools/release/check_artifact_targets.mjs"]) run(["tools/dev/bun.sh", "tools/release/sync-release-pr.mjs", "--check"]) run(["bun", "tools/release/check_release_pr_coverage.mjs"]) run(["python3", "tools/release/check_release_metadata.py"]) From 43d2230e16c906b3f028983069f8e7861e4a7ac6 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 22:44:26 +0000 Subject: [PATCH 232/308] chore: move local registry metadata queries to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 5 + tools/release/check_release_metadata.py | 35 +++- tools/release/local_registry_metadata.mjs | 160 ++++++++++++++++++ 3 files changed, 195 insertions(+), 5 deletions(-) create mode 100644 tools/release/local_registry_metadata.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 7bc2a679..f7391796 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -2196,6 +2196,11 @@ until the current-state gates here are checked with fresh local evidence. 4. port `src/extensions/tools/check-extension-model.py` as a separate generator migration, because it is the canonical multi-language extension model and needs generated-output parity across SDKs. +- The local-registry metadata needed by release metadata checks now has a Bun + helper in `tools/release/local_registry_metadata.mjs`. It exposes the + local-publish artifact preset and extension manifest discovery/dedupe without + importing `local_registry_publish.py`, so `check_release_metadata.py` no + longer depends on another Python module while it awaits its full Bun port. - While those Python entrypoints remain, policy tooling now keeps Python compile bytecode out of source/tool directories. `check-policy-tools.sh` routes `py_compile` output through `PYTHONPYCACHEPREFIX` under its temp directory, diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 3fd7dbfb..97229061 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -67,6 +67,25 @@ def release_graph_json(command: str, args: tuple[str, ...] = ()) -> Any: fail(f"release graph {command} query did not return valid JSON: {error}") +def local_registry_metadata_json(command: str, args: tuple[str, ...] = ()) -> Any: + try: + output = subprocess.check_output( + ["tools/dev/bun.sh", "tools/release/local_registry_metadata.mjs", command, *args], + cwd=ROOT, + text=True, + stderr=subprocess.PIPE, + ) + except subprocess.CalledProcessError as error: + detail = (error.stderr or "").strip() + if detail: + fail(f"local registry metadata {command} query failed: {detail}") + fail(f"local registry metadata {command} query failed with exit code {error.returncode}") + try: + return json.loads(output) + except json.JSONDecodeError as error: + fail(f"local registry metadata {command} query did not return valid JSON: {error}") + + @lru_cache(maxsize=None) def release_graph_rows(command: str, args: tuple[str, ...] = ()) -> tuple[dict[str, Any], ...]: rows = release_graph_json(command, args) @@ -629,6 +648,8 @@ def validate_graph_files() -> None: check_release_metadata_source = read_text("tools/release/check_release_metadata.py") if re.search(r"(?m)^import product_metadata$", check_release_metadata_source): fail("check_release_metadata.py must consume Bun release graph rows instead of importing product_metadata.py") + if re.search(r"(?m)^import local_registry_publish$", check_release_metadata_source) or "local_registry_metadata.mjs" not in check_release_metadata_source: + fail("check_release_metadata.py must consume local registry metadata through the Bun helper instead of importing local_registry_publish.py") if ( "compatibility-version-entries [--require-source-product]" not in release_graph_query or "compatibilityVersionEntries(graphProducts()" not in sync_release_pr @@ -899,8 +920,6 @@ 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") @@ -933,7 +952,9 @@ def validate_local_registry_publisher() -> None: 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() + artifacts = local_registry_metadata_json("local-publish-artifacts") + if not isinstance(artifacts, list) or not all(isinstance(item, str) and item for item in artifacts): + fail("Bun local registry metadata helper must return local-publish artifact names as a non-empty string list") 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)) @@ -972,8 +993,12 @@ def validate_local_registry_publisher() -> None: + "\n", encoding="utf-8", ) - manifests = local_registry_publish.discover_extension_manifests([first.parent, second.parent]) - if manifests != [first / "extension-artifacts.json"]: + manifests = local_registry_metadata_json( + "discover-extension-manifests", + ("--root", str(first.parent), "--root", str(second.parent)), + ) + expected_manifest = str(first / "extension-artifacts.json") + if manifests != [expected_manifest]: fail("local registry extension manifest discovery must deduplicate product/version/sql rows by root priority") diff --git a/tools/release/local_registry_metadata.mjs b/tools/release/local_registry_metadata.mjs new file mode 100644 index 00000000..9d93840d --- /dev/null +++ b/tools/release/local_registry_metadata.mjs @@ -0,0 +1,160 @@ +#!/usr/bin/env bun +import { existsSync, readFileSync, realpathSync, statSync } from "node:fs"; +import path from "node:path"; + +import { compareText, localPublishArtifactRows } from "./release-artifact-targets.mjs"; + +const ROOT = path.resolve(import.meta.dir, "../.."); +const TOOL = "local_registry_metadata.mjs"; + +function fail(message) { + console.error(`${TOOL}: ${message}`); + process.exit(1); +} + +function usage() { + return `usage: tools/release/local_registry_metadata.mjs + +Commands: + local-publish-artifacts [--aggregate-only] + discover-extension-manifests --root PATH [--root PATH...] +`; +} + +function sortedUnique(values) { + return [...new Set(values)].sort(compareText); +} + +export function localPublishArtifactNames({ aggregateOnly = false } = {}) { + const names = localPublishArtifactRows({ aggregateOnly }, TOOL).map((row) => row.artifactName); + if (names.length === 0) { + fail("release graph returned no local-publish artifacts"); + } + const unique = sortedUnique(names); + if (unique.length !== names.length) { + const duplicates = unique.filter((name) => names.filter((candidate) => candidate === name).length > 1); + fail(`release graph returned duplicate local-publish artifacts: ${duplicates.join(", ")}`); + } + return unique; +} + +export function localPublishArtifacts() { + return localPublishArtifactNames(); +} + +export function localPublishAggregateArtifacts() { + return localPublishArtifactNames({ aggregateOnly: true }); +} + +function repoRelativeOrAbsolute(file) { + const relative = path.relative(ROOT, file); + return relative.startsWith("..") || path.isAbsolute(relative) + ? file + : relative.split(path.sep).join("/"); +} + +function extensionManifestIdentity(manifest) { + let data; + try { + data = JSON.parse(readFileSync(manifest, "utf8")); + } catch { + return ["path", realpathSync(manifest)]; + } + const product = data.product; + const version = data.version; + const sqlName = data.sqlName; + if ([product, version, sqlName].every((value) => typeof value === "string" && value.length > 0)) { + return ["extension", product, version, sqlName]; + } + return ["path", realpathSync(manifest)]; +} + +function extensionManifestCandidates(root) { + if (!existsSync(root)) { + return []; + } + const stat = statSync(root); + if (stat.isFile() && path.basename(root) === "extension-artifacts.json") { + return [root]; + } + if (!stat.isDirectory()) { + return []; + } + return [...new Bun.Glob("**/extension-artifacts.json").scanSync({ cwd: root, absolute: true })] + .filter((candidate) => statSync(candidate).isFile()) + .sort(compareText); +} + +export function discoverExtensionManifests(roots) { + const manifests = new Map(); + const seenPaths = new Set(); + for (const root of roots) { + for (const manifest of extensionManifestCandidates(root)) { + const resolved = realpathSync(manifest); + if (seenPaths.has(resolved)) { + continue; + } + seenPaths.add(resolved); + const identity = JSON.stringify(extensionManifestIdentity(manifest)); + if (!manifests.has(identity)) { + manifests.set(identity, manifest); + } + } + } + return [...manifests.values()]; +} + +function parseRoots(argv) { + const roots = []; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === "--root") { + if (index + 1 >= argv.length) { + fail("--root requires a value"); + } + roots.push(path.resolve(ROOT, argv[index + 1])); + index += 1; + } else if (value.startsWith("--root=")) { + roots.push(path.resolve(ROOT, value.slice("--root=".length))); + } else { + fail(`unknown argument: ${value}`); + } + } + if (roots.length === 0) { + fail("discover-extension-manifests requires at least one --root"); + } + return roots; +} + +function printJson(value) { + console.log(`${JSON.stringify(value, null, 2)}\n`.trimEnd()); +} + +function main(argv) { + const [command, ...rest] = argv; + if (command === "--help" || command === "-h" || command === undefined) { + console.log(usage()); + return command === undefined ? 1 : 0; + } + if (command === "local-publish-artifacts") { + let aggregateOnly = false; + for (const arg of rest) { + if (arg === "--aggregate-only") { + aggregateOnly = true; + } else { + fail(`unknown argument: ${arg}`); + } + } + printJson(localPublishArtifactNames({ aggregateOnly })); + return 0; + } + if (command === "discover-extension-manifests") { + printJson(discoverExtensionManifests(parseRoots(rest)).map(repoRelativeOrAbsolute)); + return 0; + } + fail(`unknown command: ${command}`); +} + +if (import.meta.main) { + process.exit(main(Bun.argv.slice(2))); +} From ce216bd7102a1b2b85bb77e7e611c7a32b6ed5d9 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 22:54:30 +0000 Subject: [PATCH 233/308] chore: route local registry metadata through bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 3 + tools/release/check_release_metadata.py | 4 +- tools/release/local_registry_publish.py | 74 +++++++++---------- 3 files changed, 40 insertions(+), 41 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index f7391796..5913122d 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -2201,6 +2201,9 @@ until the current-state gates here are checked with fresh local evidence. local-publish artifact preset and extension manifest discovery/dedupe without importing `local_registry_publish.py`, so `check_release_metadata.py` no longer depends on another Python module while it awaits its full Bun port. + The Python local-registry publisher also consumes that helper for those + metadata decisions, leaving publishing mechanics in Python for now while the + release graph and manifest-dedupe policy live in Bun. - While those Python entrypoints remain, policy tooling now keeps Python compile bytecode out of source/tool directories. `check-policy-tools.sh` routes `py_compile` output through `PYTHONPYCACHEPREFIX` under its temp directory, diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 97229061..72685e2b 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -962,7 +962,9 @@ def validate_local_registry_publisher() -> None: 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 'release_graph_rows("local-publish-artifacts"' not in publisher + or "local_registry_metadata_json(\"local-publish-artifacts\"" not in publisher + or "local_registry_metadata_json(\"discover-extension-manifests\"" not in publisher + or "def extension_manifest_identity" in publisher or "local_publish_artifact_names(aggregate_only=True)" not in publisher or "local_publish_artifact_names()" not in publisher or 'release_graph_rows(\n "artifact-targets"' not in publisher diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py index 6916e297..c5a2c06a 100755 --- a/tools/release/local_registry_publish.py +++ b/tools/release/local_registry_publish.py @@ -76,6 +76,23 @@ def release_graph_json(command: str, args: tuple[str, ...] = ()) -> Any: raise RuntimeError(f"release graph {command} query did not return valid JSON: {error}") from error +def local_registry_metadata_json(command: str, args: tuple[str, ...] = ()) -> Any: + try: + completed = run( + ["tools/dev/bun.sh", "tools/release/local_registry_metadata.mjs", command, *args], + capture=True, + ) + except subprocess.CalledProcessError as error: + detail = (error.stderr or "").strip() + if detail: + raise RuntimeError(f"local registry metadata {command} query failed: {detail}") from error + raise RuntimeError(f"local registry metadata {command} query failed with exit code {error.returncode}") from error + try: + return json.loads(completed.stdout) + except json.JSONDecodeError as error: + raise RuntimeError(f"local registry metadata {command} query did not return valid JSON: {error}") from error + + @lru_cache(maxsize=None) def release_graph_rows(command: str, args: tuple[str, ...] = ()) -> tuple[dict[str, Any], ...]: rows = release_graph_json(command, args) @@ -98,21 +115,15 @@ def local_publish_artifacts() -> list[str]: def local_publish_artifact_names(*, aggregate_only: bool = False) -> list[str]: args = ("--aggregate-only",) if aggregate_only else () - names: list[str] = [] - for row in release_graph_rows("local-publish-artifacts", args): - artifact_name = row.get("artifactName") - aggregate = row.get("aggregate") - if not isinstance(artifact_name, str) or not artifact_name: - raise RuntimeError("release graph local-publish-artifacts rows must declare a non-empty artifactName") - if not isinstance(aggregate, bool): - raise RuntimeError(f"release graph local-publish-artifacts {artifact_name}.aggregate must be true or false") - names.append(artifact_name) + names = local_registry_metadata_json("local-publish-artifacts", args) + if not isinstance(names, list) or not all(isinstance(name, str) and name for name in names): + raise RuntimeError("local registry metadata local-publish-artifacts must return a non-empty string list") if not names: - raise RuntimeError("release graph returned no local-publish artifacts") + raise RuntimeError("local registry metadata returned no local-publish artifacts") duplicates = sorted({name for name in names if names.count(name) > 1}) if duplicates: - raise RuntimeError("release graph returned duplicate local-publish artifacts: " + ", ".join(duplicates)) - return sorted(names) + raise RuntimeError("local registry metadata returned duplicate local-publish artifacts: " + ", ".join(duplicates)) + return names def rel(path: Path) -> str: @@ -520,35 +531,18 @@ def extension_npm_payload_package(sql_name: str, target: str, index: int) -> str def discover_extension_manifests(roots: list[Path]) -> list[Path]: - manifests: dict[tuple[str, ...], Path] = {} - seen_paths: set[Path] = set() + args: list[str] = [] for root in roots: - if root.is_file() and root.name == "extension-artifacts.json": - candidates = [root] - elif root.is_dir(): - candidates = sorted(path for path in root.rglob("extension-artifacts.json") if path.is_file()) - else: - continue - for manifest in candidates: - resolved = manifest.resolve() - if resolved in seen_paths: - continue - seen_paths.add(resolved) - manifests.setdefault(extension_manifest_identity(manifest), manifest) - return list(manifests.values()) - - -def extension_manifest_identity(manifest: Path) -> tuple[str, ...]: - try: - data = json.loads(manifest.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): - return ("path", str(manifest.resolve())) - product = data.get("product") - version = data.get("version") - sql_name = data.get("sqlName") - if all(isinstance(value, str) and value for value in [product, version, sql_name]): - return ("extension", str(product), str(version), str(sql_name)) - return ("path", str(manifest.resolve())) + args.extend(["--root", str(root)]) + if not args: + return [] + values = local_registry_metadata_json("discover-extension-manifests", tuple(args)) + if not isinstance(values, list) or not all(isinstance(value, str) and value for value in values): + raise RuntimeError("local registry metadata discover-extension-manifests must return a string list") + return [ + Path(value) if Path(value).is_absolute() else ROOT / value + for value in values + ] def safe_package_path(package_name: str) -> str: From 21ca590cc3699861deae87bf890046dab61a6041 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 23:10:47 +0000 Subject: [PATCH 234/308] chore: move release check orchestration to bun --- .github/workflows/release.yml | 8 +-- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 8 +++ docs/maintainers/release-setup.md | 6 +- docs/maintainers/release.md | 2 +- tools/policy/check-release-policy.mjs | 10 ++- tools/release/check_release_metadata.py | 12 ++++ tools/release/moon.yml | 6 +- tools/release/release-check.mjs | 62 +++++++++++++++++++ tools/release/release.py | 19 +----- 9 files changed, 102 insertions(+), 31 deletions(-) create mode 100644 tools/release/release-check.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 99321114..0127b1a6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -88,7 +88,7 @@ jobs: - name: Validate release metadata run: | - tools/release/release.py check + tools/dev/bun.sh tools/release/release-check.mjs - name: Require release PR token env: @@ -151,7 +151,7 @@ jobs: tools/dev/bun.sh tools/release/sync-release-pr.mjs tools/dev/bun.sh tools/release/sync-release-pr.mjs --check - tools/release/release.py check + tools/dev/bun.sh tools/release/release-check.mjs if git diff --quiet; then echo "Derived release files already match release-please output." @@ -227,7 +227,7 @@ jobs: - name: Validate release metadata run: | - tools/release/release.py check + tools/dev/bun.sh tools/release/release-check.mjs - name: Enable pnpm for registry release checks run: | @@ -304,7 +304,7 @@ jobs: if: ${{ steps.release_plan.outputs.has_release_changes == 'true' }} env: PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} - run: tools/release/release.py check --products-json "${PRODUCTS_JSON}" + run: tools/dev/bun.sh tools/release/release-check.mjs --products-json "${PRODUCTS_JSON}" - name: Validate product versions and registry state 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 5913122d..50b7f248 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,14 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-27: Moved the active release metadata check orchestration to the Bun + entrypoint `tools/release/release-check.mjs`. Moon `release-tools:check`, + `release-tools:release-check`, and the release workflow now call the Bun + helper directly, while `tools/release/release.py check` remains only a + compatibility delegator. The new helper runs release policy, + release-please config, artifact target, release PR sync/coverage, + release-metadata, and consumer-shape readiness checks in the same order as + the previous Python command. - 2026-06-27: Ported the WASIX Cargo artifact packager from `tools/release/package_liboliphaunt_wasix_cargo_artifacts.py` to the Bun entrypoint `tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs`. diff --git a/docs/maintainers/release-setup.md b/docs/maintainers/release-setup.md index 0a989ced..ca06a4f6 100644 --- a/docs/maintainers/release-setup.md +++ b/docs/maintainers/release-setup.md @@ -70,6 +70,8 @@ compatibility files and lockfile updates back to the same PR when needed. If no release PR exists, the sync step exits cleanly. Run `tools/dev/bun.sh tools/release/sync-release-pr.mjs --check` locally after manual version experiments; it is also part of `tools/release/release.py check`. +The active CI path calls the Bun orchestrator directly: +`tools/dev/bun.sh tools/release/release-check.mjs`. The publish job still needs the repository-scoped `GITHUB_TOKEN` for GitHub release asset uploads, artifact attestations, release-please release creation, @@ -83,7 +85,7 @@ Useful verification: gh repo view f0rr0/oliphaunt gh workflow list --repo f0rr0/oliphaunt tools/dev/bun.sh tools/release/release_plan.mjs --from-product-tags --include-current-tags --head-ref HEAD -tools/release/release.py check +tools/dev/bun.sh tools/release/release-check.mjs ``` For normal releases, leave the `Release` workflow's `release_commit` input @@ -384,7 +386,7 @@ registry state: ```bash moon run dev-tools:doctor -tools/release/release.py check +tools/dev/bun.sh tools/release/release-check.mjs tools/dev/bun.sh tools/release/release_plan.mjs --from-product-tags --include-current-tags --head-ref HEAD tools/release/release.py check-registries --products-json '' --head-ref HEAD tools/release/release.py publish-dry-run --products-json '' --head-ref HEAD diff --git a/docs/maintainers/release.md b/docs/maintainers/release.md index f7095aae..a59973e5 100644 --- a/docs/maintainers/release.md +++ b/docs/maintainers/release.md @@ -58,7 +58,7 @@ Use these commands while preparing or checking releases: ```sh tools/dev/bun.sh tools/release/release_plan.mjs -tools/release/release.py check +tools/dev/bun.sh tools/release/release-check.mjs tools/release/release.py check-registries tools/release/release.py publish-dry-run tools/release/release.py publish diff --git a/tools/policy/check-release-policy.mjs b/tools/policy/check-release-policy.mjs index 0adda3c0..d525db22 100644 --- a/tools/policy/check-release-policy.mjs +++ b/tools/policy/check-release-policy.mjs @@ -1019,7 +1019,11 @@ function checkCiPolicy() { } const releaseWorkflowBlocks = workflowJobBlocks(".github/workflows/release.yml"); - const releaseToolPatterns = ["tools/release/release.py", "tools/release/artifact_target_matrix.mjs"]; + const releaseToolPatterns = [ + "tools/release/release.py", + "tools/release/release-check.mjs", + "tools/release/artifact_target_matrix.mjs", + ]; const missingMoonSetup = sorted( Object.entries(releaseWorkflowBlocks) .filter(([, block]) => releaseToolPatterns.some((pattern) => block.includes(pattern)) && !block.includes("./.github/actions/setup-moon")) @@ -1033,9 +1037,9 @@ function checkCiPolicy() { fail(`missing consumer shape fixture: ${CONSUMER_SHAPE_PRODUCTS_FIXTURE}`); } assertContains( - "tools/release/release.py", + "tools/release/release-check.mjs", "check_release_pr_coverage.mjs", - "release checks must verify release-please version bumps cover Moon-selected products", + "release checks must verify release-please version bumps cover Moon-selected products through the Bun release-check orchestrator", ); for (const repoPath of [ ".github/workflows/release.yml", diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 72685e2b..7dd1651c 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -635,6 +635,7 @@ def validate_graph_files() -> None: release_graph_source = read_text("tools/release/release-graph.mjs") release_artifact_targets = read_text("tools/release/release-artifact-targets.mjs") sync_release_pr = read_text("tools/release/sync-release-pr.mjs") + release_check = read_text("tools/release/release-check.mjs") release_pr_coverage = read_text("tools/release/check_release_pr_coverage.mjs") build_extension_ci_artifacts = read_text("tools/release/build-extension-ci-artifacts.mjs") check_staged_artifacts = read_text("tools/release/check-staged-artifacts.mjs") @@ -700,6 +701,17 @@ def validate_graph_files() -> None: ): fail("product config metadata must be adapted through the Bun release graph product-configs query") release_source = read_text("tools/release/release.py") + release_workflow = read_text(".github/workflows/release.yml") + release_moon = read_text("tools/release/moon.yml") + if ( + '"tools/release/release-check.mjs", *args' not in release_source + or "tools/release/check_release_pr_coverage.mjs" not in release_check + or "tools/release/check_release_metadata.py" not in release_check + or "tools/release/check_consumer_shape.py" not in release_check + or "tools/dev/bun.sh tools/release/release-check.mjs" not in release_workflow + or "tools/dev/bun.sh tools/release/release-check.mjs" not in release_moon + ): + fail("release check orchestration must live in the Bun release-check helper while release.py keeps only a compatibility delegator") if ( "publish-step-target-coverage [--product PRODUCT]" not in release_graph_query or "export function publishStepTargetCoverageRows(" not in release_graph_source diff --git a/tools/release/moon.yml b/tools/release/moon.yml index 8b23edf0..34894e4d 100644 --- a/tools/release/moon.yml +++ b/tools/release/moon.yml @@ -1,7 +1,7 @@ $schema: "https://moonrepo.dev/schemas/project.json" id: "release-tools" -language: "python" +language: "javascript" layer: "tool" stack: "infrastructure" tags: ["tools", "release"] @@ -52,7 +52,7 @@ owners: tasks: check: tags: ["policy", "assertion", "quality", "static"] - command: "tools/release/release.py check" + command: "tools/dev/bun.sh tools/release/release-check.mjs" inputs: - "/.github/**/*" - "/Cargo.lock" @@ -83,7 +83,7 @@ tasks: runFromWorkspaceRoot: true release-check: tags: ["release", "package"] - command: "tools/release/release.py check" + command: "tools/dev/bun.sh tools/release/release-check.mjs" inputs: - "/.github/**/*" - "/Cargo.lock" diff --git a/tools/release/release-check.mjs b/tools/release/release-check.mjs new file mode 100644 index 00000000..2d7185bd --- /dev/null +++ b/tools/release/release-check.mjs @@ -0,0 +1,62 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const TOOL = "release-check.mjs"; + +function fail(message) { + console.error(`${TOOL}: ${message}`); + process.exit(1); +} + +function run(args) { + console.log(`\n==> ${args.join(" ")}`); + const result = spawnSync(args[0], args.slice(1), { + cwd: ROOT, + stdio: "inherit", + }); + if (result.error) { + fail(`${args[0]} failed to start: ${result.error.message}`); + } + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + +function parseArgs(argv) { + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === "-h" || arg === "--help") { + console.log(`usage: tools/release/release-check.mjs [release.py check passthrough args] + +Runs the repository release metadata, release-please, artifact target, +release PR, and consumer-shape readiness checks. Current passthrough flags are +accepted for compatibility with release.py check and release workflow callers. +`); + process.exit(0); + } + } +} + +function main(argv) { + parseArgs(argv); + run(["tools/dev/bun.sh", "tools/policy/check-release-policy.mjs"]); + run(["tools/dev/bun.sh", "tools/release/check_release_please_config.mjs"]); + run(["tools/dev/bun.sh", "tools/release/check_artifact_targets.mjs"]); + run(["tools/dev/bun.sh", "tools/release/sync-release-pr.mjs", "--check"]); + run(["tools/dev/bun.sh", "tools/release/check_release_pr_coverage.mjs"]); + run(["python3", "tools/release/check_release_metadata.py"]); + run(["tools/release/check_consumer_shape.py", "--format", "json", "--require-ready"]); + run([ + "tools/release/check_consumer_shape.py", + "--format", + "json", + "--require-ready", + "--products-json", + '["oliphaunt-react-native"]', + ]); +} + +main(Bun.argv.slice(2)); diff --git a/tools/release/release.py b/tools/release/release.py index 372875f4..638a37eb 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -2193,24 +2193,7 @@ def run_product_publish_dry_runs(products: list[str], *, allow_dirty: bool, head def command_check(args: list[str]) -> None: - run(["tools/dev/bun.sh", "tools/policy/check-release-policy.mjs"]) - run(["tools/release/check_release_please_config.mjs"]) - run(["tools/dev/bun.sh", "tools/release/check_artifact_targets.mjs"]) - run(["tools/dev/bun.sh", "tools/release/sync-release-pr.mjs", "--check"]) - 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( - [ - "tools/release/release.py", - "consumer-shape", - "--format", - "json", - "--require-ready", - "--products-json", - '["oliphaunt-react-native"]', - ] - ) + run(["tools/dev/bun.sh", "tools/release/release-check.mjs", *args]) def command_check_registries(args: list[str]) -> None: From 2ad60d44ac60c51381651f2b9287d28f7c7216ee Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 23:33:28 +0000 Subject: [PATCH 235/308] chore: move release gate wrappers to bun --- .github/workflows/release.yml | 6 +-- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 6 +++ docs/maintainers/release-setup.md | 14 ++--- docs/maintainers/release.md | 6 +-- tools/policy/check-release-policy.mjs | 11 ++-- tools/release/check_release_metadata.py | 24 +++++++-- tools/release/moon.yml | 2 +- tools/release/release-check-registries.mjs | 53 +++++++++++++++++++ tools/release/release-check.mjs | 43 ++++----------- tools/release/release-cli-utils.mjs | 23 ++++++++ tools/release/release-consumer-shape.mjs | 6 +++ tools/release/release-verify.mjs | 36 +++++++++++++ tools/release/release.py | 44 ++------------- 13 files changed, 179 insertions(+), 95 deletions(-) create mode 100644 tools/release/release-check-registries.mjs create mode 100644 tools/release/release-cli-utils.mjs create mode 100644 tools/release/release-consumer-shape.mjs create mode 100644 tools/release/release-verify.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0127b1a6..72761a14 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -312,7 +312,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} - run: tools/release/release.py check-registries --products-json "${PRODUCTS_JSON}" --head-ref "$RELEASE_HEAD_SHA" + run: tools/dev/bun.sh tools/release/release-check-registries.mjs --products-json "${PRODUCTS_JSON}" --head-ref "$RELEASE_HEAD_SHA" - name: Download WASIX runtime build artifacts if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && steps.release_plan.outputs.product_liboliphaunt_wasix == 'true' }} @@ -692,7 +692,7 @@ jobs: run: | gh auth setup-git git fetch --force --tags origin - tools/release/release.py verify-release --products-json "${PRODUCTS_JSON}" --head-ref "$RELEASE_HEAD_SHA" + tools/dev/bun.sh tools/release/release-verify.mjs --products-json "${PRODUCTS_JSON}" --head-ref "$RELEASE_HEAD_SHA" - name: Run consumer shape gates if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' }} @@ -700,4 +700,4 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} - run: tools/release/release.py consumer-shape --require-ready --products-json "${PRODUCTS_JSON}" + run: tools/dev/bun.sh tools/release/release-consumer-shape.mjs --require-ready --products-json "${PRODUCTS_JSON}" diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 50b7f248..f294a644 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -86,6 +86,12 @@ until the current-state gates here are checked with fresh local evidence. release-please config, artifact target, release PR sync/coverage, release-metadata, and consumer-shape readiness checks in the same order as the previous Python command. +- 2026-06-27: Moved the remaining non-publish release workflow command + surfaces to Bun helpers: `release-check-registries.mjs`, + `release-verify.mjs`, and `release-consumer-shape.mjs`. The release workflow + and Moon consumer-shape task now use those helpers directly; `release.py` + keeps compatibility delegators for existing local command habits while active + CI/release orchestration is no longer routed through Python for these gates. - 2026-06-27: Ported the WASIX Cargo artifact packager from `tools/release/package_liboliphaunt_wasix_cargo_artifacts.py` to the Bun entrypoint `tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs`. diff --git a/docs/maintainers/release-setup.md b/docs/maintainers/release-setup.md index ca06a4f6..268b71c2 100644 --- a/docs/maintainers/release-setup.md +++ b/docs/maintainers/release-setup.md @@ -373,9 +373,9 @@ Asset provenance requires: The release workflow already declares those permissions. Verification uses: ```bash -tools/release/release.py verify-release --products-json '["liboliphaunt-native"]' --head-ref HEAD -tools/release/release.py verify-release --products-json '["oliphaunt-rust"]' --head-ref HEAD -tools/release/release.py verify-release --products-json '["oliphaunt-wasix-rust"]' --head-ref HEAD +tools/dev/bun.sh tools/release/release-verify.mjs --products-json '["liboliphaunt-native"]' --head-ref HEAD +tools/dev/bun.sh tools/release/release-verify.mjs --products-json '["oliphaunt-rust"]' --head-ref HEAD +tools/dev/bun.sh tools/release/release-verify.mjs --products-json '["oliphaunt-wasix-rust"]' --head-ref HEAD ``` ## Setup Validation @@ -388,9 +388,9 @@ registry state: moon run dev-tools:doctor tools/dev/bun.sh tools/release/release-check.mjs tools/dev/bun.sh tools/release/release_plan.mjs --from-product-tags --include-current-tags --head-ref HEAD -tools/release/release.py check-registries --products-json '' --head-ref HEAD +tools/dev/bun.sh tools/release/release-check-registries.mjs --products-json '' --head-ref HEAD tools/release/release.py publish-dry-run --products-json '' --head-ref HEAD -tools/release/release.py consumer-shape --require-ready --format markdown +tools/dev/bun.sh tools/release/release-consumer-shape.mjs --require-ready --format markdown ``` For the first public release, select every product that introduces a public @@ -445,8 +445,8 @@ Run these from GitHub Actions after environments and secrets exist: 2. merge the generated release PR after CI is green 3. `Release` with `publish-dry-run` 4. `Release` with `publish` -5. `tools/release/release.py verify-release --products-json '' --head-ref HEAD` -6. `tools/release/release.py consumer-shape --require-ready --products-json ''` +5. `tools/dev/bun.sh tools/release/release-verify.mjs --products-json '' --head-ref HEAD` +6. `tools/dev/bun.sh tools/release/release-consumer-shape.mjs --require-ready --products-json ''` Do not treat successful registry setup as full release readiness. The consumer-shape report still has to be green: tracked package metadata, diff --git a/docs/maintainers/release.md b/docs/maintainers/release.md index a59973e5..dbb54a73 100644 --- a/docs/maintainers/release.md +++ b/docs/maintainers/release.md @@ -59,11 +59,11 @@ Use these commands while preparing or checking releases: ```sh tools/dev/bun.sh tools/release/release_plan.mjs tools/dev/bun.sh tools/release/release-check.mjs -tools/release/release.py check-registries +tools/dev/bun.sh tools/release/release-check-registries.mjs tools/release/release.py publish-dry-run tools/release/release.py publish -tools/release/release.py verify-release -tools/release/release.py consumer-shape +tools/dev/bun.sh tools/release/release-verify.mjs +tools/dev/bun.sh tools/release/release-consumer-shape.mjs ``` `consumer-shape` validates tracked package metadata, install docs, SwiftPM, diff --git a/tools/policy/check-release-policy.mjs b/tools/policy/check-release-policy.mjs index d525db22..8c548a66 100644 --- a/tools/policy/check-release-policy.mjs +++ b/tools/policy/check-release-policy.mjs @@ -1022,6 +1022,9 @@ function checkCiPolicy() { const releaseToolPatterns = [ "tools/release/release.py", "tools/release/release-check.mjs", + "tools/release/release-check-registries.mjs", + "tools/release/release-consumer-shape.mjs", + "tools/release/release-verify.mjs", "tools/release/artifact_target_matrix.mjs", ]; const missingMoonSetup = sorted( @@ -1406,13 +1409,13 @@ function checkReleaseWorkflowPolicy() { assertStepContains( publishSteps, "Verify published release", - "tools/release/release.py verify-release --products-json", - "Release workflow must verify published products through the release CLI", + "tools/dev/bun.sh tools/release/release-verify.mjs --products-json", + "Release workflow must verify published products through the Bun release verifier", ); assertContains( - "tools/release/release.py", + "tools/release/release-verify.mjs", "tools/release/verify_github_release_attestations.mjs", - "release.py verify-release must verify GitHub artifact attestations", + "release-verify.mjs must verify GitHub artifact attestations", ); for (const snippet of ["--signer-workflow", ".github/workflows/release.yml", "--source-ref", "refs/heads/main", "--deny-self-hosted-runners"]) { assertContains( diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 7dd1651c..78211480 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -636,6 +636,9 @@ def validate_graph_files() -> None: release_artifact_targets = read_text("tools/release/release-artifact-targets.mjs") sync_release_pr = read_text("tools/release/sync-release-pr.mjs") release_check = read_text("tools/release/release-check.mjs") + release_check_registries = read_text("tools/release/release-check-registries.mjs") + release_consumer_shape = read_text("tools/release/release-consumer-shape.mjs") + release_verify = read_text("tools/release/release-verify.mjs") release_pr_coverage = read_text("tools/release/check_release_pr_coverage.mjs") build_extension_ci_artifacts = read_text("tools/release/build-extension-ci-artifacts.mjs") check_staged_artifacts = read_text("tools/release/check-staged-artifacts.mjs") @@ -705,13 +708,26 @@ def validate_graph_files() -> None: release_moon = read_text("tools/release/moon.yml") if ( '"tools/release/release-check.mjs", *args' not in release_source + or '"tools/release/release-check-registries.mjs", *args' not in release_source + or '"tools/release/release-consumer-shape.mjs", *args' not in release_source + or '"tools/release/release-verify.mjs", *args' not in release_source or "tools/release/check_release_pr_coverage.mjs" not in release_check or "tools/release/check_release_metadata.py" not in release_check - or "tools/release/check_consumer_shape.py" not in release_check + or "tools/release/release-consumer-shape.mjs" not in release_check + or "tools/release/check_release_versions.mjs" not in release_check_registries + or "tools/release/check_registry_publication.mjs" not in release_check_registries + or "tools/release/check_consumer_shape.py" not in release_consumer_shape + or "tools/release/check_release_versions.mjs" not in release_verify + or "tools/release/release-consumer-shape.mjs" not in release_verify + or "tools/release/verify_github_release_attestations.mjs" not in release_verify or "tools/dev/bun.sh tools/release/release-check.mjs" not in release_workflow + or "tools/dev/bun.sh tools/release/release-check-registries.mjs" not in release_workflow + or "tools/dev/bun.sh tools/release/release-consumer-shape.mjs" not in release_workflow + or "tools/dev/bun.sh tools/release/release-verify.mjs" not in release_workflow or "tools/dev/bun.sh tools/release/release-check.mjs" not in release_moon + or "tools/dev/bun.sh tools/release/release-consumer-shape.mjs" not in release_moon ): - fail("release check orchestration must live in the Bun release-check helper while release.py keeps only a compatibility delegator") + fail("active release check, registry-check, verify, and consumer-shape orchestration must live in Bun helpers while release.py keeps compatibility delegators") if ( "publish-step-target-coverage [--product PRODUCT]" not in release_graph_query or "export function publishStepTargetCoverageRows(" not in release_graph_source @@ -906,8 +922,8 @@ def validate_release_setup_docs() -> None: "MAVEN_CENTRAL_USERNAME", "SwiftPM plus GitHub release assets", "oliphaunt-broker", - "consumer-shape --require-ready --products-json ''", - "check-registries --products-json '' --head-ref HEAD", + "tools/dev/bun.sh tools/release/release-consumer-shape.mjs --require-ready --products-json ''", + "tools/dev/bun.sh tools/release/release-check-registries.mjs --products-json '' --head-ref HEAD", "release_commit", "full 40-character SHA that should be published", "The workflow still runs the latest release scripts", diff --git a/tools/release/moon.yml b/tools/release/moon.yml index 34894e4d..386b0891 100644 --- a/tools/release/moon.yml +++ b/tools/release/moon.yml @@ -114,7 +114,7 @@ tasks: runFromWorkspaceRoot: true consumer-shape: tags: ["release", "package"] - command: "tools/release/release.py consumer-shape --format markdown" + command: "tools/dev/bun.sh tools/release/release-consumer-shape.mjs --format markdown" inputs: - "/src/shared/fixtures/consumer-shape/**/*" - "/Cargo.lock" diff --git a/tools/release/release-check-registries.mjs b/tools/release/release-check-registries.mjs new file mode 100644 index 00000000..3042244b --- /dev/null +++ b/tools/release/release-check-registries.mjs @@ -0,0 +1,53 @@ +#!/usr/bin/env bun +import { fail, run } from "./release-cli-utils.mjs"; + +const TOOL = "release-check-registries.mjs"; + +function productsJsonArg(args) { + for (let index = 0; index < args.length; index += 1) { + const value = args[index]; + if (value === "--products-json") { + if (index + 1 >= args.length) { + fail(TOOL, "--products-json requires a value", 2); + } + return args[index + 1]; + } + if (value.startsWith("--products-json=")) { + return value.slice("--products-json=".length); + } + } + return null; +} + +function main(argv) { + if (argv.includes("-h") || argv.includes("--help")) { + console.log("usage: tools/release/release-check-registries.mjs [--products-json JSON] [--head-ref REF] [--require-identities]"); + process.exit(0); + } + + const requireIdentities = argv.includes("--require-identities"); + const passthrough = argv.filter((value) => value !== "--require-identities"); + if (passthrough.length === 0) { + console.log("No release products selected; registry publication checks skipped."); + return; + } + + run(TOOL, ["tools/dev/bun.sh", "tools/release/check_release_versions.mjs", ...passthrough, "--check-registries"], { failExitCode: 2 }); + if (!requireIdentities) { + return; + } + + const productsJson = productsJsonArg(passthrough); + if (productsJson === null) { + fail(TOOL, "check-registries --require-identities requires --products-json", 2); + } + run(TOOL, [ + "tools/dev/bun.sh", + "tools/release/check_registry_publication.mjs", + "--products-json", + productsJson, + "--require-identities", + ], { failExitCode: 2 }); +} + +main(Bun.argv.slice(2)); diff --git a/tools/release/release-check.mjs b/tools/release/release-check.mjs index 2d7185bd..5d3235a2 100644 --- a/tools/release/release-check.mjs +++ b/tools/release/release-check.mjs @@ -1,30 +1,8 @@ #!/usr/bin/env bun -import { spawnSync } from "node:child_process"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; +import { run } from "./release-cli-utils.mjs"; -const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const TOOL = "release-check.mjs"; -function fail(message) { - console.error(`${TOOL}: ${message}`); - process.exit(1); -} - -function run(args) { - console.log(`\n==> ${args.join(" ")}`); - const result = spawnSync(args[0], args.slice(1), { - cwd: ROOT, - stdio: "inherit", - }); - if (result.error) { - fail(`${args[0]} failed to start: ${result.error.message}`); - } - if (result.status !== 0) { - process.exit(result.status ?? 1); - } -} - function parseArgs(argv) { for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; @@ -42,15 +20,16 @@ accepted for compatibility with release.py check and release workflow callers. function main(argv) { parseArgs(argv); - run(["tools/dev/bun.sh", "tools/policy/check-release-policy.mjs"]); - run(["tools/dev/bun.sh", "tools/release/check_release_please_config.mjs"]); - run(["tools/dev/bun.sh", "tools/release/check_artifact_targets.mjs"]); - run(["tools/dev/bun.sh", "tools/release/sync-release-pr.mjs", "--check"]); - run(["tools/dev/bun.sh", "tools/release/check_release_pr_coverage.mjs"]); - run(["python3", "tools/release/check_release_metadata.py"]); - run(["tools/release/check_consumer_shape.py", "--format", "json", "--require-ready"]); - run([ - "tools/release/check_consumer_shape.py", + run(TOOL, ["tools/dev/bun.sh", "tools/policy/check-release-policy.mjs"]); + run(TOOL, ["tools/dev/bun.sh", "tools/release/check_release_please_config.mjs"]); + run(TOOL, ["tools/dev/bun.sh", "tools/release/check_artifact_targets.mjs"]); + run(TOOL, ["tools/dev/bun.sh", "tools/release/sync-release-pr.mjs", "--check"]); + run(TOOL, ["tools/dev/bun.sh", "tools/release/check_release_pr_coverage.mjs"]); + run(TOOL, ["python3", "tools/release/check_release_metadata.py"]); + run(TOOL, ["tools/dev/bun.sh", "tools/release/release-consumer-shape.mjs", "--format", "json", "--require-ready"]); + run(TOOL, [ + "tools/dev/bun.sh", + "tools/release/release-consumer-shape.mjs", "--format", "json", "--require-ready", diff --git a/tools/release/release-cli-utils.mjs b/tools/release/release-cli-utils.mjs new file mode 100644 index 00000000..eab9a223 --- /dev/null +++ b/tools/release/release-cli-utils.mjs @@ -0,0 +1,23 @@ +import { spawnSync } from "node:child_process"; +import path from "node:path"; + +export const ROOT = path.resolve(import.meta.dir, "../.."); + +export function fail(tool, message, exitCode = 1) { + console.error(`${tool}: ${message}`); + process.exit(exitCode); +} + +export function run(tool, args, { failExitCode = 1 } = {}) { + console.log(`\n==> ${args.join(" ")}`); + const result = spawnSync(args[0], args.slice(1), { + cwd: ROOT, + stdio: "inherit", + }); + if (result.error) { + fail(tool, `${args[0]} failed to start: ${result.error.message}`, failExitCode); + } + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} diff --git a/tools/release/release-consumer-shape.mjs b/tools/release/release-consumer-shape.mjs new file mode 100644 index 00000000..959bc0e5 --- /dev/null +++ b/tools/release/release-consumer-shape.mjs @@ -0,0 +1,6 @@ +#!/usr/bin/env bun +import { run } from "./release-cli-utils.mjs"; + +const TOOL = "release-consumer-shape.mjs"; + +run(TOOL, ["tools/release/check_consumer_shape.py", ...Bun.argv.slice(2)]); diff --git a/tools/release/release-verify.mjs b/tools/release/release-verify.mjs new file mode 100644 index 00000000..b222a716 --- /dev/null +++ b/tools/release/release-verify.mjs @@ -0,0 +1,36 @@ +#!/usr/bin/env bun +import { fail, run } from "./release-cli-utils.mjs"; + +const TOOL = "release-verify.mjs"; + +function consumerShapeScopeArgs(args) { + const scoped = []; + for (let index = 0; index < args.length;) { + const value = args[index]; + if (value === "--products-json") { + if (index + 1 >= args.length) { + fail(TOOL, "--products-json requires a value", 2); + } + scoped.push(value, args[index + 1]); + index += 2; + continue; + } + if (value.startsWith("--products-json=")) { + scoped.push(value); + } + index += 1; + } + return scoped; +} + +function main(argv) { + if (argv.includes("-h") || argv.includes("--help")) { + console.log("usage: tools/release/release-verify.mjs [--products-json JSON] [--head-ref REF]"); + process.exit(0); + } + run(TOOL, ["tools/dev/bun.sh", "tools/release/check_release_versions.mjs", ...argv, "--check-registries"], { failExitCode: 2 }); + run(TOOL, ["tools/dev/bun.sh", "tools/release/release-consumer-shape.mjs", "--require-ready", ...consumerShapeScopeArgs(argv)], { failExitCode: 2 }); + run(TOOL, ["tools/dev/bun.sh", "tools/release/verify_github_release_attestations.mjs", ...argv], { failExitCode: 2 }); +} + +main(Bun.argv.slice(2)); diff --git a/tools/release/release.py b/tools/release/release.py index 638a37eb..0ce63834 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -2197,53 +2197,15 @@ def command_check(args: list[str]) -> None: def command_check_registries(args: list[str]) -> None: - require_identities = "--require-identities" in args - args = [value for value in args if value != "--require-identities"] - if not args: - print("No release products selected; registry publication checks skipped.") - return - 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: - fail("check-registries --require-identities requires --products-json") - run( - [ - *REGISTRY_PUBLICATION_CHECK, - "--products-json", - products_json, - "--require-identities", - ] - ) + run(["tools/dev/bun.sh", "tools/release/release-check-registries.mjs", *args]) def command_consumer_shape(args: list[str]) -> None: - result = subprocess.run(["tools/release/check_consumer_shape.py", *args], cwd=ROOT, check=False) - if result.returncode != 0: - raise SystemExit(result.returncode) - - -def consumer_shape_scope_args(args: list[str]) -> list[str]: - scoped: list[str] = [] - index = 0 - while index < len(args): - value = args[index] - if value == "--products-json": - if index + 1 >= len(args): - fail("--products-json requires a value") - scoped.extend([value, args[index + 1]]) - index += 2 - continue - if value.startswith("--products-json="): - scoped.append(value) - index += 1 - return scoped + run(["tools/dev/bun.sh", "tools/release/release-consumer-shape.mjs", *args]) def command_verify_release(args: list[str]) -> None: - 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]) + run(["tools/dev/bun.sh", "tools/release/release-verify.mjs", *args]) def publish_liboliphaunt_github_assets(head_ref: str) -> None: From b0d00c98a6ee1f71e1ff3473b8807e0c3b30b9fb Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 23:45:23 +0000 Subject: [PATCH 236/308] chore: move rust publish source prep to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 9 + src/sdks/rust/tools/check-sdk.sh | 2 +- tools/policy/check-tooling-stack.sh | 7 +- tools/release/check_release_metadata.py | 12 ++ tools/release/prepare-rust-release-source.mjs | 185 ++++++++++++++++++ tools/release/release.py | 12 -- 6 files changed, 212 insertions(+), 15 deletions(-) create mode 100644 tools/release/prepare-rust-release-source.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index f294a644..6cd725b6 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -92,6 +92,15 @@ until the current-state gates here are checked with fresh local evidence. and Moon consumer-shape task now use those helpers directly; `release.py` keeps compatibility delegators for existing local command habits while active CI/release orchestration is no longer routed through Python for these gates. +- 2026-06-27: Moved the Rust SDK generated publish-source preparation command + from `tools/release/release.py prepare-rust-release-source` to the Bun + entrypoint `tools/release/prepare-rust-release-source.mjs`. The Rust SDK + broker Cargo relay check now calls the Bun helper directly, and release + metadata/tooling guards reject reintroducing the removed `release.py` + command surface. Fresh smoke evidence generated + `target/release/cargo-package-sources/oliphaunt/Cargo.toml` with per-target + `liboliphaunt-native-*` and `oliphaunt-broker-*` dependencies plus the + `oliphaunt-tools` facade, and without copying `crates/oliphaunt-build`. - 2026-06-27: Ported the WASIX Cargo artifact packager from `tools/release/package_liboliphaunt_wasix_cargo_artifacts.py` to the Bun entrypoint `tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs`. diff --git a/src/sdks/rust/tools/check-sdk.sh b/src/sdks/rust/tools/check-sdk.sh index a2cfcee5..92b64a4c 100755 --- a/src/sdks/rust/tools/check-sdk.sh +++ b/src/sdks/rust/tools/check-sdk.sh @@ -178,7 +178,7 @@ check_broker_cargo_relay_fixture() { --output-dir "$cargo_artifacts" \ --version "$broker_version" - run python3 tools/release/release.py prepare-rust-release-source + run tools/dev/bun.sh tools/release/prepare-rust-release-source.mjs 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 0cad17d8..8018312b 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -362,8 +362,11 @@ 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 || - fail "Rust SDK check must prepare generated publish source through the release CLI" +grep -Fq 'tools/dev/bun.sh tools/release/prepare-rust-release-source.mjs' src/sdks/rust/tools/check-sdk.sh || + fail "Rust SDK check must prepare generated publish source through the Bun helper" +if grep -Fq '"prepare-rust-release-source"' tools/release/release.py; then + fail "release.py must not retain the Rust SDK prepare-rust-release-source command surface after it moved to Bun" +fi 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 diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 78211480..3c17a667 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -639,6 +639,7 @@ def validate_graph_files() -> None: release_check_registries = read_text("tools/release/release-check-registries.mjs") release_consumer_shape = read_text("tools/release/release-consumer-shape.mjs") release_verify = read_text("tools/release/release-verify.mjs") + prepare_rust_release_source = read_text("tools/release/prepare-rust-release-source.mjs") release_pr_coverage = read_text("tools/release/check_release_pr_coverage.mjs") build_extension_ci_artifacts = read_text("tools/release/build-extension-ci-artifacts.mjs") check_staged_artifacts = read_text("tools/release/check-staged-artifacts.mjs") @@ -706,6 +707,7 @@ def validate_graph_files() -> None: release_source = read_text("tools/release/release.py") release_workflow = read_text(".github/workflows/release.yml") release_moon = read_text("tools/release/moon.yml") + rust_sdk_check = read_text("src/sdks/rust/tools/check-sdk.sh") if ( '"tools/release/release-check.mjs", *args' not in release_source or '"tools/release/release-check-registries.mjs", *args' not in release_source @@ -728,6 +730,16 @@ def validate_graph_files() -> None: or "tools/dev/bun.sh tools/release/release-consumer-shape.mjs" not in release_moon ): fail("active release check, registry-check, verify, and consumer-shape orchestration must live in Bun helpers while release.py keeps compatibility delegators") + if ( + "tools/dev/bun.sh tools/release/prepare-rust-release-source.mjs" not in rust_sdk_check + or '"prepare-rust-release-source"' in release_source + or "renderReleaseCargoToml(" not in prepare_rust_release_source + or "currentProductVersionSync(RUST_PRODUCT" not in prepare_rust_release_source + or "allArtifactTargets({ product, kind, surface, publishedOnly: true }" not in prepare_rust_release_source + or 'registryPackageRows({ product: LIBOLIPHAUNT_NATIVE_PRODUCT, packageKind: "crates" }' not in prepare_rust_release_source + or "oliphaunt-tools, not target tools crates" not in prepare_rust_release_source + ): + fail("Rust SDK generated publish-source preparation must live in the Bun helper instead of the release.py command surface") if ( "publish-step-target-coverage [--product PRODUCT]" not in release_graph_query or "export function publishStepTargetCoverageRows(" not in release_graph_source diff --git a/tools/release/prepare-rust-release-source.mjs b/tools/release/prepare-rust-release-source.mjs new file mode 100644 index 00000000..fecdb11b --- /dev/null +++ b/tools/release/prepare-rust-release-source.mjs @@ -0,0 +1,185 @@ +#!/usr/bin/env bun +import { cpSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { + allArtifactTargets, + compareText, + currentProductVersionSync, + registryPackageRows, +} from "./release-artifact-targets.mjs"; +import { ROOT } from "./release-cli-utils.mjs"; + +const TOOL = "prepare-rust-release-source.mjs"; +const LIBOLIPHAUNT_NATIVE_PRODUCT = "liboliphaunt-native"; +const LIBOLIPHAUNT_TOOLS_PRODUCT = "oliphaunt-tools"; +const BROKER_PRODUCT = "oliphaunt-broker"; +const RUST_PRODUCT = "oliphaunt-rust"; + +function fail(message) { + console.error(`${TOOL}: ${message}`); + process.exit(2); +} + +function rel(file) { + return path.relative(ROOT, file).split(path.sep).join("/"); +} + +function liboliphauntCargoPackageName(targetId, packageBase = LIBOLIPHAUNT_NATIVE_PRODUCT) { + return `${packageBase}-${targetId}`; +} + +function brokerCargoPackageName(targetId) { + return `${BROKER_PRODUCT}-${targetId}`; +} + +function rustArtifactCargoTargetCfg(target) { + 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(`unsupported Cargo target cfg for ${target.id}`); +} + +function packageSection(text) { + const parts = text.split("[package]"); + if (parts.length < 2) { + fail("generated oliphaunt release source is missing [package]"); + } + return parts[1].split("\n[", 1)[0]; +} + +function publishedArtifactTargets({ product, kind, surface }) { + return allArtifactTargets({ product, kind, surface, publishedOnly: true }, TOOL); +} + +function renderReleaseCargoToml(source, nativeVersion, brokerVersion) { + let text = source + .replace("repository.workspace = true", 'repository = "https://github.com/f0rr0/oliphaunt"') + .replace("homepage.workspace = true", 'homepage = "https://oliphaunt.dev"'); + if (!text.includes("[workspace]")) { + text = `${text.trimEnd()}\n\n[workspace]\n`; + } + + const lines = [ + "", + "# Generated for crates.io publishing. Source checkouts keep native runtime", + "# and broker artifact crates out of the local dependency graph until those", + "# artifacts are published and indexed.", + ]; + const targetDependencies = new Map(); + const addTargetDependency = (cfg, dependency) => { + const dependencies = targetDependencies.get(cfg) ?? []; + dependencies.push(dependency); + targetDependencies.set(cfg, dependencies); + }; + + for (const target of publishedArtifactTargets({ + product: LIBOLIPHAUNT_NATIVE_PRODUCT, + kind: "native-runtime", + surface: "rust-native-direct", + })) { + const cfg = rustArtifactCargoTargetCfg(target); + addTargetDependency(cfg, `${liboliphauntCargoPackageName(target.target)} = { version = "=${nativeVersion}" }`); + addTargetDependency(cfg, `${LIBOLIPHAUNT_TOOLS_PRODUCT} = { version = "=${nativeVersion}" }`); + } + for (const target of publishedArtifactTargets({ + product: BROKER_PRODUCT, + kind: "broker-helper", + surface: "rust-broker", + })) { + const cfg = rustArtifactCargoTargetCfg(target); + addTargetDependency(cfg, `${brokerCargoPackageName(target.target)} = { version = "=${brokerVersion}" }`); + } + + for (const cfg of [...targetDependencies.keys()].sort(compareText)) { + lines.push("", `[target.'cfg(${cfg})'.dependencies]`); + lines.push(...targetDependencies.get(cfg).sort(compareText)); + } + return `${text.trimEnd()}\n${lines.join("\n")}\n`; +} + +function validateReleaseArtifactCoverage(manifest, nativeVersion) { + const brokerCrates = registryPackageRows({ product: BROKER_PRODUCT, packageKind: "crates" }, TOOL) + .map((row) => row.packageName); + const missingBroker = brokerCrates.filter((crate) => !manifest.includes(`${crate} = `)); + if (missingBroker.length > 0) { + fail(`generated oliphaunt release source is missing broker Cargo artifact dependencies: ${missingBroker.join(", ")}`); + } + + const nativeTargets = publishedArtifactTargets({ + product: LIBOLIPHAUNT_NATIVE_PRODUCT, + kind: "native-runtime", + surface: "rust-native-direct", + }); + const nativeRuntimeCrates = nativeTargets.map((target) => liboliphauntCargoPackageName(target.target)); + const nativeCrates = registryPackageRows({ product: LIBOLIPHAUNT_NATIVE_PRODUCT, packageKind: "crates" }, TOOL) + .map((row) => row.packageName); + if (nativeCrates.length === 0) { + fail( + "oliphaunt-rust cannot publish a working native Cargo consumer path: " + + "oliphaunt-build requires Cargo-resolved liboliphaunt-native native-runtime " + + `artifacts for ${nativeTargets.map((target) => target.target).join(", ")}, but liboliphaunt-native declares no crates.io ` + + "artifact packages. Split/size native runtime artifacts into crates.io-sized packages before publishing oliphaunt-rust.", + ); + } + + const missingNative = nativeRuntimeCrates.filter( + (crate) => !manifest.includes(`${crate} = { version = "=${nativeVersion}" }`), + ); + if (missingNative.length > 0) { + fail(`generated oliphaunt release source is missing native runtime Cargo artifact dependencies: ${missingNative.join(", ")}`); + } + if (!manifest.includes(`${LIBOLIPHAUNT_TOOLS_PRODUCT} = { version = "=${nativeVersion}" }`)) { + fail(`generated oliphaunt release source is missing native tools facade dependency ${LIBOLIPHAUNT_TOOLS_PRODUCT}`); + } + const directToolDeps = nativeCrates + .filter((crate) => crate.startsWith(`${LIBOLIPHAUNT_TOOLS_PRODUCT}-`) && manifest.includes(`${crate} = `)) + .sort(compareText); + if (directToolDeps.length > 0) { + fail(`generated oliphaunt release source must depend on oliphaunt-tools, not target tools crates: ${directToolDeps.join(", ")}`); + } +} + +function prepareRustReleaseSource() { + const version = currentProductVersionSync(RUST_PRODUCT, TOOL); + const nativeVersion = currentProductVersionSync(LIBOLIPHAUNT_NATIVE_PRODUCT, TOOL); + const brokerVersion = currentProductVersionSync(BROKER_PRODUCT, TOOL); + const sourceDir = path.join(ROOT, "src/sdks/rust"); + const stageDir = path.join(ROOT, "target/release/cargo-package-sources/oliphaunt"); + rmSync(stageDir, { recursive: true, force: true }); + cpSync(sourceDir, stageDir, { + recursive: true, + filter: (source) => path.basename(source) !== "target", + }); + rmSync(path.join(stageDir, "crates/oliphaunt-build"), { recursive: true, force: true }); + + const cargoToml = path.join(stageDir, "Cargo.toml"); + const rendered = renderReleaseCargoToml(readFileSync(cargoToml, "utf8"), nativeVersion, brokerVersion); + writeFileSync(cargoToml, rendered, "utf8"); + if (!packageSection(rendered).includes(`version = "${version}"`)) { + fail(`generated oliphaunt release source must keep SDK version ${version}`); + } + validateReleaseArtifactCoverage(rendered, nativeVersion); + console.log(rel(cargoToml)); +} + +function main(argv) { + if (argv.includes("-h") || argv.includes("--help")) { + console.log("usage: tools/release/prepare-rust-release-source.mjs"); + process.exit(0); + } + if (argv.length > 0) { + fail(`prepare-rust-release-source does not accept extra arguments: ${argv.join(" ")}`); + } + prepareRustReleaseSource(); +} + +main(Bun.argv.slice(2)); diff --git a/tools/release/release.py b/tools/release/release.py index 0ce63834..b523c7a4 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -2102,15 +2102,6 @@ def validate_staged_sdk_package(product: str) -> None: run(["tools/dev/bun.sh", "tools/release/check-staged-artifacts.mjs", "--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") @@ -3669,7 +3660,6 @@ def main(argv: list[str]) -> int: "check", "check-registries", "consumer-shape", - "prepare-rust-release-source", "verify-release", ]: subparsers.add_parser(name, add_help=False) @@ -3694,8 +3684,6 @@ def main(argv: list[str]) -> int: command_check_registries(passthrough) elif command == "consumer-shape": command_consumer_shape(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 3b3e6169fe08437d4dea3b45388a9a8a92293410 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sat, 27 Jun 2026 23:53:49 +0000 Subject: [PATCH 237/308] chore: add bun local registry entrypoint --- docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 10 ++++++++++ docs/maintainers/examples-ci-release-validation.md | 2 +- examples/README.md | 4 ++-- examples/tools/with-local-registries.sh | 2 +- tools/policy/check-tooling-stack.sh | 9 +++++++++ tools/release/check_release_metadata.py | 11 +++++++++++ tools/release/local-registry-publish.mjs | 6 ++++++ 7 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 tools/release/local-registry-publish.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 6cd725b6..f5ca4912 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -101,6 +101,16 @@ until the current-state gates here are checked with fresh local evidence. `target/release/cargo-package-sources/oliphaunt/Cargo.toml` with per-target `liboliphaunt-native-*` and `oliphaunt-broker-*` dependencies plus the `oliphaunt-tools` facade, and without copying `crates/oliphaunt-build`. +- 2026-06-27: Added the Bun user-facing local-registry entrypoint + `tools/release/local-registry-publish.mjs` and moved current example setup + docs plus the missing-registry helper message off direct + `python3 tools/release/local_registry_publish.py` commands. The wrapper keeps + the existing `download`, `status`, and `publish` CLI contract while giving + examples a stable Bun command surface for the eventual full port. Release + metadata and tooling guards now reject drifting example setup back to direct + Python. Fresh smokes passed for `--help`, `status`, + `download --preset local-publish --dry-run`, strict Cargo dry-run publish, + and strict npm dry-run publish through the Bun entrypoint. - 2026-06-27: Ported the WASIX Cargo artifact packager from `tools/release/package_liboliphaunt_wasix_cargo_artifacts.py` to the Bun entrypoint `tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs`. diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index ddbddbbc..c8099e1d 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -127,7 +127,7 @@ the release/tooling surface after the runtime tool crate split. locally. - On 2026-06-27, strict npm local-registry publication was rerun against the current split runtime/tools package surface with - `tools/release/local_registry_publish.py publish --surface npm --strict`. + `tools/dev/bun.sh tools/release/local-registry-publish.mjs publish --surface npm --strict`. The run published/replaced the JS SDK package, native root runtime package, split native tools package, ICU package, broker/node-direct packages, and native extension package/payload families through Verdaccio. Direct generated diff --git a/examples/README.md b/examples/README.md index 2a054313..dc017fb9 100644 --- a/examples/README.md +++ b/examples/README.md @@ -20,7 +20,7 @@ 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 +tools/dev/bun.sh tools/release/local-registry-publish.mjs download --run-id 28049923289 --preset local-publish tools/dev/bun.sh tools/release/package-liboliphaunt-cargo-artifacts.mjs \ --asset-dir target/local-registry-artifacts/liboliphaunt-native-release-assets-linux-x64-gnu \ --output-dir target/local-registry-generated/liboliphaunt-native-cargo \ @@ -33,7 +33,7 @@ tools/dev/bun.sh tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs \ --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 \ +tools/dev/bun.sh tools/release/local-registry-publish.mjs 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 \ diff --git a/examples/tools/with-local-registries.sh b/examples/tools/with-local-registries.sh index 7a128e29..85620966 100755 --- a/examples/tools/with-local-registries.sh +++ b/examples/tools/with-local-registries.sh @@ -12,7 +12,7 @@ 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 + echo "stage it with tools/dev/bun.sh tools/release/local-registry-publish.mjs before running examples" >&2 exit 1 fi diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 8018312b..923e7bb8 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -367,6 +367,15 @@ grep -Fq 'tools/dev/bun.sh tools/release/prepare-rust-release-source.mjs' src/sd if grep -Fq '"prepare-rust-release-source"' tools/release/release.py; then fail "release.py must not retain the Rust SDK prepare-rust-release-source command surface after it moved to Bun" fi +grep -Fq 'tools/dev/bun.sh tools/release/local-registry-publish.mjs download' examples/README.md || + fail "example local-registry setup docs must use the Bun local-registry command" +grep -Fq 'tools/dev/bun.sh tools/release/local-registry-publish.mjs publish' examples/README.md || + fail "example local-registry publish docs must use the Bun local-registry command" +grep -Fq 'tools/dev/bun.sh tools/release/local-registry-publish.mjs publish --surface npm --strict' docs/maintainers/examples-ci-release-validation.md || + fail "maintainer local-registry validation docs must use the Bun local-registry command" +if grep -Fq 'python3 tools/release/local_registry_publish.py' examples/README.md; then + fail "example docs must not expose direct Python local-registry commands" +fi 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 diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 3c17a667..a8f9a060 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -640,6 +640,7 @@ def validate_graph_files() -> None: release_consumer_shape = read_text("tools/release/release-consumer-shape.mjs") release_verify = read_text("tools/release/release-verify.mjs") prepare_rust_release_source = read_text("tools/release/prepare-rust-release-source.mjs") + local_registry_publish = read_text("tools/release/local-registry-publish.mjs") release_pr_coverage = read_text("tools/release/check_release_pr_coverage.mjs") build_extension_ci_artifacts = read_text("tools/release/build-extension-ci-artifacts.mjs") check_staged_artifacts = read_text("tools/release/check-staged-artifacts.mjs") @@ -708,6 +709,8 @@ def validate_graph_files() -> None: release_workflow = read_text(".github/workflows/release.yml") release_moon = read_text("tools/release/moon.yml") rust_sdk_check = read_text("src/sdks/rust/tools/check-sdk.sh") + examples_readme = read_text("examples/README.md") + examples_local_registries = read_text("examples/tools/with-local-registries.sh") if ( '"tools/release/release-check.mjs", *args' not in release_source or '"tools/release/release-check-registries.mjs", *args' not in release_source @@ -740,6 +743,14 @@ def validate_graph_files() -> None: or "oliphaunt-tools, not target tools crates" not in prepare_rust_release_source ): fail("Rust SDK generated publish-source preparation must live in the Bun helper instead of the release.py command surface") + if ( + '["python3", "tools/release/local_registry_publish.py", ...Bun.argv.slice(2)]' not in local_registry_publish + or "tools/dev/bun.sh tools/release/local-registry-publish.mjs download" not in examples_readme + or "tools/dev/bun.sh tools/release/local-registry-publish.mjs publish" not in examples_readme + or "python3 tools/release/local_registry_publish.py" in examples_readme + or "tools/dev/bun.sh tools/release/local-registry-publish.mjs" not in examples_local_registries + ): + fail("example local-registry setup must use the Bun local-registry command surface") if ( "publish-step-target-coverage [--product PRODUCT]" not in release_graph_query or "export function publishStepTargetCoverageRows(" not in release_graph_source diff --git a/tools/release/local-registry-publish.mjs b/tools/release/local-registry-publish.mjs new file mode 100644 index 00000000..2f2669ca --- /dev/null +++ b/tools/release/local-registry-publish.mjs @@ -0,0 +1,6 @@ +#!/usr/bin/env bun +import { run } from "./release-cli-utils.mjs"; + +const TOOL = "local-registry-publish.mjs"; + +run(TOOL, ["python3", "tools/release/local_registry_publish.py", ...Bun.argv.slice(2)]); From 46df66dd95a0539d886885410b1306ce5f27e345 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 00:02:03 +0000 Subject: [PATCH 238/308] chore: port local registry status to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 8 + tools/policy/check-tooling-stack.sh | 2 + tools/release/check_release_metadata.py | 5 +- tools/release/local-registry-publish.mjs | 184 +++++++++++++++++- 4 files changed, 197 insertions(+), 2 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index f5ca4912..e3257bac 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -111,6 +111,14 @@ until the current-state gates here are checked with fresh local evidence. Python. Fresh smokes passed for `--help`, `status`, `download --preset local-publish --dry-run`, strict Cargo dry-run publish, and strict npm dry-run publish through the Bun entrypoint. +- 2026-06-27: Ported the local-registry `status` subcommand into + `tools/release/local-registry-publish.mjs`. The Bun implementation now + discovers the same default and explicit artifact roots, lists Cargo/npm/Maven + and Swift artifacts, and reports tool availability without invoking Python; + only `download` and `publish` still fall back to the Python backend. Fresh + parity checks diffed Bun `status` output byte-for-byte against + `tools/release/local_registry_publish.py status` for default roots and + `--artifact-root target/sdk-artifacts`. - 2026-06-27: Ported the WASIX Cargo artifact packager from `tools/release/package_liboliphaunt_wasix_cargo_artifacts.py` to the Bun entrypoint `tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs`. diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 923e7bb8..f3b75720 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -373,6 +373,8 @@ grep -Fq 'tools/dev/bun.sh tools/release/local-registry-publish.mjs publish' exa fail "example local-registry publish docs must use the Bun local-registry command" grep -Fq 'tools/dev/bun.sh tools/release/local-registry-publish.mjs publish --surface npm --strict' docs/maintainers/examples-ci-release-validation.md || fail "maintainer local-registry validation docs must use the Bun local-registry command" +grep -Fq 'if (command === "status")' tools/release/local-registry-publish.mjs || + fail "local-registry status must run in the Bun entrypoint, not through the Python fallback" if grep -Fq 'python3 tools/release/local_registry_publish.py' examples/README.md; then fail "example docs must not expose direct Python local-registry commands" fi diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index a8f9a060..1a121a5b 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -744,7 +744,10 @@ def validate_graph_files() -> None: ): fail("Rust SDK generated publish-source preparation must live in the Bun helper instead of the release.py command surface") if ( - '["python3", "tools/release/local_registry_publish.py", ...Bun.argv.slice(2)]' not in local_registry_publish + 'if (command === "status")' not in local_registry_publish + or "function status(argv)" not in local_registry_publish + or "function discoverRoots(" not in local_registry_publish + or '["python3", "tools/release/local_registry_publish.py", ...Bun.argv.slice(2)]' not in local_registry_publish or "tools/dev/bun.sh tools/release/local-registry-publish.mjs download" not in examples_readme or "tools/dev/bun.sh tools/release/local-registry-publish.mjs publish" not in examples_readme or "python3 tools/release/local_registry_publish.py" in examples_readme diff --git a/tools/release/local-registry-publish.mjs b/tools/release/local-registry-publish.mjs index 2f2669ca..5cb95669 100644 --- a/tools/release/local-registry-publish.mjs +++ b/tools/release/local-registry-publish.mjs @@ -1,6 +1,188 @@ #!/usr/bin/env bun +import { accessSync, constants, existsSync, readdirSync, statSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; import { run } from "./release-cli-utils.mjs"; +import { ROOT } from "./release-cli-utils.mjs"; const TOOL = "local-registry-publish.mjs"; +const DEFAULT_RUN_ID = "28049923289"; +const DEFAULT_CURRENT_ARTIFACT_ROOT = path.join(ROOT, "target/local-registry-current"); +const DEFAULT_ARTIFACT_ROOT = path.join(ROOT, "target/local-registry-artifacts"); +const DEFAULT_ROOTS = [ + DEFAULT_CURRENT_ARTIFACT_ROOT, + DEFAULT_ARTIFACT_ROOT, + path.join(ROOT, "target/sdk-artifacts"), + path.join(ROOT, "target/package/tmp-crate"), + path.join(ROOT, "target/package/tmp-registry"), + path.join(ROOT, "target/local-registry-generated/broker-cargo"), + path.join(ROOT, "target/oliphaunt-broker/cargo-artifacts"), + path.join(ROOT, "target/extension-artifacts"), +]; -run(TOOL, ["python3", "tools/release/local_registry_publish.py", ...Bun.argv.slice(2)]); +function rel(file) { + const relative = path.relative(ROOT, file); + return relative && !relative.startsWith("..") && !path.isAbsolute(relative) + ? relative.split(path.sep).join("/") + : file.split(path.sep).join("/"); +} + +function compareText(left, right) { + return left < right ? -1 : left > right ? 1 : 0; +} + +function executableExists(name) { + const pathEnv = process.env.PATH ?? ""; + const extensions = os.platform() === "win32" + ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";") + : [""]; + for (const directory of pathEnv.split(path.delimiter)) { + if (!directory) { + continue; + } + for (const extension of extensions) { + const candidate = path.join(directory, os.platform() === "win32" && !name.includes(".") ? `${name}${extension}` : name); + try { + accessSync(candidate, constants.X_OK); + return true; + } catch { + // Keep searching. + } + } + } + return false; +} + +function walkFiles(root) { + const files = []; + const visit = (current) => { + const entries = readdirSync(current, { withFileTypes: true }) + .sort((left, right) => compareText(left.name, right.name)); + for (const entry of entries) { + const entryPath = path.join(current, entry.name); + if (entry.isDirectory()) { + visit(entryPath); + } else if (entry.isFile()) { + files.push(entryPath); + } + } + }; + visit(root); + return files; +} + +function walkDirsNamed(root, name) { + const dirs = []; + const visit = (current) => { + const entries = readdirSync(current, { withFileTypes: true }) + .sort((left, right) => compareText(left.name, right.name)); + for (const entry of entries) { + const entryPath = path.join(current, entry.name); + if (!entry.isDirectory()) { + continue; + } + if (entry.name === name) { + dirs.push(entryPath); + } + visit(entryPath); + } + }; + visit(root); + return dirs; +} + +function discoverRoots(artifactRoots) { + const roots = artifactRoots.length > 0 ? artifactRoots : DEFAULT_ROOTS; + const seen = new Set(); + const result = []; + for (const root of roots) { + const resolved = path.resolve(ROOT, root); + if (seen.has(resolved) || !existsSync(resolved)) { + continue; + } + seen.add(resolved); + result.push(resolved); + } + return result; +} + +function discoverFiles(roots, suffixes) { + const files = new Set(); + for (const root of roots) { + const stats = statSync(root); + if (stats.isFile() && suffixes.some((suffix) => path.basename(root).endsWith(suffix))) { + files.add(root); + continue; + } + if (stats.isDirectory()) { + for (const file of walkFiles(root)) { + if (suffixes.some((suffix) => path.basename(file).endsWith(suffix))) { + files.add(file); + } + } + } + } + return [...files].sort(compareText); +} + +function parseStatusArgs(argv) { + const artifactRoots = []; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === "--artifact-root") { + if (index + 1 >= argv.length) { + console.error(`${TOOL}: --artifact-root requires a value`); + process.exit(2); + } + artifactRoots.push(argv[index + 1]); + index += 1; + continue; + } + if (value.startsWith("--artifact-root=")) { + artifactRoots.push(value.slice("--artifact-root=".length)); + continue; + } + if (value === "-h" || value === "--help") { + run(TOOL, ["python3", "tools/release/local_registry_publish.py", "status", ...argv]); + process.exit(0); + } + console.error(`${TOOL}: unknown status argument ${value}`); + process.exit(2); + } + return { artifactRoots }; +} + +function status(argv) { + const { artifactRoots } = parseStatusArgs(argv); + const roots = discoverRoots(artifactRoots); + const report = { + artifact_roots: roots.map((root) => root), + artifacts: { + cargo: discoverFiles(roots, [".crate"]).map(rel), + maven_roots: roots + .filter((root) => statSync(root).isDirectory()) + .flatMap((root) => walkDirsNamed(root, "maven").map(rel)), + npm: discoverFiles(roots, [".tgz"]).map(rel), + swift: discoverFiles(roots, [".swift", ".zip"]) + .filter((file) => path.basename(file) === "Package.swift.release" || file.includes("swift")) + .map(rel), + }, + default_run_id: DEFAULT_RUN_ID, + tools: { + cargo: executableExists("cargo"), + gh: executableExists("gh"), + java: executableExists("java"), + npm: executableExists("npm"), + pnpm: executableExists("pnpm"), + swift: executableExists("swift"), + }, + }; + console.log(JSON.stringify(report, null, 2)); +} + +const [command, ...args] = Bun.argv.slice(2); +if (command === "status") { + status(args); +} else { + run(TOOL, ["python3", "tools/release/local_registry_publish.py", ...Bun.argv.slice(2)]); +} From 513ccd077e4996cd7b910f2c6be9eb6b832443de Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 00:29:38 +0000 Subject: [PATCH 239/308] chore: port local registry download to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 12 +- tools/policy/check-tooling-stack.sh | 2 + tools/release/check_release_metadata.py | 3 + tools/release/local-registry-publish.mjs | 221 +++++++++++++++++- 4 files changed, 232 insertions(+), 6 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index e3257bac..bca009f2 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -115,10 +115,18 @@ until the current-state gates here are checked with fresh local evidence. `tools/release/local-registry-publish.mjs`. The Bun implementation now discovers the same default and explicit artifact roots, lists Cargo/npm/Maven and Swift artifacts, and reports tool availability without invoking Python; - only `download` and `publish` still fall back to the Python backend. Fresh - parity checks diffed Bun `status` output byte-for-byte against + at that point, `download` and `publish` still fell back to the Python + backend. Fresh parity checks diffed Bun `status` output byte-for-byte against `tools/release/local_registry_publish.py status` for default roots and `--artifact-root target/sdk-artifacts`. +- 2026-06-28: Ported the local-registry `download` subcommand into + `tools/release/local-registry-publish.mjs`. The Bun implementation now uses + the shared Bun local-publish artifact metadata, queries GitHub Actions + artifact metadata through `gh api`, preserves dry-run output, and downloads + selected artifacts with `gh run download`; only `publish` still falls back to + the Python backend. Fresh parity checks diffed Bun and Python dry-run output + for `--preset local-publish` and a single explicit artifact, and a disposable + real download smoke fetched `oliphaunt-wasix-rust-package-artifacts`. - 2026-06-27: Ported the WASIX Cargo artifact packager from `tools/release/package_liboliphaunt_wasix_cargo_artifacts.py` to the Bun entrypoint `tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs`. diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index f3b75720..a2bbad4c 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -375,6 +375,8 @@ grep -Fq 'tools/dev/bun.sh tools/release/local-registry-publish.mjs publish --su fail "maintainer local-registry validation docs must use the Bun local-registry command" grep -Fq 'if (command === "status")' tools/release/local-registry-publish.mjs || fail "local-registry status must run in the Bun entrypoint, not through the Python fallback" +grep -Fq 'if (command === "download")' tools/release/local-registry-publish.mjs || + fail "local-registry download must run in the Bun entrypoint, not through the Python fallback" if grep -Fq 'python3 tools/release/local_registry_publish.py' examples/README.md; then fail "example docs must not expose direct Python local-registry commands" fi diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 1a121a5b..25c86416 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -745,8 +745,11 @@ def validate_graph_files() -> None: fail("Rust SDK generated publish-source preparation must live in the Bun helper instead of the release.py command surface") if ( 'if (command === "status")' not in local_registry_publish + or 'if (command === "download")' not in local_registry_publish or "function status(argv)" not in local_registry_publish + or "function download(argv)" not in local_registry_publish or "function discoverRoots(" not in local_registry_publish + or "tools/release/local_registry_metadata.mjs" not in local_registry_publish or '["python3", "tools/release/local_registry_publish.py", ...Bun.argv.slice(2)]' not in local_registry_publish or "tools/dev/bun.sh tools/release/local-registry-publish.mjs download" not in examples_readme or "tools/dev/bun.sh tools/release/local-registry-publish.mjs publish" not in examples_readme diff --git a/tools/release/local-registry-publish.mjs b/tools/release/local-registry-publish.mjs index 5cb95669..5d1a7e22 100644 --- a/tools/release/local-registry-publish.mjs +++ b/tools/release/local-registry-publish.mjs @@ -1,12 +1,13 @@ #!/usr/bin/env bun -import { accessSync, constants, existsSync, readdirSync, statSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { accessSync, constants, existsSync, mkdirSync, readdirSync, rmSync, statSync } from "node:fs"; import os from "node:os"; import path from "node:path"; -import { run } from "./release-cli-utils.mjs"; -import { ROOT } from "./release-cli-utils.mjs"; +import { fail, ROOT, run } from "./release-cli-utils.mjs"; const TOOL = "local-registry-publish.mjs"; const DEFAULT_RUN_ID = "28049923289"; +const DEFAULT_REPO = "f0rr0/oliphaunt"; const DEFAULT_CURRENT_ARTIFACT_ROOT = path.join(ROOT, "target/local-registry-current"); const DEFAULT_ARTIFACT_ROOT = path.join(ROOT, "target/local-registry-artifacts"); const DEFAULT_ROOTS = [ @@ -31,6 +32,44 @@ function compareText(left, right) { return left < right ? -1 : left > right ? 1 : 0; } +function commandOutput(args) { + const result = spawnSync(args[0], args.slice(1), { + cwd: ROOT, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.error) { + fail(TOOL, `${args[0]} failed to start: ${result.error.message}`); + } + if (result.status !== 0) { + const detail = (result.stderr || result.stdout || "").trim(); + fail(TOOL, detail || `${args.join(" ")} failed with exit code ${result.status}`, result.status ?? 1); + } + return result.stdout; +} + +function runQuiet(args) { + const result = spawnSync(args[0], args.slice(1), { + cwd: ROOT, + stdio: "inherit", + }); + if (result.error) { + fail(TOOL, `${args[0]} failed to start: ${result.error.message}`); + } + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + +function commandJson(args, label) { + const output = commandOutput(args); + try { + return JSON.parse(output); + } catch (error) { + fail(TOOL, `${label} did not return valid JSON: ${error.message}`); + } +} + function executableExists(name) { const pathEnv = process.env.PATH ?? ""; const extensions = os.platform() === "win32" @@ -53,6 +92,12 @@ function executableExists(name) { return false; } +function requireCommand(name) { + if (!executableExists(name)) { + fail(TOOL, `missing required command: ${name}`); + } +} + function walkFiles(root) { const files = []; const visit = (current) => { @@ -125,6 +170,172 @@ function discoverFiles(roots, suffixes) { return [...files].sort(compareText); } +function localPublishArtifacts() { + const names = commandJson([ + "tools/dev/bun.sh", + "tools/release/local_registry_metadata.mjs", + "local-publish-artifacts", + ], "local registry metadata local-publish-artifacts"); + if (!Array.isArray(names) || names.some((name) => typeof name !== "string" || name.length === 0)) { + fail(TOOL, "local registry metadata local-publish-artifacts must return a non-empty string list"); + } + if (names.length === 0) { + fail(TOOL, "local registry metadata returned no local-publish artifacts"); + } + const duplicates = [...new Set(names.filter((name, index) => names.indexOf(name) !== index))].sort(compareText); + if (duplicates.length > 0) { + fail(TOOL, `local registry metadata returned duplicate local-publish artifacts: ${duplicates.join(", ")}`); + } + return names; +} + +function listCiArtifacts(repo, runId) { + requireCommand("gh"); + const data = commandJson([ + "gh", + "api", + `repos/${repo}/actions/runs/${runId}/artifacts?per_page=100`, + "--paginate", + ], `GitHub Actions artifacts for ${repo} run ${runId}`); + if (Array.isArray(data)) { + return data.flatMap((page) => Array.isArray(page?.artifacts) ? page.artifacts : []); + } + return Array.isArray(data?.artifacts) ? data.artifacts : []; +} + +function parseDownloadArgs(argv) { + const options = { + repo: DEFAULT_REPO, + runId: DEFAULT_RUN_ID, + destination: DEFAULT_ARTIFACT_ROOT, + artifacts: [], + preset: null, + force: false, + dryRun: false, + }; + const readValue = (index, flag) => { + if (index + 1 >= argv.length) { + fail(TOOL, `${flag} requires a value`, 2); + } + return argv[index + 1]; + }; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === "-h" || value === "--help") { + console.log(`usage: tools/release/local-registry-publish.mjs download [--repo REPO] [--run-id RUN_ID] [--destination DIR] [--artifact NAME] [--preset local-publish] [--force] [--dry-run]`); + process.exit(0); + } + if (value === "--repo") { + options.repo = readValue(index, value); + index += 1; + continue; + } + if (value.startsWith("--repo=")) { + options.repo = value.slice("--repo=".length); + continue; + } + if (value === "--run-id") { + options.runId = readValue(index, value); + index += 1; + continue; + } + if (value.startsWith("--run-id=")) { + options.runId = value.slice("--run-id=".length); + continue; + } + if (value === "--destination") { + options.destination = path.resolve(ROOT, readValue(index, value)); + index += 1; + continue; + } + if (value.startsWith("--destination=")) { + options.destination = path.resolve(ROOT, value.slice("--destination=".length)); + continue; + } + if (value === "--artifact") { + options.artifacts.push(readValue(index, value)); + index += 1; + continue; + } + if (value.startsWith("--artifact=")) { + options.artifacts.push(value.slice("--artifact=".length)); + continue; + } + if (value === "--preset") { + options.preset = readValue(index, value); + index += 1; + continue; + } + if (value.startsWith("--preset=")) { + options.preset = value.slice("--preset=".length); + continue; + } + if (value === "--force") { + options.force = true; + continue; + } + if (value === "--dry-run") { + options.dryRun = true; + continue; + } + fail(TOOL, `unknown download argument ${value}`, 2); + } + if (options.preset !== null && options.preset !== "local-publish") { + fail(TOOL, `download --preset must be local-publish, got ${options.preset}`, 2); + } + return options; +} + +function download(argv) { + const options = parseDownloadArgs(argv); + const selectedArtifacts = [ + ...options.artifacts, + ...(options.preset === "local-publish" ? localPublishArtifacts() : []), + ]; + const artifacts = [...new Set(selectedArtifacts)].sort(compareText); + if (artifacts.length === 0) { + console.error("No artifacts selected; pass --artifact or --preset local-publish."); + process.exit(2); + } + + const available = new Map(listCiArtifacts(options.repo, options.runId).map((artifact) => [artifact.name, artifact])); + const missing = artifacts.filter((artifact) => !available.has(artifact)); + if (missing.length > 0) { + console.error(`Run ${options.runId} is missing artifacts: ${missing.join(", ")}`); + process.exit(1); + } + if (options.dryRun) { + for (const artifact of artifacts) { + console.log(`${artifact}\t${available.get(artifact).size_in_bytes ?? 0}`); + } + return; + } + + mkdirSync(options.destination, { recursive: true }); + for (const artifact of artifacts) { + const artifactDir = path.join(options.destination, artifact); + if (existsSync(artifactDir) && readdirSync(artifactDir).length > 0 && !options.force) { + console.log(`Skipping existing ${rel(artifactDir)}`); + continue; + } + rmSync(artifactDir, { recursive: true, force: true }); + mkdirSync(artifactDir, { recursive: true }); + console.log(`Downloading ${artifact} from ${options.repo} run ${options.runId}`); + runQuiet([ + "gh", + "run", + "download", + options.runId, + "--repo", + options.repo, + "--name", + artifact, + "--dir", + artifactDir, + ]); + } +} + function parseStatusArgs(argv) { const artifactRoots = []; for (let index = 0; index < argv.length; index += 1) { @@ -181,7 +392,9 @@ function status(argv) { } const [command, ...args] = Bun.argv.slice(2); -if (command === "status") { +if (command === "download") { + download(args); +} else if (command === "status") { status(args); } else { run(TOOL, ["python3", "tools/release/local_registry_publish.py", ...Bun.argv.slice(2)]); From 4d3b148c73e92db31971b5630478f52846f56711 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 00:38:14 +0000 Subject: [PATCH 240/308] chore: port local registry maven and swift publish to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 8 + tools/policy/check-tooling-stack.sh | 4 + tools/release/check_release_metadata.py | 4 + tools/release/local-registry-publish.mjs | 210 +++++++++++++++++- 4 files changed, 225 insertions(+), 1 deletion(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index bca009f2..3728dcd8 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -127,6 +127,14 @@ until the current-state gates here are checked with fresh local evidence. the Python backend. Fresh parity checks diffed Bun and Python dry-run output for `--preset local-publish` and a single explicit artifact, and a disposable real download smoke fetched `oliphaunt-wasix-rust-package-artifacts`. +- 2026-06-28: Ported the low-risk local-registry `publish --surface maven` and + `publish --surface swift` paths into `tools/release/local-registry-publish.mjs`. + Explicit Maven/Swift publishes now preserve the Python JSON report shape, + dry-run messages, strict missing-artifact behavior, `report.json` writes, and + copy/stage behavior in Bun. Mixed, Cargo, npm, and all-surface publishes still + fall back to the Python backend until their generation/indexing logic is + ported with equivalent coverage. Fresh parity checks diffed Bun and Python + dry-run output byte-for-byte for Maven, Swift, and combined Maven+Swift. - 2026-06-27: Ported the WASIX Cargo artifact packager from `tools/release/package_liboliphaunt_wasix_cargo_artifacts.py` to the Bun entrypoint `tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs`. diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index a2bbad4c..1c8154a9 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -377,6 +377,10 @@ grep -Fq 'if (command === "status")' tools/release/local-registry-publish.mjs || fail "local-registry status must run in the Bun entrypoint, not through the Python fallback" grep -Fq 'if (command === "download")' tools/release/local-registry-publish.mjs || fail "local-registry download must run in the Bun entrypoint, not through the Python fallback" +grep -Fq 'function publishMaven(' tools/release/local-registry-publish.mjs || + fail "local-registry Maven publish surface must run in the Bun entrypoint" +grep -Fq 'function publishSwift(' tools/release/local-registry-publish.mjs || + fail "local-registry Swift publish surface must run in the Bun entrypoint" if grep -Fq 'python3 tools/release/local_registry_publish.py' examples/README.md; then fail "example docs must not expose direct Python local-registry commands" fi diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 25c86416..e749c80f 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -746,8 +746,12 @@ def validate_graph_files() -> None: if ( 'if (command === "status")' not in local_registry_publish or 'if (command === "download")' not in local_registry_publish + or 'if (command === "publish")' not in local_registry_publish or "function status(argv)" not in local_registry_publish or "function download(argv)" not in local_registry_publish + or "function publishMaven(" not in local_registry_publish + or "function publishSwift(" not in local_registry_publish + or "function canPublishInBun(" not in local_registry_publish or "function discoverRoots(" not in local_registry_publish or "tools/release/local_registry_metadata.mjs" not in local_registry_publish or '["python3", "tools/release/local_registry_publish.py", ...Bun.argv.slice(2)]' not in local_registry_publish diff --git a/tools/release/local-registry-publish.mjs b/tools/release/local-registry-publish.mjs index 5d1a7e22..56c96eff 100644 --- a/tools/release/local-registry-publish.mjs +++ b/tools/release/local-registry-publish.mjs @@ -1,6 +1,16 @@ #!/usr/bin/env bun import { spawnSync } from "node:child_process"; -import { accessSync, constants, existsSync, mkdirSync, readdirSync, rmSync, statSync } from "node:fs"; +import { + accessSync, + constants, + copyFileSync, + existsSync, + mkdirSync, + readdirSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; import os from "node:os"; import path from "node:path"; import { fail, ROOT, run } from "./release-cli-utils.mjs"; @@ -170,6 +180,18 @@ function discoverFiles(roots, suffixes) { return [...files].sort(compareText); } +function copyTreeContents(source, destination) { + let copied = 0; + for (const file of walkFiles(source)) { + const relative = path.relative(source, file); + const target = path.join(destination, relative); + mkdirSync(path.dirname(target), { recursive: true }); + copyFileSync(file, target); + copied += 1; + } + return copied; +} + function localPublishArtifacts() { const names = commandJson([ "tools/dev/bun.sh", @@ -336,6 +358,190 @@ function download(argv) { } } +function surfaceResult(surface) { + return { + surface, + published: [], + staged: [], + skipped: [], + }; +} + +function reportSurfaceResult(result) { + return { + published: result.published, + skipped: result.skipped, + staged: result.staged, + surface: result.surface, + }; +} + +function addSkip(result, message, strict) { + result.skipped.push(message); + if (strict) { + fail(TOOL, message); + } +} + +function publishMaven(roots, registryRoot, dryRun, strict) { + const result = surfaceResult("maven"); + const candidates = roots + .filter((root) => statSync(root).isDirectory()) + .flatMap((root) => walkDirsNamed(root, "maven")) + .sort(compareText); + if (candidates.length === 0) { + addSkip(result, "no staged Maven repository directories named maven found", strict); + return result; + } + const mavenRoot = path.join(registryRoot, "maven"); + if (dryRun) { + result.published.push(...candidates.map((candidate) => `dry-run maven copy ${rel(candidate)}`)); + return result; + } + rmSync(mavenRoot, { recursive: true, force: true }); + mkdirSync(mavenRoot, { recursive: true }); + for (const candidate of candidates) { + const count = copyTreeContents(candidate, mavenRoot); + result.published.push(`${rel(candidate)} (${count} files)`); + } + result.staged.push(rel(mavenRoot)); + return result; +} + +function publishSwift(roots, registryRoot, dryRun, strict) { + const result = surfaceResult("swift"); + const swiftFiles = discoverFiles(roots, [".swift", ".zip"]) + .filter((file) => path.basename(file) === "Package.swift.release" || path.basename(file).endsWith("-source.zip") || file.includes("swift")); + if (swiftFiles.length === 0) { + addSkip(result, "no SwiftPM package artifacts found", strict); + return result; + } + if (!executableExists("swift")) { + result.skipped.push("swift is not installed; staged artifacts are copyable, registry publish skipped on this Linux host"); + } + const swiftRoot = path.join(registryRoot, "swift"); + if (dryRun) { + result.published.push(...swiftFiles.map((file) => `dry-run swift stage ${rel(file)}`)); + return result; + } + rmSync(swiftRoot, { recursive: true, force: true }); + mkdirSync(swiftRoot, { recursive: true }); + for (const file of swiftFiles) { + const target = path.join(swiftRoot, path.basename(file)); + copyFileSync(file, target); + result.staged.push(rel(target)); + } + return result; +} + +function parsePublishArgs(argv) { + const options = { + artifactRoots: [], + registryRoot: path.join(ROOT, "target/local-registries"), + surfaces: [], + verdaccioPort: "4873", + dryRun: false, + strict: false, + help: false, + }; + const readValue = (index, flag) => { + if (index + 1 >= argv.length) { + fail(TOOL, `${flag} requires a value`, 2); + } + return argv[index + 1]; + }; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === "-h" || value === "--help") { + options.help = true; + continue; + } + if (value === "--artifact-root") { + options.artifactRoots.push(readValue(index, value)); + index += 1; + continue; + } + if (value.startsWith("--artifact-root=")) { + options.artifactRoots.push(value.slice("--artifact-root=".length)); + continue; + } + if (value === "--registry-root") { + options.registryRoot = path.resolve(ROOT, readValue(index, value)); + index += 1; + continue; + } + if (value.startsWith("--registry-root=")) { + options.registryRoot = path.resolve(ROOT, value.slice("--registry-root=".length)); + continue; + } + if (value === "--surface") { + options.surfaces.push(readValue(index, value)); + index += 1; + continue; + } + if (value.startsWith("--surface=")) { + options.surfaces.push(value.slice("--surface=".length)); + continue; + } + if (value === "--verdaccio-port") { + options.verdaccioPort = readValue(index, value); + index += 1; + continue; + } + if (value.startsWith("--verdaccio-port=")) { + options.verdaccioPort = value.slice("--verdaccio-port=".length); + continue; + } + if (value === "--dry-run") { + options.dryRun = true; + continue; + } + if (value === "--strict") { + options.strict = true; + continue; + } + fail(TOOL, `unknown publish argument ${value}`, 2); + } + const invalidSurfaces = options.surfaces.filter((surface) => !["npm", "cargo", "maven", "swift"].includes(surface)); + if (invalidSurfaces.length > 0) { + fail(TOOL, `unsupported publish surface: ${invalidSurfaces[0]}`, 2); + } + return options; +} + +function canPublishInBun(options) { + return !options.help && options.surfaces.length > 0 && options.surfaces.every((surface) => surface === "maven" || surface === "swift"); +} + +function publish(argv) { + const options = parsePublishArgs(argv); + if (!canPublishInBun(options)) { + run(TOOL, ["python3", "tools/release/local_registry_publish.py", "publish", ...argv]); + return; + } + const roots = discoverRoots(options.artifactRoots); + mkdirSync(options.registryRoot, { recursive: true }); + const results = []; + for (const surface of options.surfaces) { + if (surface === "maven") { + results.push(publishMaven(roots, options.registryRoot, options.dryRun, options.strict)); + } else if (surface === "swift") { + results.push(publishSwift(roots, options.registryRoot, options.dryRun, options.strict)); + } + } + const report = { + artifact_roots: roots, + dry_run: options.dryRun, + registry_root: options.registryRoot, + surfaces: results.map(reportSurfaceResult), + }; + const text = `${JSON.stringify(report, null, 2)}\n`; + if (!options.dryRun) { + writeFileSync(path.join(options.registryRoot, "report.json"), text); + } + process.stdout.write(text); +} + function parseStatusArgs(argv) { const artifactRoots = []; for (let index = 0; index < argv.length; index += 1) { @@ -394,6 +600,8 @@ function status(argv) { const [command, ...args] = Bun.argv.slice(2); if (command === "download") { download(args); +} else if (command === "publish") { + publish(args); } else if (command === "status") { status(args); } else { From aa714466ce057fa0e7c53cd6bd8c87fdc5b7bf08 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 00:46:06 +0000 Subject: [PATCH 241/308] chore: port local registry cargo dry run to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 8 +++ tools/policy/check-tooling-stack.sh | 2 + tools/release/check_release_metadata.py | 2 + tools/release/local-registry-publish.mjs | 68 ++++++++++++++++++- 4 files changed, 78 insertions(+), 2 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 3728dcd8..8c75c43f 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -135,6 +135,14 @@ until the current-state gates here are checked with fresh local evidence. fall back to the Python backend until their generation/indexing logic is ported with equivalent coverage. Fresh parity checks diffed Bun and Python dry-run output byte-for-byte for Maven, Swift, and combined Maven+Swift. +- 2026-06-28: Ported `publish --surface cargo --dry-run` into + `tools/release/local-registry-publish.mjs`. The Bun implementation preserves + the Python dry-run report shape, release-asset/source/native-extension staging + messages, extension manifest discovery, strict no-crate failure, and sorted + local `.crate` listing. Real Cargo publishing still falls back to Python until + the source-crate generation and file-backed Cargo index writer are ported. + Fresh parity checks diffed Bun and Python output byte-for-byte for strict + Cargo dry-run and combined strict Cargo+Maven+Swift dry-run. - 2026-06-27: Ported the WASIX Cargo artifact packager from `tools/release/package_liboliphaunt_wasix_cargo_artifacts.py` to the Bun entrypoint `tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs`. diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 1c8154a9..09da9dd6 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -381,6 +381,8 @@ grep -Fq 'function publishMaven(' tools/release/local-registry-publish.mjs || fail "local-registry Maven publish surface must run in the Bun entrypoint" grep -Fq 'function publishSwift(' tools/release/local-registry-publish.mjs || fail "local-registry Swift publish surface must run in the Bun entrypoint" +grep -Fq 'function publishCargoDryRun(' tools/release/local-registry-publish.mjs || + fail "local-registry Cargo dry-run publish surface must run in the Bun entrypoint" if grep -Fq 'python3 tools/release/local_registry_publish.py' examples/README.md; then fail "example docs must not expose direct Python local-registry commands" fi diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index e749c80f..07322e04 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -749,6 +749,8 @@ def validate_graph_files() -> None: or 'if (command === "publish")' not in local_registry_publish or "function status(argv)" not in local_registry_publish or "function download(argv)" not in local_registry_publish + or "function publishCargoDryRun(" not in local_registry_publish + or "function discoverExtensionManifests(" not in local_registry_publish or "function publishMaven(" not in local_registry_publish or "function publishSwift(" not in local_registry_publish or "function canPublishInBun(" not in local_registry_publish diff --git a/tools/release/local-registry-publish.mjs b/tools/release/local-registry-publish.mjs index 56c96eff..b9eb87b8 100644 --- a/tools/release/local-registry-publish.mjs +++ b/tools/release/local-registry-publish.mjs @@ -211,6 +211,25 @@ function localPublishArtifacts() { return names; } +function discoverExtensionManifests(roots) { + if (roots.length === 0) { + return []; + } + const args = [ + "tools/dev/bun.sh", + "tools/release/local_registry_metadata.mjs", + "discover-extension-manifests", + ]; + for (const root of roots) { + args.push("--root", root); + } + const values = commandJson(args, "local registry metadata discover-extension-manifests"); + if (!Array.isArray(values) || values.some((value) => typeof value !== "string" || value.length === 0)) { + fail(TOOL, "local registry metadata discover-extension-manifests must return a string list"); + } + return values.map((value) => path.resolve(ROOT, value)); +} + function listCiArtifacts(repo, runId) { requireCommand("gh"); const data = commandJson([ @@ -434,6 +453,47 @@ function publishSwift(roots, registryRoot, dryRun, strict) { return result; } +function hostCargoReleaseTarget() { + const arch = os.arch(); + const platform = os.platform(); + if (platform === "linux" && arch === "x64") { + return "linux-x64-gnu"; + } + if (platform === "linux" && arch === "arm64") { + return "linux-arm64-gnu"; + } + if (platform === "darwin" && arch === "arm64") { + return "macos-arm64"; + } + if (platform === "win32" && arch === "x64") { + return "windows-x64-msvc"; + } + return null; +} + +function publishCargoDryRun(roots, strict) { + const result = surfaceResult("cargo"); + result.staged.push("dry-run generated release-asset Cargo artifact crates"); + result.staged.push("dry-run generated local Cargo source crates"); + + const target = hostCargoReleaseTarget(); + if (target === null) { + result.skipped.push("current host does not map to a supported native extension Cargo target"); + } else if (discoverExtensionManifests(roots).length === 0) { + result.skipped.push("no extension-artifacts.json manifests found for native extension Cargo crates"); + } else { + result.staged.push(`dry-run native extension Cargo crates for ${target}`); + } + + const crates = discoverFiles(roots, [".crate"]); + if (crates.length === 0) { + addSkip(result, "no .crate artifacts found", strict); + return result; + } + result.published.push(...crates.map((cratePath) => `dry-run cargo index ${rel(cratePath)}`)); + return result; +} + function parsePublishArgs(argv) { const options = { artifactRoots: [], @@ -510,7 +570,9 @@ function parsePublishArgs(argv) { } function canPublishInBun(options) { - return !options.help && options.surfaces.length > 0 && options.surfaces.every((surface) => surface === "maven" || surface === "swift"); + return !options.help + && options.surfaces.length > 0 + && options.surfaces.every((surface) => surface === "maven" || surface === "swift" || (surface === "cargo" && options.dryRun)); } function publish(argv) { @@ -523,7 +585,9 @@ function publish(argv) { mkdirSync(options.registryRoot, { recursive: true }); const results = []; for (const surface of options.surfaces) { - if (surface === "maven") { + if (surface === "cargo") { + results.push(publishCargoDryRun(roots, options.strict)); + } else if (surface === "maven") { results.push(publishMaven(roots, options.registryRoot, options.dryRun, options.strict)); } else if (surface === "swift") { results.push(publishSwift(roots, options.registryRoot, options.dryRun, options.strict)); From c2b3babef47c91b898ce320cdf5f8a2ad169aba3 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 01:06:38 +0000 Subject: [PATCH 242/308] chore: port local registry npm dry run to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 12 ++ tools/policy/check-tooling-stack.sh | 4 + tools/release/check_release_metadata.py | 2 + tools/release/local-registry-publish.mjs | 164 +++++++++++++++++- 4 files changed, 181 insertions(+), 1 deletion(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 8c75c43f..29d36baf 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -143,6 +143,18 @@ until the current-state gates here are checked with fresh local evidence. the source-crate generation and file-backed Cargo index writer are ported. Fresh parity checks diffed Bun and Python output byte-for-byte for strict Cargo dry-run and combined strict Cargo+Maven+Swift dry-run. +- 2026-06-28: Ported `publish --surface npm --dry-run` into + `tools/release/local-registry-publish.mjs`. The Bun implementation now owns + npm tarball identity detection, duplicate tarball preference, dry-run + extension package staging, Verdaccio URL reporting, and local pnpm-store + invalidation reporting while real npm publish still falls back to Python for + the Verdaccio/auth/publish flow. Fresh parity checks diffed Bun and Python + output byte-for-byte for strict npm dry-run and combined strict + Cargo+npm+Maven+Swift dry-run. Fresh gates passed: `node --check` for the + Bun entrypoint, Python `py_compile` for the touched metadata guard, + `check_release_metadata.py`, `check-tooling-stack.sh`, + `check-policy-tools.sh`, `check-docs.sh`, `check-python-entrypoints.mjs + --json`, and `tools/release/release.py check`. - 2026-06-27: Ported the WASIX Cargo artifact packager from `tools/release/package_liboliphaunt_wasix_cargo_artifacts.py` to the Bun entrypoint `tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs`. diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 09da9dd6..aa873d3e 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -383,6 +383,10 @@ grep -Fq 'function publishSwift(' tools/release/local-registry-publish.mjs || fail "local-registry Swift publish surface must run in the Bun entrypoint" grep -Fq 'function publishCargoDryRun(' tools/release/local-registry-publish.mjs || fail "local-registry Cargo dry-run publish surface must run in the Bun entrypoint" +grep -Fq 'function publishNpmDryRun(' tools/release/local-registry-publish.mjs || + fail "local-registry npm dry-run publish surface must run in the Bun entrypoint" +grep -Fq 'function selectNpmTarballs(' tools/release/local-registry-publish.mjs || + fail "local-registry npm dry-run tarball selection must run in the Bun entrypoint" if grep -Fq 'python3 tools/release/local_registry_publish.py' examples/README.md; then fail "example docs must not expose direct Python local-registry commands" fi diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 07322e04..da2db2c2 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -750,6 +750,8 @@ def validate_graph_files() -> None: or "function status(argv)" not in local_registry_publish or "function download(argv)" not in local_registry_publish or "function publishCargoDryRun(" not in local_registry_publish + or "function publishNpmDryRun(" not in local_registry_publish + or "function selectNpmTarballs(" not in local_registry_publish or "function discoverExtensionManifests(" not in local_registry_publish or "function publishMaven(" not in local_registry_publish or "function publishSwift(" not in local_registry_publish diff --git a/tools/release/local-registry-publish.mjs b/tools/release/local-registry-publish.mjs index b9eb87b8..af783325 100644 --- a/tools/release/local-registry-publish.mjs +++ b/tools/release/local-registry-publish.mjs @@ -6,6 +6,7 @@ import { copyFileSync, existsSync, mkdirSync, + readFileSync, readdirSync, rmSync, statSync, @@ -58,6 +59,18 @@ function commandOutput(args) { return result.stdout; } +function tryCommandOutput(args) { + const result = spawnSync(args[0], args.slice(1), { + cwd: ROOT, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.error || result.status !== 0) { + return null; + } + return result.stdout; +} + function runQuiet(args) { const result = spawnSync(args[0], args.slice(1), { cwd: ROOT, @@ -471,6 +484,147 @@ function hostCargoReleaseTarget() { return null; } +function hostNpmTarget() { + return hostCargoReleaseTarget(); +} + +function extensionNpmPackage(sqlName) { + return `@oliphaunt/extension-${sqlName.replaceAll("_", "-")}`; +} + +function npmPackageIdentity(tarball) { + const members = tryCommandOutput(["tar", "-tzf", tarball]); + if (members === null) { + return null; + } + for (const member of members.split(/\r?\n/u).filter(Boolean)) { + if (!member.endsWith("/package.json")) { + continue; + } + const rawPackageJson = tryCommandOutput(["tar", "-xOzf", tarball, member]); + if (rawPackageJson === null) { + continue; + } + try { + const packageJson = JSON.parse(rawPackageJson); + if (typeof packageJson.name === "string" && typeof packageJson.version === "string") { + return { name: packageJson.name, version: packageJson.version }; + } + } catch { + return null; + } + } + return null; +} + +function pathIsUnder(file, root) { + const relative = path.relative(path.resolve(root), path.resolve(file)); + return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); +} + +function npmTarballPriority(tarball, registryRoot) { + let priority = 20; + for (const [root, value] of [ + [path.join(ROOT, "target/release/npm-packages"), 100], + [path.join(ROOT, "target/sdk-artifacts"), 90], + [path.join(registryRoot, "npm-extension-packages"), 80], + [DEFAULT_CURRENT_ARTIFACT_ROOT, 60], + [DEFAULT_ARTIFACT_ROOT, 30], + ]) { + if (pathIsUnder(tarball, root)) { + priority = value; + break; + } + } + let modified = 0; + try { + modified = statSync(tarball).mtimeMs; + } catch { + // Missing tarballs are handled by the caller's artifact discovery. + } + return [priority, modified, tarball]; +} + +function compareNpmTarballPriority(left, right) { + for (let index = 0; index < 2; index += 1) { + if (left[index] !== right[index]) { + return left[index] - right[index]; + } + } + return compareText(left[2], right[2]); +} + +function selectNpmTarballs(tarballs, registryRoot, result) { + const selected = new Map(); + const unidentified = []; + for (const tarball of tarballs) { + const identity = npmPackageIdentity(tarball); + if (identity === null) { + unidentified.push(tarball); + continue; + } + const key = `${identity.name}\0${identity.version}`; + const current = selected.get(key); + if (current === undefined) { + selected.set(key, tarball); + continue; + } + const preferred = compareNpmTarballPriority( + npmTarballPriority(tarball, registryRoot), + npmTarballPriority(current, registryRoot), + ) > 0 + ? tarball + : current; + const skipped = preferred === tarball ? current : tarball; + selected.set(key, preferred); + result.staged.push( + `preferred ${rel(preferred)} over ${rel(skipped)} for ${identity.name}@${identity.version}`, + ); + } + return [...unidentified, ...selected.values()].sort(compareText); +} + +function stageExtensionNpmPackagesDryRun(roots, target, result) { + const manifests = discoverExtensionManifests(roots); + if (manifests.length === 0) { + result.skipped.push("no extension-artifacts.json manifests found for npm extension packages"); + return; + } + if (target === null) { + result.skipped.push("current host does not map to a supported npm extension target"); + return; + } + for (const manifestPath of manifests) { + const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); + const sqlName = manifest.sqlName; + const version = manifest.version; + if (typeof sqlName === "string" && typeof version === "string") { + result.staged.push(`dry-run npm extension packages ${extensionNpmPackage(sqlName)}@${version} (${target})`); + } + } +} + +function publishNpmDryRun(roots, registryRoot, strict, port) { + const result = surfaceResult("npm"); + result.staged.push("dry-run generated liboliphaunt and broker npm artifact packages"); + stageExtensionNpmPackagesDryRun(roots, hostNpmTarget(), result); + + const tarballs = selectNpmTarballs(discoverFiles(roots, [".tgz"]), registryRoot, result); + if (tarballs.length === 0) { + addSkip(result, "no npm .tgz artifacts found", strict); + return result; + } + + result.staged.push(`verdaccio=http://127.0.0.1:${port}`); + for (const tarball of tarballs) { + const identity = npmPackageIdentity(tarball); + const label = identity === null ? rel(tarball) : `${identity.name}@${identity.version}`; + result.published.push(`dry-run npm publish ${label}`); + } + result.staged.push(`cleared local pnpm store ${rel(path.join(registryRoot, "pnpm-store"))}`); + return result; +} + function publishCargoDryRun(roots, strict) { const result = surfaceResult("cargo"); result.staged.push("dry-run generated release-asset Cargo artifact crates"); @@ -572,7 +726,13 @@ function parsePublishArgs(argv) { function canPublishInBun(options) { return !options.help && options.surfaces.length > 0 - && options.surfaces.every((surface) => surface === "maven" || surface === "swift" || (surface === "cargo" && options.dryRun)); + && options.surfaces.every( + (surface) => + surface === "maven" || + surface === "swift" || + (surface === "cargo" && options.dryRun) || + (surface === "npm" && options.dryRun), + ); } function publish(argv) { @@ -587,6 +747,8 @@ function publish(argv) { for (const surface of options.surfaces) { if (surface === "cargo") { results.push(publishCargoDryRun(roots, options.strict)); + } else if (surface === "npm") { + results.push(publishNpmDryRun(roots, options.registryRoot, options.strict, options.verdaccioPort)); } else if (surface === "maven") { results.push(publishMaven(roots, options.registryRoot, options.dryRun, options.strict)); } else if (surface === "swift") { From 7ca9775e65f29aa7e25c374bbd36adc7a27590c9 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 01:19:43 +0000 Subject: [PATCH 243/308] chore: port prebuilt npm local registry publish to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 12 +- tools/policy/check-tooling-stack.sh | 4 + tools/release/check_release_metadata.py | 2 + tools/release/local-registry-publish.mjs | 342 +++++++++++++++++- 4 files changed, 348 insertions(+), 12 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 29d36baf..d1acbb81 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -147,14 +147,22 @@ until the current-state gates here are checked with fresh local evidence. `tools/release/local-registry-publish.mjs`. The Bun implementation now owns npm tarball identity detection, duplicate tarball preference, dry-run extension package staging, Verdaccio URL reporting, and local pnpm-store - invalidation reporting while real npm publish still falls back to Python for - the Verdaccio/auth/publish flow. Fresh parity checks diffed Bun and Python + invalidation reporting. Fresh parity checks diffed Bun and Python output byte-for-byte for strict npm dry-run and combined strict Cargo+npm+Maven+Swift dry-run. Fresh gates passed: `node --check` for the Bun entrypoint, Python `py_compile` for the touched metadata guard, `check_release_metadata.py`, `check-tooling-stack.sh`, `check-policy-tools.sh`, `check-docs.sh`, `check-python-entrypoints.mjs --json`, and `tools/release/release.py check`. +- 2026-06-28: Ported the real local-registry npm publish loop for prebuilt + `.tgz` artifact roots into `tools/release/local-registry-publish.mjs`. Bun + now owns Verdaccio config/startup, local auth token setup, package existence + checks, replacement unpublish, publish, `report.json`, and local pnpm-store + invalidation when no native/extension npm package synthesis is required. + Fresh smoke published `target/sdk-artifacts/oliphaunt-js/oliphaunt-ts-0.1.0.tgz` + into a disposable Verdaccio registry on port 4891 and stopped the temporary + registry process. Full native runtime/tools and exact-extension npm package + synthesis still falls back to Python until those generators are ported. - 2026-06-27: Ported the WASIX Cargo artifact packager from `tools/release/package_liboliphaunt_wasix_cargo_artifacts.py` to the Bun entrypoint `tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs`. diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index aa873d3e..0f921653 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -385,6 +385,10 @@ grep -Fq 'function publishCargoDryRun(' tools/release/local-registry-publish.mjs fail "local-registry Cargo dry-run publish surface must run in the Bun entrypoint" grep -Fq 'function publishNpmDryRun(' tools/release/local-registry-publish.mjs || fail "local-registry npm dry-run publish surface must run in the Bun entrypoint" +grep -Fq 'async function publishNpmTarballs(' tools/release/local-registry-publish.mjs || + fail "local-registry prebuilt npm tarball publish loop must run in the Bun entrypoint" +grep -Fq 'async function ensureVerdaccio(' tools/release/local-registry-publish.mjs || + fail "local-registry Verdaccio orchestration must run in the Bun entrypoint for prebuilt npm tarballs" grep -Fq 'function selectNpmTarballs(' tools/release/local-registry-publish.mjs || fail "local-registry npm dry-run tarball selection must run in the Bun entrypoint" if grep -Fq 'python3 tools/release/local_registry_publish.py' examples/README.md; then diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index da2db2c2..5fe43a6b 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -751,6 +751,8 @@ def validate_graph_files() -> None: or "function download(argv)" not in local_registry_publish or "function publishCargoDryRun(" not in local_registry_publish or "function publishNpmDryRun(" not in local_registry_publish + or "async function publishNpmTarballs(" not in local_registry_publish + or "async function ensureVerdaccio(" not in local_registry_publish or "function selectNpmTarballs(" not in local_registry_publish or "function discoverExtensionManifests(" not in local_registry_publish or "function publishMaven(" not in local_registry_publish diff --git a/tools/release/local-registry-publish.mjs b/tools/release/local-registry-publish.mjs index af783325..dc62b493 100644 --- a/tools/release/local-registry-publish.mjs +++ b/tools/release/local-registry-publish.mjs @@ -1,11 +1,13 @@ #!/usr/bin/env bun -import { spawnSync } from "node:child_process"; +import { spawn, spawnSync } from "node:child_process"; import { accessSync, + closeSync, constants, copyFileSync, existsSync, mkdirSync, + openSync, readFileSync, readdirSync, rmSync, @@ -21,6 +23,7 @@ const DEFAULT_RUN_ID = "28049923289"; const DEFAULT_REPO = "f0rr0/oliphaunt"; const DEFAULT_CURRENT_ARTIFACT_ROOT = path.join(ROOT, "target/local-registry-current"); const DEFAULT_ARTIFACT_ROOT = path.join(ROOT, "target/local-registry-artifacts"); +const NPM_PACKAGE_SIZE_LIMIT_BYTES = 10 * 1024 * 1024; const DEFAULT_ROOTS = [ DEFAULT_CURRENT_ARTIFACT_ROOT, DEFAULT_ARTIFACT_ROOT, @@ -59,12 +62,17 @@ function commandOutput(args) { return result.stdout; } -function tryCommandOutput(args) { - const result = spawnSync(args[0], args.slice(1), { +function commandResult(args, { timeout = undefined } = {}) { + return spawnSync(args[0], args.slice(1), { cwd: ROOT, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], + timeout, }); +} + +function tryCommandOutput(args) { + const result = commandResult(args); if (result.error || result.status !== 0) { return null; } @@ -625,6 +633,318 @@ function publishNpmDryRun(roots, registryRoot, strict, port) { return result; } +function npmTarballsRequirePythonGeneration(roots) { + const manifests = discoverExtensionManifests(roots); + if (manifests.length > 0) { + return true; + } + for (const root of roots) { + if (!statSync(root).isDirectory()) { + continue; + } + for (const file of walkFiles(root)) { + const name = path.basename(file); + if ( + /^(liboliphaunt|oliphaunt-tools)-[^/]+\.(tar\.gz|zip)$/u.test(name) || + /^oliphaunt-broker-[^/]+\.(tar\.gz|zip)$/u.test(name) + ) { + return true; + } + } + } + return false; +} + +function writeVerdaccioConfig(root, port) { + const resolvedRoot = path.resolve(root); + const config = path.join(resolvedRoot, "config.yaml"); + const storage = path.join(resolvedRoot, "storage"); + mkdirSync(storage, { recursive: true }); + mkdirSync(path.join(resolvedRoot, "plugins"), { recursive: true }); + const text = [ + `storage: ${storage}`, + "max_body_size: 100mb", + "auth:", + " htpasswd:", + ` file: ${path.join(resolvedRoot, "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}", + "", + ].join("\n"); + const previous = existsSync(config) ? readFileSync(config, "utf8") : null; + writeFileSync(config, text); + writeFileSync(path.join(resolvedRoot, "registry-url.txt"), `http://127.0.0.1:${port}\n`); + return { config, changed: previous !== text }; +} + +function processExists(pid) { + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +} + +function stopRecordedVerdaccio(root) { + const pidFile = path.join(root, "verdaccio.pid"); + if (!existsSync(pidFile)) { + return; + } + const pid = Number.parseInt(readFileSync(pidFile, "utf8").trim(), 10); + if (!Number.isInteger(pid)) { + rmSync(pidFile, { force: true }); + return; + } + try { + process.kill(pid, "SIGTERM"); + } catch { + rmSync(pidFile, { force: true }); + return; + } + for (let index = 0; index < 30; index += 1) { + if (!processExists(pid)) { + rmSync(pidFile, { force: true }); + return; + } + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100); + } + try { + process.kill(pid, "SIGKILL"); + } catch { + // Process already exited. + } + rmSync(pidFile, { force: true }); +} + +function npmPing(registryUrl) { + if (!executableExists("npm")) { + return false; + } + const result = commandResult([ + "npm", + "ping", + "--registry", + registryUrl, + "--fetch-timeout=1000", + "--fetch-retries=0", + ], { timeout: 3000 }); + return !result.error && result.status === 0; +} + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function ensureVerdaccio(root, port, dryRun) { + const registryUrl = `http://127.0.0.1:${port}`; + const { config, changed } = writeVerdaccioConfig(root, port); + if (changed && !dryRun) { + stopRecordedVerdaccio(root); + } + if (npmPing(registryUrl)) { + return registryUrl; + } + if (dryRun) { + return registryUrl; + } + + requireCommand("pnpm"); + const logPath = path.join(root, "verdaccio.log"); + mkdirSync(path.dirname(logPath), { recursive: true }); + const log = openSync(logPath, "a"); + const child = spawn( + "pnpm", + ["dlx", "verdaccio@6", "--config", config, "--listen", registryUrl], + { + cwd: ROOT, + detached: true, + stdio: ["ignore", log, log], + }, + ); + child.unref(); + closeSync(log); + writeFileSync(path.join(root, "verdaccio.pid"), `${child.pid}\n`); + for (let attempt = 0; attempt < 60; attempt += 1) { + if (npmPing(registryUrl)) { + return registryUrl; + } + if (child.exitCode !== null) { + fail(TOOL, `Verdaccio exited early; see ${rel(logPath)}`); + } + await sleep(1000); + } + fail(TOOL, `Timed out waiting for Verdaccio; see ${rel(logPath)}`); +} + +function npmAuthIsValid(registryUrl, npmrc) { + const result = commandResult([ + "npm", + "whoami", + "--registry", + registryUrl, + "--userconfig", + npmrc, + "--loglevel=error", + ], { timeout: 10000 }); + return !result.error && result.status === 0; +} + +async function ensureVerdaccioNpmrc(root, registryUrl, dryRun) { + if (dryRun) { + return null; + } + const npmrc = path.join(root, "npmrc"); + if (existsSync(npmrc)) { + const text = readFileSync(npmrc, "utf8"); + if (text.includes("always-auth")) { + writeFileSync(npmrc, `${text.split(/\r?\n/u).filter((line) => !line.startsWith("always-auth=")).join("\n")}\n`); + } + if (npmAuthIsValid(registryUrl, npmrc)) { + return npmrc; + } + rmSync(npmrc, { force: true }); + } + const username = "oliphaunt-local"; + const response = await fetch(`${registryUrl}/-/user/org.couchdb.user:${username}`, { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + name: username, + password: "oliphaunt-local", + email: "local-registry@oliphaunt.invalid", + type: "user", + roles: [], + date: new Date().toISOString().replace(/\.\d{3}Z$/u, ".000Z"), + }), + }); + if (!response.ok) { + fail(TOOL, `failed to create local Verdaccio user: HTTP ${response.status}: ${await response.text()}`); + } + const data = await response.json(); + if (typeof data.token !== "string" || data.token.length === 0) { + fail(TOOL, "Verdaccio did not return an auth token for the local user"); + } + const host = registryUrl.replace(/^https?:\/\//u, ""); + writeFileSync(npmrc, [`registry=${registryUrl}/`, `//${host}/:_authToken=${data.token}`, ""].join("\n")); + return npmrc; +} + +function npmPackageExists(registryUrl, npmrc, name, version) { + const command = [ + "npm", + "view", + `${name}@${version}`, + "version", + "--registry", + registryUrl, + "--fetch-retries=0", + "--loglevel=error", + ]; + if (npmrc !== null) { + command.push("--userconfig", npmrc); + } + const result = commandResult(command, { timeout: 10000 }); + return !result.error && result.status === 0 && result.stdout.trim() === version; +} + +function runNpmPublishCommand(args) { + const result = spawnSync(args[0], args.slice(1), { + cwd: ROOT, + stdio: "inherit", + }); + if (result.error) { + fail(TOOL, `${args[0]} failed to start: ${result.error.message}`); + } + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} + +async function publishNpmTarballs(roots, registryRoot, strict, port) { + const result = surfaceResult("npm"); + result.skipped.push("no liboliphaunt release assets found for native npm artifact packages"); + result.skipped.push("no broker release assets found for broker npm artifact packages"); + if (discoverExtensionManifests(roots).length === 0) { + result.skipped.push("no extension-artifacts.json manifests found for npm extension packages"); + } + + const tarballs = selectNpmTarballs(discoverFiles(roots, [".tgz"]), registryRoot, result); + if (tarballs.length === 0) { + addSkip(result, "no npm .tgz artifacts found", strict); + return result; + } + for (const tarball of tarballs) { + const size = statSync(tarball).size; + if (size > NPM_PACKAGE_SIZE_LIMIT_BYTES) { + addSkip(result, `${rel(tarball)} is ${size} bytes, exceeding the 10 MiB npm package limit`, strict); + return result; + } + } + + const verdaccioRoot = path.join(registryRoot, "verdaccio"); + const registryUrl = await ensureVerdaccio(verdaccioRoot, port, false); + const npmrc = await ensureVerdaccioNpmrc(verdaccioRoot, registryUrl, false); + result.staged.push(`verdaccio=${registryUrl}`); + + for (const tarball of tarballs) { + const identity = npmPackageIdentity(tarball); + if (identity !== null && npmPackageExists(registryUrl, npmrc, identity.name, identity.version)) { + const command = [ + "npm", + "unpublish", + `${identity.name}@${identity.version}`, + "--registry", + registryUrl, + "--force", + "--loglevel=error", + ]; + if (npmrc !== null) { + command.push("--userconfig", npmrc); + } + runNpmPublishCommand(command); + result.staged.push(`replaced ${identity.name}@${identity.version}`); + } + const command = [ + "npm", + "publish", + tarball, + "--registry", + registryUrl, + "--provenance=false", + "--ignore-scripts", + "--access", + "public", + "--loglevel=error", + ]; + if (npmrc !== null) { + command.push("--userconfig", npmrc); + } + runNpmPublishCommand(command); + result.published.push(rel(tarball)); + } + rmSync(path.join(registryRoot, "pnpm-store"), { recursive: true, force: true }); + result.staged.push(`cleared local pnpm store ${rel(path.join(registryRoot, "pnpm-store"))}`); + return result; +} + function publishCargoDryRun(roots, strict) { const result = surfaceResult("cargo"); result.staged.push("dry-run generated release-asset Cargo artifact crates"); @@ -723,7 +1043,7 @@ function parsePublishArgs(argv) { return options; } -function canPublishInBun(options) { +function canPublishInBun(options, roots) { return !options.help && options.surfaces.length > 0 && options.surfaces.every( @@ -731,24 +1051,26 @@ function canPublishInBun(options) { surface === "maven" || surface === "swift" || (surface === "cargo" && options.dryRun) || - (surface === "npm" && options.dryRun), + (surface === "npm" && (options.dryRun || !npmTarballsRequirePythonGeneration(roots))), ); } -function publish(argv) { +async function publish(argv) { const options = parsePublishArgs(argv); - if (!canPublishInBun(options)) { + const roots = discoverRoots(options.artifactRoots); + if (!canPublishInBun(options, roots)) { run(TOOL, ["python3", "tools/release/local_registry_publish.py", "publish", ...argv]); return; } - const roots = discoverRoots(options.artifactRoots); mkdirSync(options.registryRoot, { recursive: true }); const results = []; for (const surface of options.surfaces) { if (surface === "cargo") { results.push(publishCargoDryRun(roots, options.strict)); } else if (surface === "npm") { - results.push(publishNpmDryRun(roots, options.registryRoot, options.strict, options.verdaccioPort)); + results.push(options.dryRun + ? publishNpmDryRun(roots, options.registryRoot, options.strict, options.verdaccioPort) + : await publishNpmTarballs(roots, options.registryRoot, options.strict, options.verdaccioPort)); } else if (surface === "maven") { results.push(publishMaven(roots, options.registryRoot, options.dryRun, options.strict)); } else if (surface === "swift") { @@ -827,7 +1149,7 @@ const [command, ...args] = Bun.argv.slice(2); if (command === "download") { download(args); } else if (command === "publish") { - publish(args); + await publish(args); } else if (command === "status") { status(args); } else { From fd8ff2621ba6e4514d15b66f516abbcc9e33b974 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 01:23:37 +0000 Subject: [PATCH 244/308] chore: keep local registry status in bun --- docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 6 ++++++ tools/policy/check-tooling-stack.sh | 5 +++++ tools/release/check_release_metadata.py | 2 ++ tools/release/local-registry-publish.mjs | 11 ++++++++++- 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index d1acbb81..4374e1fc 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -163,6 +163,12 @@ until the current-state gates here are checked with fresh local evidence. into a disposable Verdaccio registry on port 4891 and stopped the temporary registry process. Full native runtime/tools and exact-extension npm package synthesis still falls back to Python until those generators are ported. +- 2026-06-28: Removed the last Python delegation from the local-registry + `status` subcommand by adding Bun-native `status --help` output. The regular + status report was already generated in Bun; metadata and tooling guards now + reject reintroducing a status-specific Python fallback. Fresh checks diffed + the Bun and Python status JSON report byte-for-byte and verified the Bun help + path without invoking Python. - 2026-06-27: Ported the WASIX Cargo artifact packager from `tools/release/package_liboliphaunt_wasix_cargo_artifacts.py` to the Bun entrypoint `tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs`. diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 0f921653..10e489cc 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -385,6 +385,11 @@ grep -Fq 'function publishCargoDryRun(' tools/release/local-registry-publish.mjs fail "local-registry Cargo dry-run publish surface must run in the Bun entrypoint" grep -Fq 'function publishNpmDryRun(' tools/release/local-registry-publish.mjs || fail "local-registry npm dry-run publish surface must run in the Bun entrypoint" +grep -Fq 'function statusHelp()' tools/release/local-registry-publish.mjs || + fail "local-registry status help must run in the Bun entrypoint" +if grep -Fq '["python3", "tools/release/local_registry_publish.py", "status"' tools/release/local-registry-publish.mjs; then + fail "local-registry status command must not delegate help or execution to Python" +fi grep -Fq 'async function publishNpmTarballs(' tools/release/local-registry-publish.mjs || fail "local-registry prebuilt npm tarball publish loop must run in the Bun entrypoint" grep -Fq 'async function ensureVerdaccio(' tools/release/local-registry-publish.mjs || diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 5fe43a6b..c3d642b2 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -748,6 +748,7 @@ def validate_graph_files() -> None: or 'if (command === "download")' not in local_registry_publish or 'if (command === "publish")' not in local_registry_publish or "function status(argv)" not in local_registry_publish + or "function statusHelp()" not in local_registry_publish or "function download(argv)" not in local_registry_publish or "function publishCargoDryRun(" not in local_registry_publish or "function publishNpmDryRun(" not in local_registry_publish @@ -760,6 +761,7 @@ def validate_graph_files() -> None: or "function canPublishInBun(" not in local_registry_publish or "function discoverRoots(" not in local_registry_publish or "tools/release/local_registry_metadata.mjs" not in local_registry_publish + or '["python3", "tools/release/local_registry_publish.py", "status"' in local_registry_publish or '["python3", "tools/release/local_registry_publish.py", ...Bun.argv.slice(2)]' not in local_registry_publish or "tools/dev/bun.sh tools/release/local-registry-publish.mjs download" not in examples_readme or "tools/dev/bun.sh tools/release/local-registry-publish.mjs publish" not in examples_readme diff --git a/tools/release/local-registry-publish.mjs b/tools/release/local-registry-publish.mjs index dc62b493..fce1719e 100644 --- a/tools/release/local-registry-publish.mjs +++ b/tools/release/local-registry-publish.mjs @@ -1108,7 +1108,7 @@ function parseStatusArgs(argv) { continue; } if (value === "-h" || value === "--help") { - run(TOOL, ["python3", "tools/release/local_registry_publish.py", "status", ...argv]); + statusHelp(); process.exit(0); } console.error(`${TOOL}: unknown status argument ${value}`); @@ -1117,6 +1117,15 @@ function parseStatusArgs(argv) { return { artifactRoots }; } +function statusHelp() { + console.log(`usage: local-registry-publish.mjs status [-h] [--artifact-root ARTIFACT_ROOT] + +options: + -h, --help show this help message and exit + --artifact-root ARTIFACT_ROOT +`); +} + function status(argv) { const { artifactRoots } = parseStatusArgs(argv); const roots = discoverRoots(artifactRoots); From 5871ee0e98dce8bfcc8e40fc15ddc5e74dece90b Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 01:27:27 +0000 Subject: [PATCH 245/308] chore: keep local registry help in bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 6 +++ tools/policy/check-tooling-stack.sh | 8 +++ tools/release/check_release_metadata.py | 5 ++ tools/release/local-registry-publish.mjs | 54 ++++++++++++++++++- 4 files changed, 72 insertions(+), 1 deletion(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 4374e1fc..58086cd0 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -169,6 +169,12 @@ until the current-state gates here are checked with fresh local evidence. reject reintroducing a status-specific Python fallback. Fresh checks diffed the Bun and Python status JSON report byte-for-byte and verified the Bun help path without invoking Python. +- 2026-06-28: Moved the rest of the local-registry help surface into + `tools/release/local-registry-publish.mjs`. Top-level `--help`, + `download --help`, `publish --help`, and `status --help` now return directly + from Bun, and guards require the helper functions plus the `publish --help` + pre-fallback branch. The remaining Python fallback is limited to unported + real publish generation paths and unknown-command compatibility. - 2026-06-27: Ported the WASIX Cargo artifact packager from `tools/release/package_liboliphaunt_wasix_cargo_artifacts.py` to the Bun entrypoint `tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs`. diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 10e489cc..a54b6198 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -385,11 +385,19 @@ grep -Fq 'function publishCargoDryRun(' tools/release/local-registry-publish.mjs fail "local-registry Cargo dry-run publish surface must run in the Bun entrypoint" grep -Fq 'function publishNpmDryRun(' tools/release/local-registry-publish.mjs || fail "local-registry npm dry-run publish surface must run in the Bun entrypoint" +grep -Fq 'function mainHelp()' tools/release/local-registry-publish.mjs || + fail "local-registry top-level help must run in the Bun entrypoint" +grep -Fq 'function downloadHelp()' tools/release/local-registry-publish.mjs || + fail "local-registry download help must run in the Bun entrypoint" +grep -Fq 'function publishHelp()' tools/release/local-registry-publish.mjs || + fail "local-registry publish help must run in the Bun entrypoint" grep -Fq 'function statusHelp()' tools/release/local-registry-publish.mjs || fail "local-registry status help must run in the Bun entrypoint" if grep -Fq '["python3", "tools/release/local_registry_publish.py", "status"' tools/release/local-registry-publish.mjs; then fail "local-registry status command must not delegate help or execution to Python" fi +grep -Fq 'if (options.help)' tools/release/local-registry-publish.mjs || + fail "local-registry publish help must be handled before Python fallback" grep -Fq 'async function publishNpmTarballs(' tools/release/local-registry-publish.mjs || fail "local-registry prebuilt npm tarball publish loop must run in the Bun entrypoint" grep -Fq 'async function ensureVerdaccio(' tools/release/local-registry-publish.mjs || diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index c3d642b2..485fefcd 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -747,8 +747,12 @@ def validate_graph_files() -> None: 'if (command === "status")' not in local_registry_publish or 'if (command === "download")' not in local_registry_publish or 'if (command === "publish")' not in local_registry_publish + or 'command === "-h" || command === "--help"' not in local_registry_publish + or "function mainHelp()" not in local_registry_publish or "function status(argv)" not in local_registry_publish or "function statusHelp()" not in local_registry_publish + or "function downloadHelp()" not in local_registry_publish + or "function publishHelp()" not in local_registry_publish or "function download(argv)" not in local_registry_publish or "function publishCargoDryRun(" not in local_registry_publish or "function publishNpmDryRun(" not in local_registry_publish @@ -761,6 +765,7 @@ def validate_graph_files() -> None: or "function canPublishInBun(" not in local_registry_publish or "function discoverRoots(" not in local_registry_publish or "tools/release/local_registry_metadata.mjs" not in local_registry_publish + or "if (options.help)" not in local_registry_publish or '["python3", "tools/release/local_registry_publish.py", "status"' in local_registry_publish or '["python3", "tools/release/local_registry_publish.py", ...Bun.argv.slice(2)]' not in local_registry_publish or "tools/dev/bun.sh tools/release/local-registry-publish.mjs download" not in examples_readme diff --git a/tools/release/local-registry-publish.mjs b/tools/release/local-registry-publish.mjs index fce1719e..e9e34611 100644 --- a/tools/release/local-registry-publish.mjs +++ b/tools/release/local-registry-publish.mjs @@ -284,7 +284,7 @@ function parseDownloadArgs(argv) { for (let index = 0; index < argv.length; index += 1) { const value = argv[index]; if (value === "-h" || value === "--help") { - console.log(`usage: tools/release/local-registry-publish.mjs download [--repo REPO] [--run-id RUN_ID] [--destination DIR] [--artifact NAME] [--preset local-publish] [--force] [--dry-run]`); + downloadHelp(); process.exit(0); } if (value === "--repo") { @@ -348,6 +348,21 @@ function parseDownloadArgs(argv) { return options; } +function downloadHelp() { + console.log(`usage: local-registry-publish.mjs download [-h] [--repo REPO] [--run-id RUN_ID] [--destination DESTINATION] [--artifact ARTIFACT] [--preset local-publish] [--force] [--dry-run] + +options: + -h, --help show this help message and exit + --repo REPO + --run-id RUN_ID + --destination DESTINATION + --artifact ARTIFACT + --preset local-publish + --force + --dry-run +`); +} + function download(argv) { const options = parseDownloadArgs(argv); const selectedArtifacts = [ @@ -1057,6 +1072,10 @@ function canPublishInBun(options, roots) { async function publish(argv) { const options = parsePublishArgs(argv); + if (options.help) { + publishHelp(); + return; + } const roots = discoverRoots(options.artifactRoots); if (!canPublishInBun(options, roots)) { run(TOOL, ["python3", "tools/release/local_registry_publish.py", "publish", ...argv]); @@ -1090,6 +1109,21 @@ async function publish(argv) { process.stdout.write(text); } +function publishHelp() { + console.log(`usage: local-registry-publish.mjs publish [-h] [--artifact-root ARTIFACT_ROOT] [--registry-root REGISTRY_ROOT] [--surface {npm,cargo,maven,swift}] [--verdaccio-port VERDACCIO_PORT] [--dry-run] [--strict] + +options: + -h, --help show this help message and exit + --artifact-root ARTIFACT_ROOT + --registry-root REGISTRY_ROOT + --surface {npm,cargo,maven,swift} + publish only this surface; may be repeated + --verdaccio-port VERDACCIO_PORT + --dry-run + --strict +`); +} + function parseStatusArgs(argv) { const artifactRoots = []; for (let index = 0; index < argv.length; index += 1) { @@ -1154,6 +1188,22 @@ function status(argv) { console.log(JSON.stringify(report, null, 2)); } +function mainHelp() { + console.log(`usage: local-registry-publish.mjs [-h] {download,publish,status} ... + +Stage Oliphaunt release artifacts into local package registries. + +positional arguments: + {download,publish,status} + download download GitHub Actions artifacts with gh + publish publish staged artifacts to local registries + status show locally available staged artifacts + +options: + -h, --help show this help message and exit +`); +} + const [command, ...args] = Bun.argv.slice(2); if (command === "download") { download(args); @@ -1161,6 +1211,8 @@ if (command === "download") { await publish(args); } else if (command === "status") { status(args); +} else if (command === "-h" || command === "--help") { + mainHelp(); } else { run(TOOL, ["python3", "tools/release/local_registry_publish.py", ...Bun.argv.slice(2)]); } From f86e9562959ed2a810215c60c1cc961f98b967c3 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 01:30:38 +0000 Subject: [PATCH 246/308] chore: remove local registry catch-all python fallback --- docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 7 ++++++- tools/policy/check-tooling-stack.sh | 7 +++++++ tools/release/check_release_metadata.py | 4 +++- tools/release/local-registry-publish.mjs | 9 ++++++++- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 58086cd0..5f79766c 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -174,7 +174,12 @@ until the current-state gates here are checked with fresh local evidence. `download --help`, `publish --help`, and `status --help` now return directly from Bun, and guards require the helper functions plus the `publish --help` pre-fallback branch. The remaining Python fallback is limited to unported - real publish generation paths and unknown-command compatibility. + real publish generation paths. +- 2026-06-28: Removed the generic unknown-command Python fallback from + `tools/release/local-registry-publish.mjs`. Unsupported local-registry + commands now fail in Bun with exit code 2, and metadata/tooling guards require + the publish fallback to stay explicit to `publish` while rejecting a catch-all + `local_registry_publish.py` dispatch. - 2026-06-27: Ported the WASIX Cargo artifact packager from `tools/release/package_liboliphaunt_wasix_cargo_artifacts.py` to the Bun entrypoint `tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs`. diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index a54b6198..b0da38fa 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -387,6 +387,8 @@ grep -Fq 'function publishNpmDryRun(' tools/release/local-registry-publish.mjs | fail "local-registry npm dry-run publish surface must run in the Bun entrypoint" grep -Fq 'function mainHelp()' tools/release/local-registry-publish.mjs || fail "local-registry top-level help must run in the Bun entrypoint" +grep -Fq 'function unsupportedCommand(' tools/release/local-registry-publish.mjs || + fail "local-registry unsupported command handling must run in the Bun entrypoint" grep -Fq 'function downloadHelp()' tools/release/local-registry-publish.mjs || fail "local-registry download help must run in the Bun entrypoint" grep -Fq 'function publishHelp()' tools/release/local-registry-publish.mjs || @@ -398,6 +400,11 @@ if grep -Fq '["python3", "tools/release/local_registry_publish.py", "status"' to fi grep -Fq 'if (options.help)' tools/release/local-registry-publish.mjs || fail "local-registry publish help must be handled before Python fallback" +grep -Fq '["python3", "tools/release/local_registry_publish.py", "publish", ...argv]' tools/release/local-registry-publish.mjs || + fail "local-registry real publish generation fallback must stay explicit to the publish command" +if grep -Fq '["python3", "tools/release/local_registry_publish.py", ...Bun.argv.slice(2)]' tools/release/local-registry-publish.mjs; then + fail "local-registry command dispatch must not use a generic Python fallback" +fi grep -Fq 'async function publishNpmTarballs(' tools/release/local-registry-publish.mjs || fail "local-registry prebuilt npm tarball publish loop must run in the Bun entrypoint" grep -Fq 'async function ensureVerdaccio(' tools/release/local-registry-publish.mjs || diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 485fefcd..73877fbf 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -749,6 +749,7 @@ def validate_graph_files() -> None: or 'if (command === "publish")' not in local_registry_publish or 'command === "-h" || command === "--help"' not in local_registry_publish or "function mainHelp()" not in local_registry_publish + or "function unsupportedCommand(" not in local_registry_publish or "function status(argv)" not in local_registry_publish or "function statusHelp()" not in local_registry_publish or "function downloadHelp()" not in local_registry_publish @@ -766,8 +767,9 @@ def validate_graph_files() -> None: or "function discoverRoots(" not in local_registry_publish or "tools/release/local_registry_metadata.mjs" not in local_registry_publish or "if (options.help)" not in local_registry_publish + or '["python3", "tools/release/local_registry_publish.py", "publish", ...argv]' not in local_registry_publish or '["python3", "tools/release/local_registry_publish.py", "status"' in local_registry_publish - or '["python3", "tools/release/local_registry_publish.py", ...Bun.argv.slice(2)]' not in local_registry_publish + or '["python3", "tools/release/local_registry_publish.py", ...Bun.argv.slice(2)]' in local_registry_publish or "tools/dev/bun.sh tools/release/local-registry-publish.mjs download" not in examples_readme or "tools/dev/bun.sh tools/release/local-registry-publish.mjs publish" not in examples_readme or "python3 tools/release/local_registry_publish.py" in examples_readme diff --git a/tools/release/local-registry-publish.mjs b/tools/release/local-registry-publish.mjs index e9e34611..248baa1a 100644 --- a/tools/release/local-registry-publish.mjs +++ b/tools/release/local-registry-publish.mjs @@ -1204,6 +1204,13 @@ options: `); } +function unsupportedCommand(command) { + const label = command === undefined ? "" : command; + console.error(`${TOOL}: unsupported command ${label}; expected download, publish, or status`); + mainHelp(); + process.exit(2); +} + const [command, ...args] = Bun.argv.slice(2); if (command === "download") { download(args); @@ -1214,5 +1221,5 @@ if (command === "download") { } else if (command === "-h" || command === "--help") { mainHelp(); } else { - run(TOOL, ["python3", "tools/release/local_registry_publish.py", ...Bun.argv.slice(2)]); + unsupportedCommand(command); } From 7ec5374ce9b72f8558eaa482afb9f88b0cb47c4e Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 01:44:26 +0000 Subject: [PATCH 247/308] chore: port prebuilt cargo local registry publish to bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 8 + tools/policy/check-tooling-stack.sh | 10 + tools/release/check_release_metadata.py | 6 + tools/release/local-registry-publish.mjs | 341 +++++++++++++++++- 4 files changed, 361 insertions(+), 4 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 5f79766c..31f6a569 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -180,6 +180,14 @@ until the current-state gates here are checked with fresh local evidence. commands now fail in Bun with exit code 2, and metadata/tooling guards require the publish fallback to stay explicit to `publish` while rejecting a catch-all `local_registry_publish.py` dispatch. +- 2026-06-28: Ported the real local-registry Cargo publish loop for explicit + prebuilt `.crate` artifact roots into `tools/release/local-registry-publish.mjs`. + Bun now extracts crate metadata, writes the file-backed Cargo git index, + translates local versus crates.io dependency registry fields, rejects crates + over the 10 MiB package limit, writes the Cargo config snippet, clears the + local Cargo cache, and emits `report.json`. Release-asset, source-crate, and + native-extension Cargo generation paths still use the explicit Python publish + fallback until those generators are ported. - 2026-06-27: Ported the WASIX Cargo artifact packager from `tools/release/package_liboliphaunt_wasix_cargo_artifacts.py` to the Bun entrypoint `tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs`. diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index b0da38fa..c82da533 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -383,6 +383,14 @@ grep -Fq 'function publishSwift(' tools/release/local-registry-publish.mjs || fail "local-registry Swift publish surface must run in the Bun entrypoint" grep -Fq 'function publishCargoDryRun(' tools/release/local-registry-publish.mjs || fail "local-registry Cargo dry-run publish surface must run in the Bun entrypoint" +grep -Fq 'function publishCargoCrates(' tools/release/local-registry-publish.mjs || + fail "local-registry prebuilt Cargo crate publish loop must run in the Bun entrypoint" +grep -Fq 'function cargoCratesRequirePythonGeneration(' tools/release/local-registry-publish.mjs || + fail "local-registry Cargo publish must keep generation paths on the explicit Python fallback" +grep -Fq 'function cargoIndexEntry(' tools/release/local-registry-publish.mjs || + fail "local-registry Cargo index entries must be written by the Bun entrypoint for prebuilt crates" +grep -Fq 'function clearLocalCargoHomeCache(' tools/release/local-registry-publish.mjs || + fail "local-registry Cargo publish must clear local Cargo cache from the Bun entrypoint" grep -Fq 'function publishNpmDryRun(' tools/release/local-registry-publish.mjs || fail "local-registry npm dry-run publish surface must run in the Bun entrypoint" grep -Fq 'function mainHelp()' tools/release/local-registry-publish.mjs || @@ -400,6 +408,8 @@ if grep -Fq '["python3", "tools/release/local_registry_publish.py", "status"' to fi grep -Fq 'if (options.help)' tools/release/local-registry-publish.mjs || fail "local-registry publish help must be handled before Python fallback" +grep -Fq '(surface === "cargo" && (options.dryRun || !cargoCratesRequirePythonGeneration(options, roots)))' tools/release/local-registry-publish.mjs || + fail "local-registry Cargo real publish must use Bun only for prebuilt crate roots" grep -Fq '["python3", "tools/release/local_registry_publish.py", "publish", ...argv]' tools/release/local-registry-publish.mjs || fail "local-registry real publish generation fallback must stay explicit to the publish command" if grep -Fq '["python3", "tools/release/local_registry_publish.py", ...Bun.argv.slice(2)]' tools/release/local-registry-publish.mjs; then diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 73877fbf..dc896a16 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -756,6 +756,11 @@ def validate_graph_files() -> None: or "function publishHelp()" not in local_registry_publish or "function download(argv)" not in local_registry_publish or "function publishCargoDryRun(" not in local_registry_publish + or "function publishCargoCrates(" not in local_registry_publish + or "function cargoCratesRequirePythonGeneration(" not in local_registry_publish + or "function cargoMetadataForCrate(" not in local_registry_publish + or "function cargoIndexEntry(" not in local_registry_publish + or "function clearLocalCargoHomeCache(" not in local_registry_publish or "function publishNpmDryRun(" not in local_registry_publish or "async function publishNpmTarballs(" not in local_registry_publish or "async function ensureVerdaccio(" not in local_registry_publish @@ -767,6 +772,7 @@ def validate_graph_files() -> None: or "function discoverRoots(" not in local_registry_publish or "tools/release/local_registry_metadata.mjs" not in local_registry_publish or "if (options.help)" not in local_registry_publish + or '(surface === "cargo" && (options.dryRun || !cargoCratesRequirePythonGeneration(options, roots)))' not in local_registry_publish or '["python3", "tools/release/local_registry_publish.py", "publish", ...argv]' not in local_registry_publish or '["python3", "tools/release/local_registry_publish.py", "status"' in local_registry_publish or '["python3", "tools/release/local_registry_publish.py", ...Bun.argv.slice(2)]' in local_registry_publish diff --git a/tools/release/local-registry-publish.mjs b/tools/release/local-registry-publish.mjs index 248baa1a..20f39dce 100644 --- a/tools/release/local-registry-publish.mjs +++ b/tools/release/local-registry-publish.mjs @@ -1,11 +1,13 @@ #!/usr/bin/env bun import { spawn, spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; import { accessSync, closeSync, constants, copyFileSync, existsSync, + mkdtempSync, mkdirSync, openSync, readFileSync, @@ -24,6 +26,16 @@ const DEFAULT_REPO = "f0rr0/oliphaunt"; const DEFAULT_CURRENT_ARTIFACT_ROOT = path.join(ROOT, "target/local-registry-current"); const DEFAULT_ARTIFACT_ROOT = path.join(ROOT, "target/local-registry-artifacts"); const NPM_PACKAGE_SIZE_LIMIT_BYTES = 10 * 1024 * 1024; +const CARGO_PACKAGE_SIZE_LIMIT_BYTES = 10 * 1024 * 1024; +const CRATES_IO_INDEX = "https://github.com/rust-lang/crates.io-index"; +const LEGACY_WASIX_ARTIFACT_CRATES = new Set([ + "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", +]); +const NON_PUBLISHABLE_LOCAL_CARGO_CRATE_PREFIXES = ["oliphaunt-perf-"]; const DEFAULT_ROOTS = [ DEFAULT_CURRENT_ARTIFACT_ROOT, DEFAULT_ARTIFACT_ROOT, @@ -79,9 +91,9 @@ function tryCommandOutput(args) { return result.stdout; } -function runQuiet(args) { +function runQuiet(args, { cwd = ROOT } = {}) { const result = spawnSync(args[0], args.slice(1), { - cwd: ROOT, + cwd, stdio: "inherit", }); if (result.error) { @@ -983,6 +995,325 @@ function publishCargoDryRun(roots, strict) { return result; } +function cargoCratesRequirePythonGeneration(options, roots) { + if (options.artifactRoots.length === 0) { + return true; + } + if (discoverFiles(roots, [".crate"]).length === 0) { + return true; + } + if (discoverExtensionManifests(roots).length > 0) { + return true; + } + for (const root of roots) { + const stats = statSync(root); + const rootName = path.basename(root); + if ( + stats.isFile() && + ( + /^(liboliphaunt|oliphaunt-tools)-[^/]+\.(tar\.gz|zip)$/u.test(rootName) || + /^oliphaunt-broker-[^/]+\.(tar\.gz|zip)$/u.test(rootName) || + /^liboliphaunt-wasix-[^/]+\.tar\.zst$/u.test(rootName) + ) + ) { + return true; + } + if (!stats.isDirectory()) { + continue; + } + for (const file of walkFiles(root)) { + const name = path.basename(file); + if ( + /^(liboliphaunt|oliphaunt-tools)-[^/]+\.(tar\.gz|zip)$/u.test(name) || + /^oliphaunt-broker-[^/]+\.(tar\.gz|zip)$/u.test(name) || + /^liboliphaunt-wasix-[^/]+\.tar\.zst$/u.test(name) + ) { + return true; + } + } + } + return false; +} + +function cargoCratePriority(cratePath, registryRoot) { + let priority = 20; + for (const [root, value] of [ + [path.join(registryRoot, "cargo-generated"), 100], + [path.join(ROOT, "target/oliphaunt-wasix/cargo-artifacts-check"), 90], + [path.join(ROOT, "target/local-registry-generated"), 80], + [path.join(ROOT, "target/oliphaunt-wasix/cargo-artifacts"), 70], + [DEFAULT_CURRENT_ARTIFACT_ROOT, 60], + [path.join(ROOT, "target/package/tmp-registry"), 40], + [path.join(ROOT, "target/package/tmp-crate"), 30], + ]) { + if (pathIsUnder(cratePath, root)) { + priority = value; + break; + } + } + return [priority, cratePath]; +} + +function compareCargoCratePriority(left, right) { + if (left[0] !== right[0]) { + return left[0] - right[0]; + } + return compareText(left[1], right[1]); +} + +function isDefaultCargoTmpCrateArtifact(cratePath) { + return pathIsUnder(cratePath, path.join(ROOT, "target/package/tmp-crate")); +} + +function crateIndexPath(name) { + const lower = name.toLowerCase(); + if (lower.length === 1) { + return path.join("1", lower); + } + if (lower.length === 2) { + return path.join("2", lower); + } + if (lower.length === 3) { + return path.join("3", lower.slice(0, 1), lower); + } + return path.join(lower.slice(0, 2), lower.slice(2, 4), lower); +} + +function cargoPackageLinksFromManifest(manifest) { + const lines = readFileSync(manifest, "utf8").split(/\r?\n/u); + let inPackage = false; + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed === "[package]") { + inPackage = true; + continue; + } + if (trimmed.startsWith("[") && trimmed !== "[package]") { + inPackage = false; + continue; + } + if (!inPackage) { + continue; + } + const match = trimmed.match(/^links\s*=\s*"([^"]+)"\s*(?:#.*)?$/u); + if (match !== null) { + return match[1]; + } + } + return null; +} + +function cargoMetadataForCrate(cratePath) { + const temp = mkdtempSync(path.join(os.tmpdir(), "oliphaunt-crate-")); + try { + const result = commandResult(["tar", "-xzf", cratePath, "-C", temp]); + if (result.error || result.status !== 0) { + const detail = (result.stderr || result.stdout || result.error?.message || "").trim(); + throw new Error(`failed to extract ${rel(cratePath)}${detail ? `: ${detail}` : ""}`); + } + const manifests = readdirSync(temp, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => path.join(temp, entry.name, "Cargo.toml")) + .filter((manifest) => existsSync(manifest)) + .sort(compareText); + if (manifests.length === 0) { + throw new Error(`${rel(cratePath)} does not contain Cargo.toml`); + } + const metadata = commandResult([ + "cargo", + "metadata", + "--manifest-path", + manifests[0], + "--format-version", + "1", + "--no-deps", + ]); + if (metadata.error || metadata.status !== 0) { + const detail = (metadata.stderr || metadata.stdout || metadata.error?.message || "").trim(); + throw new Error(`cargo metadata failed for ${rel(cratePath)}${detail ? `: ${detail}` : ""}`); + } + let parsed; + try { + parsed = JSON.parse(metadata.stdout); + } catch (error) { + throw new Error(`cargo metadata for ${rel(cratePath)} did not return valid JSON: ${error.message}`); + } + const packages = parsed?.packages; + if (!Array.isArray(packages) || packages.length === 0 || typeof packages[0] !== "object") { + throw new Error(`cargo metadata for ${rel(cratePath)} did not return a package`); + } + return { + ...packages[0], + _oliphaunt_links: cargoPackageLinksFromManifest(manifests[0]), + }; + } finally { + rmSync(temp, { recursive: true, force: true }); + } +} + +function sha256File(file) { + return createHash("sha256").update(readFileSync(file)).digest("hex"); +} + +function cargoIndexDependency(dep, localPackageNames) { + let registry = dep.registry ?? null; + if (localPackageNames.has(dep.name)) { + registry = null; + } else if (registry === null) { + registry = CRATES_IO_INDEX; + } + return { + name: dep.name, + req: dep.req ?? "*", + features: dep.features ?? [], + optional: Boolean(dep.optional), + default_features: Boolean(dep.uses_default_features ?? dep.default_features ?? true), + target: dep.target ?? null, + kind: dep.kind ?? "normal", + registry, + package: dep.rename ?? dep.package ?? null, + }; +} + +function cargoIndexEntry(cratePath, packageData, localPackageNames) { + return { + name: packageData.name, + vers: packageData.version, + deps: (packageData.dependencies ?? []).map((dep) => cargoIndexDependency(dep, localPackageNames)), + features: packageData.features ?? {}, + features2: null, + cksum: sha256File(cratePath), + yanked: false, + links: packageData._oliphaunt_links ?? null, + rust_version: packageData.rust_version ?? null, + v: 2, + }; +} + +function clearLocalCargoHomeCache(registryRoot) { + const cargoHomeRegistry = path.join(registryRoot, "cargo-home", "registry"); + const removed = []; + for (const name of ["cache", "src", "index"]) { + const target = path.join(cargoHomeRegistry, name); + if (existsSync(target)) { + rmSync(target, { recursive: true, force: true }); + removed.push(target); + } + } + const packageCache = path.join(cargoHomeRegistry, ".package-cache"); + if (existsSync(packageCache)) { + rmSync(packageCache, { force: true }); + removed.push(packageCache); + } + return removed; +} + +function publishCargoCrates(roots, registryRoot, strict) { + const result = surfaceResult("cargo"); + result.staged.push("prebuilt .crate Cargo publish handled by Bun"); + const crates = discoverFiles(roots, [".crate"]); + if (crates.length === 0) { + addSkip(result, "no .crate artifacts found", strict); + return result; + } + requireCommand("cargo"); + requireCommand("git"); + + const cargoRoot = path.join(registryRoot, "cargo"); + const cratesDir = path.join(cargoRoot, "crates"); + const indexDir = path.join(cargoRoot, "index"); + const configSnippet = path.join(cargoRoot, "config.toml"); + rmSync(cargoRoot, { recursive: true, force: true }); + mkdirSync(cratesDir, { recursive: true }); + mkdirSync(indexDir, { recursive: true }); + writeFileSync( + path.join(indexDir, "config.json"), + `${JSON.stringify({ dl: `file://${cratesDir}/{crate}-{version}.crate` })}\n`, + ); + + const packagesByTargetName = new Map(); + for (const cratePath of crates.sort((left, right) => + compareCargoCratePriority(cargoCratePriority(left, registryRoot), cargoCratePriority(right, registryRoot)))) { + if (NON_PUBLISHABLE_LOCAL_CARGO_CRATE_PREFIXES.some((prefix) => path.basename(cratePath).startsWith(prefix))) { + result.skipped.push(`ignored non-publishable local Cargo crate artifact ${path.basename(cratePath)}`); + continue; + } + const size = statSync(cratePath).size; + if (size > CARGO_PACKAGE_SIZE_LIMIT_BYTES) { + const message = `${rel(cratePath)} is ${size} bytes, exceeding the crates.io 10 MiB package limit`; + result.skipped.push(message); + if (strict) { + fail(TOOL, message); + } + continue; + } + let packageData; + try { + packageData = cargoMetadataForCrate(cratePath); + } catch (error) { + if (isDefaultCargoTmpCrateArtifact(cratePath) && error.message.includes("does not contain Cargo.toml")) { + result.skipped.push(`ignored malformed Cargo scratch artifact ${rel(cratePath)}`); + continue; + } + result.skipped.push(error.message); + if (strict) { + throw error; + } + continue; + } + if (LEGACY_WASIX_ARTIFACT_CRATES.has(packageData.name)) { + const message = `ignored legacy WASIX artifact crate ${path.basename(cratePath)}`; + result.skipped.push(message); + if (strict) { + fail(TOOL, message); + } + continue; + } + packagesByTargetName.set(`${packageData.name}-${packageData.version}.crate`, [cratePath, packageData]); + } + + const localPackageNames = new Set( + [...packagesByTargetName.values()] + .map(([, packageData]) => packageData.name) + .filter((name) => typeof name === "string"), + ); + const entriesByPath = new Map(); + for (const [targetName, [cratePath, packageData]] of [...packagesByTargetName.entries()].sort((left, right) => compareText(left[0], right[0]))) { + const entry = cargoIndexEntry(cratePath, packageData, localPackageNames); + copyFileSync(cratePath, path.join(cratesDir, targetName)); + const indexPath = crateIndexPath(entry.name); + const entries = entriesByPath.get(indexPath) ?? []; + entries.push(entry); + entriesByPath.set(indexPath, entries); + result.published.push(targetName); + } + + for (const [indexPath, entries] of entriesByPath.entries()) { + const target = path.join(indexDir, indexPath); + mkdirSync(path.dirname(target), { recursive: true }); + writeFileSync( + target, + entries.map((entry) => `${JSON.stringify(entry)}\n`).join(""), + ); + } + + runQuiet(["git", "init"], { cwd: indexDir }); + runQuiet(["git", "config", "user.name", "Oliphaunt Local Registry"], { cwd: indexDir }); + runQuiet(["git", "config", "user.email", "local-registry@oliphaunt.invalid"], { cwd: indexDir }); + runQuiet(["git", "add", "."], { cwd: indexDir }); + runQuiet(["git", "commit", "-m", "local cargo registry"], { cwd: indexDir }); + writeFileSync(configSnippet, [ + "[registries.oliphaunt-local]", + `index = "file://${indexDir}"`, + "", + ].join("\n")); + for (const removed of clearLocalCargoHomeCache(registryRoot)) { + result.staged.push(`cleared ${rel(removed)}`); + } + result.staged.push(rel(indexDir), rel(configSnippet)); + return result; +} + function parsePublishArgs(argv) { const options = { artifactRoots: [], @@ -1065,7 +1396,7 @@ function canPublishInBun(options, roots) { (surface) => surface === "maven" || surface === "swift" || - (surface === "cargo" && options.dryRun) || + (surface === "cargo" && (options.dryRun || !cargoCratesRequirePythonGeneration(options, roots))) || (surface === "npm" && (options.dryRun || !npmTarballsRequirePythonGeneration(roots))), ); } @@ -1085,7 +1416,9 @@ async function publish(argv) { const results = []; for (const surface of options.surfaces) { if (surface === "cargo") { - results.push(publishCargoDryRun(roots, options.strict)); + results.push(options.dryRun + ? publishCargoDryRun(roots, options.strict) + : publishCargoCrates(roots, options.registryRoot, options.strict)); } else if (surface === "npm") { results.push(options.dryRun ? publishNpmDryRun(roots, options.registryRoot, options.strict, options.verdaccioPort) From 24d6b6b965ff84c1ed101276ee49c4d0a319ae50 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 02:02:25 +0000 Subject: [PATCH 248/308] chore: keep cargo local registry staging in bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 13 + tools/policy/check-tooling-stack.sh | 20 +- tools/policy/python-entrypoints.allowlist | 2 +- tools/release/cargo-source-package.mjs | 234 +++++++ tools/release/check_release_metadata.py | 16 +- tools/release/local-registry-publish.mjs | 594 +++++++++++++++++- .../package_oliphaunt_wasix_sdk_crate.mjs | 189 +----- 7 files changed, 881 insertions(+), 187 deletions(-) create mode 100644 tools/release/cargo-source-package.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 31f6a569..d65bb7ee 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -188,6 +188,19 @@ until the current-state gates here are checked with fresh local evidence. local Cargo cache, and emits `report.json`. Release-asset, source-crate, and native-extension Cargo generation paths still use the explicit Python publish fallback until those generators are ported. +- 2026-06-28: Ported local-registry Cargo release-asset and source-crate + staging into `tools/release/local-registry-publish.mjs`. Bun now stages + native runtime plus `oliphaunt-tools` release assets together, stages WASIX + runtime plus `oliphaunt-wasix-tools` artifact crates, packages + `oliphaunt-build`, `oliphaunt`, `oliphaunt-wasix`, and generated native + runtime/tools source manifests through the shared + `tools/release/cargo-source-package.mjs` helper, and prunes unavailable + non-host target artifact dependencies while failing strict mode if host + artifacts are missing. Fresh evidence: a strict native+broker Cargo publish + correctly failed when the WASIX AOT/tools artifact root was absent, and the + same publish passed after adding the WASIX artifact root, producing a local + Cargo index with 219 packages from release-shaped native runtime/tools + assets plus WASIX artifact crates. - 2026-06-27: Ported the WASIX Cargo artifact packager from `tools/release/package_liboliphaunt_wasix_cargo_artifacts.py` to the Bun entrypoint `tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs`. diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index c82da533..90cd5399 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -384,7 +384,23 @@ grep -Fq 'function publishSwift(' tools/release/local-registry-publish.mjs || grep -Fq 'function publishCargoDryRun(' tools/release/local-registry-publish.mjs || fail "local-registry Cargo dry-run publish surface must run in the Bun entrypoint" grep -Fq 'function publishCargoCrates(' tools/release/local-registry-publish.mjs || - fail "local-registry prebuilt Cargo crate publish loop must run in the Bun entrypoint" + fail "local-registry Cargo crate staging and publish loop must run in the Bun entrypoint" +grep -Fq 'function stageReleaseAssetCargoPackages(' tools/release/local-registry-publish.mjs || + fail "local-registry Cargo release-asset crate staging must run in the Bun entrypoint" +grep -Fq 'function stageCargoSourceCrates(' tools/release/local-registry-publish.mjs || + fail "local-registry Cargo source crate staging must run in the Bun entrypoint" +grep -Fq 'function pruneMissingLocalArtifactTargetDependencies(' tools/release/local-registry-publish.mjs || + fail "local-registry Cargo source staging must prune unavailable non-host artifact dependencies" +grep -Fq 'function nativeRuntimeArtifactManifests(' tools/release/local-registry-publish.mjs || + fail "local-registry Cargo source staging must publish generated native runtime and tools source manifests" +grep -Fq 'from "./cargo-source-package.mjs"' tools/release/local-registry-publish.mjs || + fail "local-registry Cargo source staging must use the shared Bun Cargo source packager" +grep -Fq 'from "./package_oliphaunt_wasix_sdk_crate.mjs"' tools/release/local-registry-publish.mjs || + fail "local-registry Cargo source staging must prepare oliphaunt-wasix through the shared Bun packager" +grep -Fq 'export function manualCargoPackageSource(' tools/release/cargo-source-package.mjs || + fail "shared Cargo source package helper must own manual source crate packaging" +grep -Fq 'if (import.meta.main)' tools/release/package_oliphaunt_wasix_sdk_crate.mjs || + fail "WASIX SDK crate packager must be import-safe for local-registry source staging" grep -Fq 'function cargoCratesRequirePythonGeneration(' tools/release/local-registry-publish.mjs || fail "local-registry Cargo publish must keep generation paths on the explicit Python fallback" grep -Fq 'function cargoIndexEntry(' tools/release/local-registry-publish.mjs || @@ -409,7 +425,7 @@ fi grep -Fq 'if (options.help)' tools/release/local-registry-publish.mjs || fail "local-registry publish help must be handled before Python fallback" grep -Fq '(surface === "cargo" && (options.dryRun || !cargoCratesRequirePythonGeneration(options, roots)))' tools/release/local-registry-publish.mjs || - fail "local-registry Cargo real publish must use Bun only for prebuilt crate roots" + fail "local-registry Cargo real publish must use Bun for supported crate, release-asset, and source-staging roots" grep -Fq '["python3", "tools/release/local_registry_publish.py", "publish", ...argv]' tools/release/local-registry-publish.mjs || fail "local-registry real publish generation fallback must stay explicit to the publish command" if grep -Fq '["python3", "tools/release/local_registry_publish.py", ...Bun.argv.slice(2)]' tools/release/local-registry-publish.mjs; then diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index 686a24d3..dc58764c 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -4,5 +4,5 @@ src/extensions/tools/check-extension-model.py extensions defer-extension-model-port generates and validates multi-language extension catalog, SDK metadata, docs, and evidence from one model tools/release/check_consumer_shape.py release-consumer-shape defer-release-graph-port validates cross-SDK package/runtime/install shape from generated release fixtures and source invariants tools/release/check_release_metadata.py release-metadata defer-release-graph-port validates release metadata and publish-step wiring through cached Bun release graph query rows -tools/release/local_registry_publish.py local-registry defer-local-registry-port publishes local Cargo, npm, Maven, and Swift registries from current release artifacts for e2e example validation +tools/release/local_registry_publish.py local-registry defer-local-registry-port remaining fallback for unported local-registry generation paths such as native extension Cargo synthesis and npm package synthesis tools/release/release.py release-orchestrator defer-release-graph-port owns protected release planning, validation, registry checks, publish dry-runs, and publish dispatch diff --git a/tools/release/cargo-source-package.mjs b/tools/release/cargo-source-package.mjs new file mode 100644 index 00000000..c434d1b5 --- /dev/null +++ b/tools/release/cargo-source-package.mjs @@ -0,0 +1,234 @@ +import { spawnSync } from "node:child_process"; +import { gzipSync } from "node:zlib"; +import { + cpSync, + mkdirSync, + readFileSync, + readdirSync, + rmSync, + statSync, + writeFileSync, +} from "node:fs"; +import path from "node:path"; + +export const CARGO_PACKAGE_SIZE_LIMIT_BYTES = 10 * 1024 * 1024; + +export function compareText(left, right) { + return left < right ? -1 : left > right ? 1 : 0; +} + +function abort(fail, message) { + if (typeof fail === "function") { + fail(message); + } + throw new Error(message); +} + +export function parseCargoPackageNameVersion(text, context, { fail = null } = {}) { + 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) { + abort(fail, `${context} must declare package.name and package.version`); + } + return { name, version }; +} + +export function readCargoPackageNameVersion(manifest, { fail = null, rel = String } = {}) { + return parseCargoPackageNameVersion(readFileSync(manifest, "utf8"), rel(manifest), { fail }); +} + +export 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 cargoMetadataPackageFromManifest(manifest, { root, fail, rel }) { + const result = spawnSync("cargo", [ + "metadata", + "--manifest-path", + manifest, + "--format-version", + "1", + "--no-deps", + ], { + cwd: root, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.error !== undefined) { + abort(fail, `cargo failed to start: ${result.error.message}`); + } + if (result.status !== 0) { + abort(fail, `cargo metadata failed for ${rel(manifest)}: ${result.stderr.trim()}`); + } + let data; + try { + data = JSON.parse(result.stdout); + } catch (error) { + abort(fail, `cargo metadata for ${rel(manifest)} did not return valid JSON: ${error.message}`); + } + const packages = data.packages; + if (!Array.isArray(packages) || packages.length !== 1 || typeof packages[0] !== "object") { + abort(fail, `cargo metadata for ${rel(manifest)} did not return exactly one package`); + } + return packages[0]; +} + +function copySourceTree(source, destination, ignoredNames) { + rmSync(destination, { recursive: true, force: true }); + mkdirSync(path.dirname(destination), { recursive: true }); + cpSync(source, destination, { + recursive: true, + filter: (sourcePath) => !ignoredNames.has(path.basename(sourcePath)), + }); +} + +function listFilesRecursive(directory) { + const files = []; + const entries = readdirSync(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(...listFilesRecursive(fullPath)); + } else if (entry.isFile() || entry.isSymbolicLink()) { + files.push(fullPath); + } + } + return files; +} + +function tarPathParts(relativePath, { fail }) { + 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 }; + } + } + abort(fail, `crate archive path is too long for ustar: ${normalized}`); +} + +function writeString(buffer, offset, length, value, { fail }) { + const bytes = Buffer.from(value); + if (bytes.length > length) { + abort(fail, `tar header field overflow for '${value}'`); + } + bytes.copy(buffer, offset); +} + +function writeOctal(buffer, offset, length, value, options) { + const text = value.toString(8); + if (text.length > length - 1) { + abort(options.fail, `tar header octal field overflow for '${value}'`); + } + writeString(buffer, offset, length, `${text.padStart(length - 1, "0")}\0`, options); +} + +function tarHeader(relativePath, size, mode, options) { + const header = Buffer.alloc(512, 0); + const { name, prefix } = tarPathParts(relativePath, options); + writeString(header, 0, 100, name, options); + writeOctal(header, 100, 8, mode, options); + writeOctal(header, 108, 8, 0, options); + writeOctal(header, 116, 8, 0, options); + writeOctal(header, 124, 12, size, options); + writeOctal(header, 136, 12, 0, options); + header.fill(0x20, 148, 156); + writeString(header, 156, 1, "0", options); + writeString(header, 257, 6, "ustar\0", options); + writeString(header, 263, 2, "00", options); + writeString(header, 345, 155, prefix, options); + let checksum = 0; + for (const byte of header) { + checksum += byte; + } + const checksumText = checksum.toString(8); + if (checksumText.length > 6) { + abort(options.fail, `tar header checksum overflow for ${relativePath}`); + } + writeString(header, 148, 8, `${checksumText.padStart(6, "0")}\0 `, options); + return header; +} + +function createTar(stageDir, packageRoot, options) { + const chunks = []; + const files = 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 stats = statSync(file); + const data = readFileSync(file); + chunks.push(tarHeader(archivePath, data.length, stats.mode & 0o777, options)); + 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); +} + +export function manualCargoPackageSource( + manifest, + outputDir, + { + root, + fail = null, + rel = String, + packageSizeLimitBytes = CARGO_PACKAGE_SIZE_LIMIT_BYTES, + }, +) { + const { name, version } = readCargoPackageNameVersion(manifest, { fail, rel }); + 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`); + copySourceTree(sourceDir, stageDir, new Set(["target", ".git", ".DS_Store"])); + + const stagedManifest = path.join(stageDir, "Cargo.toml"); + writeFileSync(stagedManifest, packagedCargoManifestText(readFileSync(stagedManifest, "utf8"))); + const packageMetadata = cargoMetadataPackageFromManifest(stagedManifest, { root, fail, rel }); + if (packageMetadata.name !== name || packageMetadata.version !== version) { + abort(fail, `${rel(stagedManifest)} produced unexpected cargo metadata`); + } + + mkdirSync(outputDir, { recursive: true }); + rmSync(cratePath, { force: true }); + writeFileSync(cratePath, gzipSync(createTar(stageDir, packageRoot, { fail }), { mtime: 0 })); + const size = statSync(cratePath).size; + if (size > packageSizeLimitBytes) { + abort(fail, `${rel(cratePath)} is ${size} bytes, above the crates.io 10 MiB package limit`); + } + return cratePath; +} diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index dc896a16..8b84669a 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -641,6 +641,8 @@ def validate_graph_files() -> None: release_verify = read_text("tools/release/release-verify.mjs") prepare_rust_release_source = read_text("tools/release/prepare-rust-release-source.mjs") local_registry_publish = read_text("tools/release/local-registry-publish.mjs") + cargo_source_package = read_text("tools/release/cargo-source-package.mjs") + wasix_sdk_packager = read_text("tools/release/package_oliphaunt_wasix_sdk_crate.mjs") release_pr_coverage = read_text("tools/release/check_release_pr_coverage.mjs") build_extension_ci_artifacts = read_text("tools/release/build-extension-ci-artifacts.mjs") check_staged_artifacts = read_text("tools/release/check-staged-artifacts.mjs") @@ -757,6 +759,18 @@ def validate_graph_files() -> None: or "function download(argv)" not in local_registry_publish or "function publishCargoDryRun(" not in local_registry_publish or "function publishCargoCrates(" not in local_registry_publish + or "function stageReleaseAssetCargoPackages(" not in local_registry_publish + or "function stageCargoSourceCrates(" not in local_registry_publish + or "function pruneMissingLocalArtifactTargetDependencies(" not in local_registry_publish + or "function nativeRuntimeArtifactManifests(" not in local_registry_publish + or "nativeSplitReleaseAssetNames(" not in local_registry_publish + or 'from "./cargo-source-package.mjs"' not in local_registry_publish + or 'from "./package_oliphaunt_wasix_sdk_crate.mjs"' not in local_registry_publish + or "export function manualCargoPackageSource(" not in cargo_source_package + or "gzipSync(createTar(" not in cargo_source_package + or "export async function prepareOliphauntWasixReleaseSource(" not in wasix_sdk_packager + or "export async function currentOliphauntWasixSdkVersion(" not in wasix_sdk_packager + or "if (import.meta.main)" not in wasix_sdk_packager or "function cargoCratesRequirePythonGeneration(" not in local_registry_publish or "function cargoMetadataForCrate(" not in local_registry_publish or "function cargoIndexEntry(" not in local_registry_publish @@ -781,7 +795,7 @@ def validate_graph_files() -> None: or "python3 tools/release/local_registry_publish.py" in examples_readme or "tools/dev/bun.sh tools/release/local-registry-publish.mjs" not in examples_local_registries ): - fail("example local-registry setup must use the Bun local-registry command surface") + fail("example local-registry setup must use the Bun local-registry command surface and stage Cargo release/source crates") if ( "publish-step-target-coverage [--product PRODUCT]" not in release_graph_query or "export function publishStepTargetCoverageRows(" not in release_graph_source diff --git a/tools/release/local-registry-publish.mjs b/tools/release/local-registry-publish.mjs index 20f39dce..63d5dcb4 100644 --- a/tools/release/local-registry-publish.mjs +++ b/tools/release/local-registry-publish.mjs @@ -18,7 +18,19 @@ import { } from "node:fs"; import os from "node:os"; import path from "node:path"; +import { + manualCargoPackageSource, + readCargoPackageNameVersion, +} from "./cargo-source-package.mjs"; +import { + allArtifactTargets, + currentProductVersionSync, +} from "./release-artifact-targets.mjs"; import { fail, ROOT, run } from "./release-cli-utils.mjs"; +import { + currentOliphauntWasixSdkVersion, + prepareOliphauntWasixReleaseSource, +} from "./package_oliphaunt_wasix_sdk_crate.mjs"; const TOOL = "local-registry-publish.mjs"; const DEFAULT_RUN_ID = "28049923289"; @@ -523,6 +535,196 @@ function hostNpmTarget() { return hostCargoReleaseTarget(); } +function localFail(message) { + fail(TOOL, message); +} + +function isFile(file) { + try { + return statSync(file).isFile(); + } catch { + return false; + } +} + +function isDirectory(file) { + try { + return statSync(file).isDirectory(); + } catch { + return false; + } +} + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); +} + +function globPatternMatches(name, pattern) { + return new RegExp(`^${escapeRegExp(pattern).replaceAll("\\*", ".*")}$`, "u").test(name); +} + +function releaseAssetCandidate(root, name, destination) { + const destinationResolved = path.resolve(destination); + if (isFile(root) && path.basename(root) === name) { + return root; + } + if (!isDirectory(root)) { + return null; + } + const candidates = walkFiles(root) + .filter((file) => path.basename(file) === name && !pathIsUnder(file, destinationResolved)) + .sort(compareText); + if (candidates.length === 0) { + return null; + } + const selected = candidates[0]; + for (const candidate of candidates.slice(1)) { + if (sha256File(candidate) !== sha256File(selected)) { + throw new Error(`conflicting release asset ${name} within ${rel(root)}: ${rel(selected)} and ${rel(candidate)} differ`); + } + } + return selected; +} + +function copyReleaseAssetSet(roots, destination, names) { + for (const root of roots) { + const selected = []; + for (const name of names) { + const candidate = releaseAssetCandidate(root, name, destination); + if (candidate === null) { + break; + } + selected.push(candidate); + } + if (selected.length !== names.length) { + continue; + } + rmSync(destination, { recursive: true, force: true }); + mkdirSync(destination, { recursive: true }); + const copied = []; + for (const source of selected) { + const target = path.join(destination, path.basename(source)); + copyFileSync(source, target); + copied.push(target); + } + return copied; + } + return []; +} + +function copyReleaseAssets(roots, destination, patterns) { + const selected = new Map(); + const destinationResolved = path.resolve(destination); + for (const root of roots) { + if (!isDirectory(root)) { + continue; + } + const rootCandidates = walkFiles(root) + .filter((file) => + patterns.some((pattern) => globPatternMatches(path.basename(file), pattern)) && + !pathIsUnder(file, destinationResolved)) + .sort(compareText); + for (const file of rootCandidates) { + const existing = selected.get(path.basename(file)); + if (existing === undefined) { + selected.set(path.basename(file), [file, root]); + continue; + } + const [existingFile, existingRoot] = existing; + if (path.resolve(existingRoot) !== path.resolve(root)) { + continue; + } + if (sha256File(existingFile) !== sha256File(file)) { + throw new Error(`conflicting release asset ${path.basename(file)} within ${rel(root)}: ${rel(existingFile)} and ${rel(file)} differ`); + } + } + } + if (selected.size === 0) { + return []; + } + rmSync(destination, { recursive: true, force: true }); + mkdirSync(destination, { recursive: true }); + const copied = []; + for (const [source] of [...selected.values()].sort((left, right) => compareText(path.basename(left[0]), path.basename(right[0])))) { + const target = path.join(destination, path.basename(source)); + copyFileSync(source, target); + copied.push(target); + } + return copied; +} + +function releaseAssetDirSelected(roots, assetDir) { + const resolved = path.resolve(assetDir); + return roots.some((root) => path.resolve(root) === resolved); +} + +function releaseAssetDirHasFiles(assetDir, patterns) { + if (!isDirectory(assetDir)) { + return false; + } + return walkFiles(assetDir).some((file) => patterns.some((pattern) => globPatternMatches(path.basename(file), pattern))); +} + +function releaseAssetDirHasExactFiles(assetDir, names) { + return isDirectory(assetDir) && names.every((name) => isFile(path.join(assetDir, name))); +} + +function missingReleaseAssetNames(assetDir, names) { + return names.filter((name) => !isFile(path.join(assetDir, name))); +} + +function nativeReleaseAssetName(version, targetId, kind) { + const matches = allArtifactTargets({ + product: "liboliphaunt-native", + kind, + publishedOnly: true, + }, TOOL) + .filter((target) => + target.target === targetId && + (target.surfaces.includes("rust-native-direct") || target.surfaces.includes("typescript-native-direct"))) + .map((target) => target.asset.replaceAll("{version}", version)); + if (matches.length !== 1) { + fail(TOOL, `expected exactly one published liboliphaunt-native ${kind} asset for ${targetId}, got ${JSON.stringify(matches)}`); + } + return matches[0]; +} + +function nativeSplitReleaseAssetNames(version, targetId) { + return [ + nativeReleaseAssetName(version, targetId, "native-runtime"), + nativeReleaseAssetName(version, targetId, "native-tools"), + ]; +} + +function nativeSplitReleaseAssetsReady(assetDir, version, targetId) { + const required = nativeSplitReleaseAssetNames(version, targetId); + return { + ready: releaseAssetDirHasExactFiles(assetDir, required), + missing: missingReleaseAssetNames(assetDir, required), + }; +} + +function nativeSplitReleaseAssetMissingMessage(assetDir, version, targetId, missing) { + const required = nativeSplitReleaseAssetNames(version, targetId).join(", "); + return `native split release asset staging for ${targetId} requires runtime and tools assets (${required}) under ${rel(assetDir)}; missing ${missing.join(", ")}`; +} + +function cargoTargetTriple(targetId) { + if (targetId === "linux-x64-gnu") { + return "x86_64-unknown-linux-gnu"; + } + if (targetId === "linux-arm64-gnu") { + return "aarch64-unknown-linux-gnu"; + } + if (targetId === "macos-arm64") { + return "aarch64-apple-darwin"; + } + if (targetId === "windows-x64-msvc") { + return "x86_64-pc-windows-msvc"; + } + return null; +} + function extensionNpmPackage(sqlName) { return `@oliphaunt/extension-${sqlName.replaceAll("_", "-")}`; } @@ -995,16 +1197,375 @@ function publishCargoDryRun(roots, strict) { return result; } -function cargoCratesRequirePythonGeneration(options, roots) { - if (options.artifactRoots.length === 0) { +function stageReleaseAssetCargoPackages(roots, registryRoot, result, strict) { + const outputRoot = path.join(registryRoot, "cargo-generated", "release-asset-crates"); + rmSync(outputRoot, { recursive: true, force: true }); + mkdirSync(outputRoot, { recursive: true }); + const generatedRoots = []; + const hostTarget = hostCargoReleaseTarget(); + + const libVersion = currentProductVersionSync("liboliphaunt-native", TOOL); + const libAssetDir = path.join(ROOT, "target/liboliphaunt/release-assets"); + const copiedLibAssets = hostTarget === null + ? [] + : copyReleaseAssetSet(roots, libAssetDir, nativeSplitReleaseAssetNames(libVersion, hostTarget)); + const libOutputDir = path.join(outputRoot, "liboliphaunt-native"); + if (hostTarget === null) { + result.skipped.push("current host does not map to a supported native runtime Cargo target"); + } else if ( + copiedLibAssets.length > 0 || + (releaseAssetDirSelected(roots, libAssetDir) && releaseAssetDirHasFiles(libAssetDir, [ + `liboliphaunt-${libVersion}-*`, + `oliphaunt-tools-${libVersion}-*`, + ])) + ) { + const { ready, missing } = nativeSplitReleaseAssetsReady(libAssetDir, libVersion, hostTarget); + if (!ready) { + const message = nativeSplitReleaseAssetMissingMessage(libAssetDir, libVersion, hostTarget, missing); + result.skipped.push(message); + if (strict) { + fail(TOOL, message); + } + } else { + if (copiedLibAssets.length > 0) { + result.staged.push(`staged ${copiedLibAssets.length} liboliphaunt release asset(s) for Cargo`); + } + runQuiet([ + "tools/dev/bun.sh", + "tools/release/package-liboliphaunt-cargo-artifacts.mjs", + "--version", + libVersion, + "--output-dir", + libOutputDir, + "--target", + hostTarget, + ]); + generatedRoots.push(libOutputDir); + } + } else { + result.skipped.push("no liboliphaunt release assets found for native Cargo artifact packages"); + } + + const brokerVersion = currentProductVersionSync("oliphaunt-broker", TOOL); + const brokerAssetDir = path.join(ROOT, "target/oliphaunt-broker/release-assets"); + const copiedBrokerAssets = copyReleaseAssets(roots, brokerAssetDir, [ + "oliphaunt-broker-*.tar.gz", + "oliphaunt-broker-*.zip", + ]); + const brokerOutputDir = path.join(outputRoot, "oliphaunt-broker"); + if (hostTarget === null) { + result.skipped.push("current host does not map to a supported broker Cargo target"); + } else if ( + copiedBrokerAssets.length > 0 || + (releaseAssetDirSelected(roots, brokerAssetDir) && releaseAssetDirHasFiles(brokerAssetDir, [ + "oliphaunt-broker-*.tar.gz", + "oliphaunt-broker-*.zip", + ])) + ) { + if (copiedBrokerAssets.length > 0) { + result.staged.push(`staged ${copiedBrokerAssets.length} broker release asset(s) for Cargo`); + } + runQuiet([ + "tools/dev/bun.sh", + "tools/release/package_broker_cargo_artifacts.mjs", + "--version", + brokerVersion, + "--output-dir", + brokerOutputDir, + "--target", + hostTarget, + ]); + generatedRoots.push(brokerOutputDir); + } else { + result.skipped.push("no broker release assets found for broker Cargo artifact packages"); + } + + const wasixVersion = currentProductVersionSync("liboliphaunt-wasix", TOOL); + const wasixAssetDir = path.join(ROOT, "target/oliphaunt-wasix/release-assets"); + const copiedWasixAssets = copyReleaseAssets(roots, wasixAssetDir, [`liboliphaunt-wasix-${wasixVersion}-*`]); + const wasixOutputDir = path.join(outputRoot, "liboliphaunt-wasix"); + if ( + copiedWasixAssets.length > 0 || + (releaseAssetDirSelected(roots, wasixAssetDir) && releaseAssetDirHasFiles(wasixAssetDir, [ + `liboliphaunt-wasix-${wasixVersion}-*`, + ])) + ) { + if (copiedWasixAssets.length > 0) { + result.staged.push(`staged ${copiedWasixAssets.length} WASIX release asset(s) for Cargo`); + } + runQuiet([ + "tools/dev/bun.sh", + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", + "--version", + wasixVersion, + "--output-dir", + wasixOutputDir, + ]); + generatedRoots.push(wasixOutputDir); + } else { + result.skipped.push("no WASIX release assets found for WASIX Cargo artifact packages"); + } + + const generatedCrates = discoverFiles(generatedRoots, [".crate"]); + if (generatedCrates.length > 0) { + result.staged.push(`generated ${generatedCrates.length} release-asset Cargo crate(s)`); + } + return generatedRoots; +} + +function cargoPackageNameFromCrate(cratePath) { + const members = tryCommandOutput(["tar", "-tzf", cratePath]); + if (members === null) { + return null; + } + const manifest = members + .split(/\r?\n/u) + .filter(Boolean) + .find((member) => member.split("/").length === 2 && member.endsWith("/Cargo.toml")); + if (manifest === undefined) { + return null; + } + const text = tryCommandOutput(["tar", "-xOzf", cratePath, manifest]); + if (text === null) { + return null; + } + try { + const packageData = Bun.TOML.parse(text)?.package; + return typeof packageData?.name === "string" && packageData.name ? packageData.name : null; + } catch { + return null; + } +} + +function cargoPackageNamesFromRoots(roots) { + const names = new Set(); + for (const cratePath of discoverFiles(roots, [".crate"])) { + const name = cargoPackageNameFromCrate(cratePath); + if (name !== null) { + names.add(name); + } + } + return names; +} + +function cargoDependencyNameMatchesHostTarget(name) { + const hostTarget = hostCargoReleaseTarget(); + if (hostTarget === null) { return true; } - if (discoverFiles(roots, [".crate"]).length === 0) { + const hostTriple = cargoTargetTriple(hostTarget); + const hostMarkers = hostTriple === null ? [hostTarget] : [hostTarget, hostTriple]; + return hostMarkers.some((marker) => + name.endsWith(`-${marker}`) || + name.includes(`-${marker}-`) || + name.includes(`-aot-${marker}`)); +} + +function pruneMissingFeatureDependencies(text, missingPackageNames) { + if (missingPackageNames.size === 0) { + return text; + } + const lines = text.split(/\r?\n/u); + const output = []; + let inFeatures = false; + let index = 0; + while (index < lines.length) { + const line = lines[index]; + if (/^\[features\]$/u.test(line)) { + inFeatures = true; + output.push(line); + index += 1; + continue; + } + if (line.startsWith("[") && !line.startsWith("[[")) { + inFeatures = false; + output.push(line); + index += 1; + continue; + } + if (!inFeatures) { + output.push(line); + index += 1; + continue; + } + const match = line.match(/^([A-Za-z0-9_-]+)\s*=/u); + if (match === null) { + output.push(line); + index += 1; + continue; + } + const featureName = match[1]; + const block = [line]; + index += 1; + let bracketDepth = [...line].filter((char) => char === "[").length - [...line].filter((char) => char === "]").length; + while (bracketDepth > 0 && index < lines.length) { + block.push(lines[index]); + bracketDepth += [...lines[index]].filter((char) => char === "[").length - [...lines[index]].filter((char) => char === "]").length; + index += 1; + } + let values; + try { + values = Bun.TOML.parse(`[features]\n${block.join("\n")}\n`).features?.[featureName]; + } catch { + output.push(...block); + continue; + } + if (!Array.isArray(values) || !values.every((value) => typeof value === "string")) { + output.push(...block); + continue; + } + const filtered = values.filter((value) => !(value.startsWith("dep:") && missingPackageNames.has(value.slice("dep:".length)))); + if (filtered.length === values.length) { + output.push(...block); + continue; + } + output.push(`${featureName} = [${filtered.map((value) => JSON.stringify(value)).join(", ")}]`); + } + return `${output.join("\n").trimEnd()}\n`; +} + +function pruneMissingLocalArtifactTargetDependencies(manifest, availablePackageNames, result, strict) { + const lines = readFileSync(manifest, "utf8").split(/\r?\n/u); + const output = []; + const removed = []; + let index = 0; + while (index < lines.length) { + const line = lines[index]; + if (!/^\[target\..*\.dependencies\]$/u.test(line)) { + output.push(line); + index += 1; + continue; + } + const block = [line]; + index += 1; + while (index < lines.length && !/^\[[^\]]+\]$/u.test(lines[index])) { + block.push(lines[index]); + index += 1; + } + const dependencyNames = []; + for (const blockLine of block.slice(1)) { + const match = blockLine.match(/^([A-Za-z0-9_-]+)\s*=/u); + if (match !== null) { + dependencyNames.push(match[1]); + } + } + const missing = dependencyNames.filter((name) => !availablePackageNames.has(name)).sort(compareText); + if (missing.length > 0) { + removed.push([line, missing]); + while (output.at(-1) === "") { + output.pop(); + } + continue; + } + if (output.length > 0 && output.at(-1) !== "") { + output.push(""); + } + output.push(...block); + } + if (removed.length === 0) { + return; + } + const missingPackages = new Set(removed.flatMap(([, missing]) => missing)); + if (strict) { + const hostMissingPackages = [...missingPackages] + .filter((name) => cargoDependencyNameMatchesHostTarget(name)) + .sort(compareText); + if (hostMissingPackages.length > 0) { + throw new Error(`${rel(manifest)} is missing local registry inputs for host target artifact dependencies: ${hostMissingPackages.join(", ")}`); + } + } + const pruned = pruneMissingFeatureDependencies(`${output.join("\n").trimEnd()}\n`, missingPackages); + writeFileSync(manifest, pruned); + for (const [header, missing] of removed) { + result.skipped.push(`${rel(manifest)} pruned ${header} because local registry inputs are missing ${missing.join(", ")}`); + } +} + +function nativeRuntimeArtifactManifests(sourceRoot, { includeParts = false } = {}) { + if (!isDirectory(sourceRoot)) { + return []; + } + const manifests = []; + const toolsFacade = path.join(sourceRoot, "oliphaunt-tools", "Cargo.toml"); + if (isFile(toolsFacade)) { + manifests.push(toolsFacade); + } + for (const entry of readdirSync(sourceRoot, { withFileTypes: true }).sort((left, right) => compareText(left.name, right.name))) { + if (!entry.isDirectory()) { + continue; + } + if (!entry.name.startsWith("liboliphaunt-native-") && !entry.name.startsWith("oliphaunt-tools-")) { + continue; + } + const manifest = path.join(sourceRoot, entry.name, "Cargo.toml"); + if (isFile(manifest)) { + manifests.push(manifest); + } + } + const seen = new Set(); + const result = []; + for (const manifest of manifests.sort(compareText)) { + if (seen.has(manifest)) { + continue; + } + seen.add(manifest); + const { name } = readCargoPackageNameVersion(manifest, { fail: localFail, rel }); + if (name.includes("-part-") && !includeParts) { + continue; + } + result.push(manifest); + } + return result; +} + +async function stageCargoSourceCrates(roots, registryRoot, result, strict) { + const outputDir = path.join(registryRoot, "cargo-generated", "source-crates"); + rmSync(outputDir, { recursive: true, force: true }); + mkdirSync(outputDir, { recursive: true }); + + const generated = []; + const packageOptions = { root: ROOT, fail: localFail, rel }; + const buildManifest = path.join(ROOT, "src/sdks/rust/crates/oliphaunt-build/Cargo.toml"); + generated.push(manualCargoPackageSource(buildManifest, outputDir, packageOptions)); + + const preparedRustSource = commandOutput([ + "tools/dev/bun.sh", + "tools/release/prepare-rust-release-source.mjs", + ]).trim().split(/\r?\n/u).filter(Boolean).at(-1); + if (preparedRustSource === undefined) { + fail(TOOL, "prepare-rust-release-source.mjs did not print a generated Cargo.toml path"); + } + const oliphauntManifest = path.resolve(ROOT, preparedRustSource); + const availablePackageNames = cargoPackageNamesFromRoots(roots); + const nativeSourceRoot = path.join(ROOT, "target/liboliphaunt/cargo-package-sources"); + const nativeRuntimePublicManifests = nativeRuntimeArtifactManifests(nativeSourceRoot); + const nativeRuntimeAllManifests = nativeRuntimeArtifactManifests(nativeSourceRoot, { includeParts: true }); + for (const manifest of nativeRuntimePublicManifests) { + availablePackageNames.add(readCargoPackageNameVersion(manifest, { fail: localFail, rel }).name); + } + pruneMissingLocalArtifactTargetDependencies(oliphauntManifest, availablePackageNames, result, strict); + generated.push(manualCargoPackageSource(oliphauntManifest, outputDir, packageOptions)); + + const wasixManifest = await prepareOliphauntWasixReleaseSource(await currentOliphauntWasixSdkVersion()); + pruneMissingLocalArtifactTargetDependencies(wasixManifest, availablePackageNames, result, strict); + generated.push(manualCargoPackageSource(wasixManifest, outputDir, packageOptions)); + + for (const manifest of nativeRuntimeAllManifests) { + generated.push(manualCargoPackageSource(manifest, outputDir, packageOptions)); + } + + result.staged.push(...generated.map(rel)); + return generated; +} + +function cargoCratesRequirePythonGeneration(options, roots) { + if (options.artifactRoots.length === 0) { return true; } if (discoverExtensionManifests(roots).length > 0) { return true; } + let hasCargoInput = discoverFiles(roots, [".crate"]).length > 0; for (const root of roots) { const stats = statSync(root); const rootName = path.basename(root); @@ -1016,7 +1577,8 @@ function cargoCratesRequirePythonGeneration(options, roots) { /^liboliphaunt-wasix-[^/]+\.tar\.zst$/u.test(rootName) ) ) { - return true; + hasCargoInput = true; + continue; } if (!stats.isDirectory()) { continue; @@ -1028,11 +1590,12 @@ function cargoCratesRequirePythonGeneration(options, roots) { /^oliphaunt-broker-[^/]+\.(tar\.gz|zip)$/u.test(name) || /^liboliphaunt-wasix-[^/]+\.tar\.zst$/u.test(name) ) { - return true; + hasCargoInput = true; + break; } } } - return false; + return !hasCargoInput; } function cargoCratePriority(cratePath, registryRoot) { @@ -1208,9 +1771,22 @@ function clearLocalCargoHomeCache(registryRoot) { return removed; } -function publishCargoCrates(roots, registryRoot, strict) { +async function publishCargoCrates(roots, registryRoot, strict) { const result = surfaceResult("cargo"); - result.staged.push("prebuilt .crate Cargo publish handled by Bun"); + const releaseAssetRoots = stageReleaseAssetCargoPackages(roots, registryRoot, result, strict); + if (releaseAssetRoots.length > 0) { + roots = [...roots, ...releaseAssetRoots]; + } + const generatedRoots = await stageCargoSourceCrates(roots, registryRoot, result, strict); + if (generatedRoots.length > 0) { + roots = [...roots, ...generatedRoots]; + } + const extensionTarget = hostCargoReleaseTarget(); + if (extensionTarget === null) { + result.skipped.push("current host does not map to a supported native extension Cargo target"); + } else { + result.skipped.push("no extension-artifacts.json manifests found for native extension Cargo crates"); + } const crates = discoverFiles(roots, [".crate"]); if (crates.length === 0) { addSkip(result, "no .crate artifacts found", strict); @@ -1418,7 +1994,7 @@ async function publish(argv) { if (surface === "cargo") { results.push(options.dryRun ? publishCargoDryRun(roots, options.strict) - : publishCargoCrates(roots, options.registryRoot, options.strict)); + : await publishCargoCrates(roots, options.registryRoot, options.strict)); } else if (surface === "npm") { results.push(options.dryRun ? publishNpmDryRun(roots, options.registryRoot, options.strict, options.verdaccioPort) diff --git a/tools/release/package_oliphaunt_wasix_sdk_crate.mjs b/tools/release/package_oliphaunt_wasix_sdk_crate.mjs index b814fca5..12bc5d72 100755 --- a/tools/release/package_oliphaunt_wasix_sdk_crate.mjs +++ b/tools/release/package_oliphaunt_wasix_sdk_crate.mjs @@ -1,11 +1,15 @@ #!/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'; +import { + compareText, + manualCargoPackageSource, + packagedCargoManifestText, +} from './cargo-source-package.mjs'; + 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}`); @@ -48,11 +52,7 @@ function parseCargoPackageNameVersion(text, context) { return { name, version }; } -async function readCargoPackageNameVersion(manifest) { - return parseCargoPackageNameVersion(await fs.readFile(manifest, 'utf8'), rel(manifest)); -} - -async function currentOliphauntWasixSdkVersion() { +export async function currentOliphauntWasixSdkVersion() { const text = await readText('src/bindings/wasix-rust/crates/oliphaunt-wasix/Cargo.toml'); return parseCargoPackageNameVersion( text, @@ -85,21 +85,6 @@ 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) { @@ -142,7 +127,7 @@ async function copySourceTree(source, destination, ignoredNames) { }); } -async function prepareOliphauntWasixReleaseSource(version) { +export async function prepareOliphauntWasixReleaseSource(version) { const runtimeVersion = await currentLiboliphauntWasixVersion(); const registryPackages = await wasixCargoRegistryPackages(); const sourceDir = path.join(root, 'src/bindings/wasix-rust/crates/oliphaunt-wasix'); @@ -167,152 +152,6 @@ async function prepareOliphauntWasixReleaseSource(version) { 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) { @@ -332,8 +171,10 @@ function parseArgs(argv) { }; } -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)); +if (import.meta.main) { + const { outputDir } = parseArgs(Bun.argv.slice(2)); + const version = await currentOliphauntWasixSdkVersion(); + const manifest = await prepareOliphauntWasixReleaseSource(version); + const cratePath = manualCargoPackageSource(manifest, outputDir, { root, fail, rel }); + console.log(rel(cratePath)); +} From 11c489d4c0431c3769dd8eab869293e040df6f5d Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 02:19:13 +0000 Subject: [PATCH 249/308] chore: keep npm release asset staging in bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 7 + tools/policy/check-tooling-stack.sh | 18 +- tools/policy/python-entrypoints.allowlist | 2 +- tools/release/check_release_metadata.py | 10 +- tools/release/local-registry-publish.mjs | 648 +++++++++++++++++- 5 files changed, 659 insertions(+), 26 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index d65bb7ee..3b3d0d79 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -201,6 +201,13 @@ until the current-state gates here are checked with fresh local evidence. same publish passed after adding the WASIX artifact root, producing a local Cargo index with 219 packages from release-shaped native runtime/tools assets plus WASIX artifact crates. +- 2026-06-28: Ported local-registry npm release-asset package staging into + `tools/release/local-registry-publish.mjs`. Bun now stages native + liboliphaunt runtime packages, split `oliphaunt-tools` packages, native ICU, + and broker helper packages from release assets, validates runtime/tool payload + membership through the shared native optimizer policy, prefers generated + tarballs over stale artifact roots, and leaves only native extension npm + synthesis on the explicit Python fallback. - 2026-06-27: Ported the WASIX Cargo artifact packager from `tools/release/package_liboliphaunt_wasix_cargo_artifacts.py` to the Bun entrypoint `tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs`. diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 90cd5399..f29a300b 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -393,6 +393,8 @@ grep -Fq 'function pruneMissingLocalArtifactTargetDependencies(' tools/release/l fail "local-registry Cargo source staging must prune unavailable non-host artifact dependencies" grep -Fq 'function nativeRuntimeArtifactManifests(' tools/release/local-registry-publish.mjs || fail "local-registry Cargo source staging must publish generated native runtime and tools source manifests" +grep -Fq 'from "./optimize_native_runtime_payload.mjs"' tools/release/local-registry-publish.mjs || + fail "local-registry npm release-asset staging must validate native runtime/tools payload splits through the shared optimizer policy" grep -Fq 'from "./cargo-source-package.mjs"' tools/release/local-registry-publish.mjs || fail "local-registry Cargo source staging must use the shared Bun Cargo source packager" grep -Fq 'from "./package_oliphaunt_wasix_sdk_crate.mjs"' tools/release/local-registry-publish.mjs || @@ -426,15 +428,27 @@ grep -Fq 'if (options.help)' tools/release/local-registry-publish.mjs || fail "local-registry publish help must be handled before Python fallback" grep -Fq '(surface === "cargo" && (options.dryRun || !cargoCratesRequirePythonGeneration(options, roots)))' tools/release/local-registry-publish.mjs || fail "local-registry Cargo real publish must use Bun for supported crate, release-asset, and source-staging roots" +grep -Fq '(surface === "npm" && (options.dryRun || !npmTarballsRequirePythonGeneration(roots)))' tools/release/local-registry-publish.mjs || + fail "local-registry npm real publish must use Bun for supported tarball and release-asset package roots" grep -Fq '["python3", "tools/release/local_registry_publish.py", "publish", ...argv]' tools/release/local-registry-publish.mjs || fail "local-registry real publish generation fallback must stay explicit to the publish command" if grep -Fq '["python3", "tools/release/local_registry_publish.py", ...Bun.argv.slice(2)]' tools/release/local-registry-publish.mjs; then fail "local-registry command dispatch must not use a generic Python fallback" fi grep -Fq 'async function publishNpmTarballs(' tools/release/local-registry-publish.mjs || - fail "local-registry prebuilt npm tarball publish loop must run in the Bun entrypoint" + fail "local-registry npm tarball/release-asset publish loop must run in the Bun entrypoint" +grep -Fq 'function stageReleaseAssetNpmPackages(' tools/release/local-registry-publish.mjs || + fail "local-registry npm release-asset package staging must run in the Bun entrypoint" +grep -Fq 'function liboliphauntNpmTarballs(' tools/release/local-registry-publish.mjs || + fail "local-registry native runtime/tools npm package generation must run in the Bun entrypoint" +grep -Fq 'function stageLiboliphauntToolsNpmPayloads(' tools/release/local-registry-publish.mjs || + fail "local-registry split native tools npm payload staging must run in the Bun entrypoint" +grep -Fq 'function stageLiboliphauntIcuNpmPayload(' tools/release/local-registry-publish.mjs || + fail "local-registry native ICU npm payload staging must run in the Bun entrypoint" +grep -Fq 'function brokerNpmTarballs(' tools/release/local-registry-publish.mjs || + fail "local-registry broker npm package generation must run in the Bun entrypoint" grep -Fq 'async function ensureVerdaccio(' tools/release/local-registry-publish.mjs || - fail "local-registry Verdaccio orchestration must run in the Bun entrypoint for prebuilt npm tarballs" + fail "local-registry Verdaccio orchestration must run in the Bun entrypoint for npm tarballs" grep -Fq 'function selectNpmTarballs(' tools/release/local-registry-publish.mjs || fail "local-registry npm dry-run tarball selection must run in the Bun entrypoint" if grep -Fq 'python3 tools/release/local_registry_publish.py' examples/README.md; then diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index dc58764c..94989aa7 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -4,5 +4,5 @@ src/extensions/tools/check-extension-model.py extensions defer-extension-model-port generates and validates multi-language extension catalog, SDK metadata, docs, and evidence from one model tools/release/check_consumer_shape.py release-consumer-shape defer-release-graph-port validates cross-SDK package/runtime/install shape from generated release fixtures and source invariants tools/release/check_release_metadata.py release-metadata defer-release-graph-port validates release metadata and publish-step wiring through cached Bun release graph query rows -tools/release/local_registry_publish.py local-registry defer-local-registry-port remaining fallback for unported local-registry generation paths such as native extension Cargo synthesis and npm package synthesis +tools/release/local_registry_publish.py local-registry defer-local-registry-port remaining fallback for unported local-registry native extension Cargo and npm package synthesis tools/release/release.py release-orchestrator defer-release-graph-port owns protected release planning, validation, registry checks, publish dry-runs, and publish dispatch diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 8b84669a..65b9d4d2 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -764,6 +764,13 @@ def validate_graph_files() -> None: or "function pruneMissingLocalArtifactTargetDependencies(" not in local_registry_publish or "function nativeRuntimeArtifactManifests(" not in local_registry_publish or "nativeSplitReleaseAssetNames(" not in local_registry_publish + or "nativeNpmReleaseAssetNames(" not in local_registry_publish + or "function stageReleaseAssetNpmPackages(" not in local_registry_publish + or "function liboliphauntNpmTarballs(" not in local_registry_publish + or "function stageLiboliphauntToolsNpmPayloads(" not in local_registry_publish + or "function stageLiboliphauntIcuNpmPayload(" not in local_registry_publish + or "function brokerNpmTarballs(" not in local_registry_publish + or 'from "./optimize_native_runtime_payload.mjs"' not in local_registry_publish or 'from "./cargo-source-package.mjs"' not in local_registry_publish or 'from "./package_oliphaunt_wasix_sdk_crate.mjs"' not in local_registry_publish or "export function manualCargoPackageSource(" not in cargo_source_package @@ -787,6 +794,7 @@ def validate_graph_files() -> None: or "tools/release/local_registry_metadata.mjs" not in local_registry_publish or "if (options.help)" not in local_registry_publish or '(surface === "cargo" && (options.dryRun || !cargoCratesRequirePythonGeneration(options, roots)))' not in local_registry_publish + or '(surface === "npm" && (options.dryRun || !npmTarballsRequirePythonGeneration(roots)))' not in local_registry_publish or '["python3", "tools/release/local_registry_publish.py", "publish", ...argv]' not in local_registry_publish or '["python3", "tools/release/local_registry_publish.py", "status"' in local_registry_publish or '["python3", "tools/release/local_registry_publish.py", ...Bun.argv.slice(2)]' in local_registry_publish @@ -795,7 +803,7 @@ def validate_graph_files() -> None: or "python3 tools/release/local_registry_publish.py" in examples_readme or "tools/dev/bun.sh tools/release/local-registry-publish.mjs" not in examples_local_registries ): - fail("example local-registry setup must use the Bun local-registry command surface and stage Cargo release/source crates") + fail("example local-registry setup must use the Bun local-registry command surface and stage Cargo plus npm release/source packages") if ( "publish-step-target-coverage [--product PRODUCT]" not in release_graph_query or "export function publishStepTargetCoverageRows(" not in release_graph_source diff --git a/tools/release/local-registry-publish.mjs b/tools/release/local-registry-publish.mjs index 63d5dcb4..b046b143 100644 --- a/tools/release/local-registry-publish.mjs +++ b/tools/release/local-registry-publish.mjs @@ -3,9 +3,11 @@ import { spawn, spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; import { accessSync, + chmodSync, closeSync, constants, copyFileSync, + cpSync, existsSync, mkdtempSync, mkdirSync, @@ -31,6 +33,10 @@ import { currentOliphauntWasixSdkVersion, prepareOliphauntWasixReleaseSource, } from "./package_oliphaunt_wasix_sdk_crate.mjs"; +import { + requiredRuntimeTools, + requiredToolsPackageTools, +} from "./optimize_native_runtime_payload.mjs"; const TOOL = "local-registry-publish.mjs"; const DEFAULT_RUN_ID = "28049923289"; @@ -696,6 +702,13 @@ function nativeSplitReleaseAssetNames(version, targetId) { ]; } +function nativeNpmReleaseAssetNames(version, targetId) { + return [ + ...nativeSplitReleaseAssetNames(version, targetId), + `liboliphaunt-${version}-icu-data.tar.gz`, + ]; +} + function nativeSplitReleaseAssetsReady(assetDir, version, targetId) { const required = nativeSplitReleaseAssetNames(version, targetId); return { @@ -704,11 +717,24 @@ function nativeSplitReleaseAssetsReady(assetDir, version, targetId) { }; } +function nativeNpmReleaseAssetsReady(assetDir, version, targetId) { + const required = nativeNpmReleaseAssetNames(version, targetId); + return { + ready: releaseAssetDirHasExactFiles(assetDir, required), + missing: missingReleaseAssetNames(assetDir, required), + }; +} + function nativeSplitReleaseAssetMissingMessage(assetDir, version, targetId, missing) { const required = nativeSplitReleaseAssetNames(version, targetId).join(", "); return `native split release asset staging for ${targetId} requires runtime and tools assets (${required}) under ${rel(assetDir)}; missing ${missing.join(", ")}`; } +function nativeNpmReleaseAssetMissingMessage(assetDir, version, targetId, missing) { + const required = nativeNpmReleaseAssetNames(version, targetId).join(", "); + return `native npm artifact staging for ${targetId} requires runtime, tools, and ICU assets (${required}) under ${rel(assetDir)}; missing ${missing.join(", ")}`; +} + function cargoTargetTriple(targetId) { if (targetId === "linux-x64-gnu") { return "x86_64-unknown-linux-gnu"; @@ -762,6 +788,7 @@ function pathIsUnder(file, root) { function npmTarballPriority(tarball, registryRoot) { let priority = 20; for (const [root, value] of [ + [path.join(registryRoot, "npm-generated"), 110], [path.join(ROOT, "target/release/npm-packages"), 100], [path.join(ROOT, "target/sdk-artifacts"), 90], [path.join(registryRoot, "npm-extension-packages"), 80], @@ -821,6 +848,602 @@ function selectNpmTarballs(tarballs, registryRoot, result) { return [...unidentified, ...selected.values()].sort(compareText); } +function safeNpmPackageFilenamePrefix(packageName) { + return packageName.replace(/^@/u, "").replaceAll("/", "-"); +} + +function readJsonFile(file) { + try { + return JSON.parse(readFileSync(file, "utf8")); + } catch (error) { + fail(TOOL, `${rel(file)} is not valid JSON: ${error.message}`); + } +} + +function npmPackageDirsUnder(packageRoot) { + const packages = new Map(); + if (!isDirectory(packageRoot)) { + fail(TOOL, `${rel(packageRoot)} does not contain npm package descriptors`); + } + for (const entry of readdirSync(packageRoot, { withFileTypes: true }).sort((left, right) => compareText(left.name, right.name))) { + if (!entry.isDirectory()) { + continue; + } + const packageDir = path.join(packageRoot, entry.name); + const packageJsonPath = path.join(packageDir, "package.json"); + if (!isFile(packageJsonPath)) { + continue; + } + const packageJson = readJsonFile(packageJsonPath); + const packageName = packageJson.name; + if (typeof packageName !== "string" || packageName.length === 0) { + fail(TOOL, `${rel(packageJsonPath)} must declare name`); + } + if (packages.has(packageName)) { + fail(TOOL, `duplicate npm package name ${packageName} in ${rel(packages.get(packageName))} and ${rel(packageDir)}`); + } + packages.set(packageName, packageDir); + } + if (packages.size === 0) { + fail(TOOL, `${rel(packageRoot)} does not contain npm package descriptors`); + } + return packages; +} + +function artifactNpmPackageTargets(product, kind, surface, packageRoot) { + const packageDirs = npmPackageDirsUnder(packageRoot); + const packages = []; + for (const target of allArtifactTargets({ product, kind, surface, publishedOnly: true }, TOOL)) { + const packageName = target.npmPackage; + if (typeof packageName !== "string" || packageName.length === 0) { + fail(TOOL, `${target.id} must declare npmPackage for npm artifact package publication`); + } + const packageDir = packageDirs.get(packageName); + if (packageDir === undefined) { + fail(TOOL, `${target.id} declares npm package ${packageName}, but no descriptor exists under ${rel(packageRoot)}`); + } + packages.push([packageName, packageDir, target]); + } + const expected = packages.map(([packageName]) => packageName).sort(compareText); + const actual = [...packageDirs.keys()].sort(compareText); + if (JSON.stringify(actual) !== JSON.stringify(expected)) { + fail(TOOL, `${rel(packageRoot)} package descriptors must match published ${product} npm artifact targets for ${surface}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`); + } + return packages.sort((left, right) => compareText(left[2].target, right[2].target)); +} + +function validateNoConsumerInstallScripts(packageJson, label) { + const scripts = packageJson.scripts; + if (scripts === undefined || scripts === null || typeof scripts !== "object" || Array.isArray(scripts)) { + return; + } + const forbidden = ["preinstall", "install", "postinstall", "prepare"].filter((script) => Object.hasOwn(scripts, script)); + if (forbidden.length > 0) { + fail(TOOL, `${label} must not declare consumer install lifecycle scripts: ${forbidden.join(", ")}`); + } +} + +function validateNpmPackageMetadata(packageName, packageDir, version, { target = null } = {}) { + const packageJsonPath = path.join(packageDir, "package.json"); + if (!isFile(packageJsonPath)) { + fail(TOOL, `${rel(packageDir)} is missing package.json`); + } + const packageJson = readJsonFile(packageJsonPath); + if (packageJson.name !== packageName) { + fail(TOOL, `${rel(packageJsonPath)} name must be ${packageName}`); + } + if (packageJson.version !== version) { + fail(TOOL, `${packageName} package version must match ${version}`); + } + if (target !== null && packageJson.oliphaunt?.target !== target) { + fail(TOOL, `${packageName} package oliphaunt.target must be ${target}`); + } + validateNoConsumerInstallScripts(packageJson, `${packageName} npm package`); +} + +function stageNpmPackageDescriptor( + packageName, + sourceDir, + stageRoot, + version, + { + extraDescriptors = [], + target = null, + } = {}, +) { + const stageDir = path.join(stageRoot, safeNpmPackageFilenamePrefix(packageName)); + rmSync(stageDir, { recursive: true, force: true }); + mkdirSync(stageDir, { recursive: true }); + for (const descriptor of ["package.json", "README.md", ...extraDescriptors]) { + const source = path.join(sourceDir, descriptor); + if (!isFile(source)) { + fail(TOOL, `${rel(sourceDir)} is missing ${descriptor}`); + } + copyFileSync(source, path.join(stageDir, descriptor)); + } + validateNpmPackageMetadata(packageName, stageDir, version, { target }); + return stageDir; +} + +function runArchiveCommand(args, label) { + const result = spawnSync(args[0], args.slice(1), { + cwd: ROOT, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.error) { + fail(TOOL, `${label} failed to start: ${result.error.message}`); + } + if (result.status !== 0) { + const detail = (result.stderr || result.stdout || "").trim(); + fail(TOOL, `${label} failed${detail ? `: ${detail}` : ""}`); + } + return result.stdout; +} + +function archiveTempDir() { + const root = path.join(ROOT, "target", "local-registry-archive-extract"); + mkdirSync(root, { recursive: true }); + return mkdtempSync(path.join(root, "extract-")); +} + +function copyExtractedTree(source, destination) { + if (!isDirectory(source)) { + fail(TOOL, `release archive is missing extracted tree ${source}`); + } + rmSync(destination, { recursive: true, force: true }); + cpSync(source, destination, { recursive: true }); +} + +function extractArchiveMember(archive, member, destination, { mode = null } = {}) { + const temp = archiveTempDir(); + try { + if (archive.endsWith(".zip")) { + requireCommand("unzip"); + runArchiveCommand(["unzip", "-q", archive, member, "-d", temp], `extract ${member} from ${rel(archive)}`); + } else { + requireCommand("tar"); + runArchiveCommand(["tar", "-xf", archive, "-C", temp, member], `extract ${member} from ${rel(archive)}`); + } + const extracted = path.join(temp, ...member.split("/")); + if (!isFile(extracted)) { + fail(TOOL, `${rel(archive)} is missing ${member}`); + } + mkdirSync(path.dirname(destination), { recursive: true }); + copyFileSync(extracted, destination); + if (mode !== null) { + chmodSync(destination, mode); + } + } finally { + rmSync(temp, { recursive: true, force: true }); + } +} + +function extractArchiveTree(archive, sourcePrefix, destination) { + const temp = archiveTempDir(); + const prefix = sourcePrefix.replace(/\/+$/u, ""); + try { + if (archive.endsWith(".zip")) { + requireCommand("unzip"); + runArchiveCommand(["unzip", "-q", archive, `${prefix}/*`, "-d", temp], `extract ${prefix} from ${rel(archive)}`); + } else { + requireCommand("tar"); + runArchiveCommand(["tar", "-xf", archive, "-C", temp, prefix], `extract ${prefix} from ${rel(archive)}`); + } + copyExtractedTree(path.join(temp, ...prefix.split("/")), destination); + } finally { + rmSync(temp, { recursive: true, force: true }); + } +} + +function runNativePayloadOptimizer(stage, target, toolSet) { + runQuiet([ + "tools/dev/bun.sh", + "tools/release/optimize_native_runtime_payload.mjs", + stage, + "--target", + target, + "--tool-set", + toolSet, + ]); +} + +function ensureNativeToolsAbsentFromRuntime(stage, target) { + const runtimeDir = path.join(stage, "runtime"); + const leaked = []; + for (const tool of requiredToolsPackageTools(target, runtimeDir)) { + if (existsSync(path.join(runtimeDir, "bin", tool))) { + leaked.push(`runtime/bin/${tool}`); + } + } + if (leaked.length > 0) { + fail(TOOL, `${rel(stage)} root runtime package must not contain split native tools: ${leaked.join(", ")}`); + } +} + +function requiredRuntimeMemberPaths(target, prefix) { + return requiredRuntimeTools(target).map((tool) => `${prefix.replace(/\/+$/u, "")}/${tool}`); +} + +function requiredToolsMemberPaths(target, prefix) { + return requiredToolsPackageTools(target).map((tool) => `${prefix.replace(/\/+$/u, "")}/${tool}`); +} + +function pnpmPackForNpmPublish(packageDir, tarballRoot) { + const packageJson = readJsonFile(path.join(packageDir, "package.json")); + const packageName = packageJson.name; + const packageVersion = packageJson.version; + if (typeof packageName !== "string" || packageName.length === 0) { + fail(TOOL, `${rel(path.join(packageDir, "package.json"))} must declare a package name`); + } + if (typeof packageVersion !== "string" || packageVersion.length === 0) { + fail(TOOL, `${rel(path.join(packageDir, "package.json"))} must declare a package version`); + } + const packDir = path.join(tarballRoot, safeNpmPackageFilenamePrefix(packageName)); + rmSync(packDir, { recursive: true, force: true }); + mkdirSync(packDir, { recursive: true }); + const result = spawnSync("pnpm", ["pack", "--pack-destination", packDir, "--json"], { + cwd: packageDir, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.error) { + fail(TOOL, `pnpm pack for ${packageName} failed to start: ${result.error.message}`); + } + if (result.status !== 0) { + const detail = (result.stderr || result.stdout || "").trim(); + fail(TOOL, `pnpm pack for ${packageName} failed${detail ? `: ${detail}` : ""}`); + } + let manifest; + try { + manifest = JSON.parse(result.stdout); + } catch (error) { + fail(TOOL, `pnpm pack for ${packageName} did not emit JSON: ${error.message}`); + } + const row = Array.isArray(manifest) ? manifest[0] : manifest; + const filename = row?.filename; + if (typeof filename !== "string" || !filename.endsWith(".tgz")) { + fail(TOOL, `pnpm pack for ${packageName} did not report a .tgz filename`); + } + const destinationTarball = path.isAbsolute(filename) + ? filename + : path.join(packDir, path.basename(filename)); + if (!isFile(destinationTarball)) { + fail(TOOL, `pnpm pack for ${packageName} did not create ${rel(destinationTarball)}`); + } + return destinationTarball; +} + +function tarballMembers(tarball) { + return runArchiveCommand(["tar", "-tzf", tarball], `list ${rel(tarball)}`) + .split(/\r?\n/u) + .map((line) => line.trim()) + .filter(Boolean); +} + +function tarballPackageJson(tarball) { + const text = runArchiveCommand(["tar", "-xOzf", tarball, "package/package.json"], `read package.json from ${rel(tarball)}`); + try { + return JSON.parse(text); + } catch (error) { + fail(TOOL, `${rel(tarball)} package/package.json is not valid JSON: ${error.message}`); + } +} + +function packedPackageContains(tarball, packageName, version, requiredMembers, { executableMembers = [] } = {}) { + const members = new Set(tarballMembers(tarball)); + if (!members.has("package/package.json")) { + fail(TOOL, `${rel(tarball)} is missing package/package.json`); + } + const packageJson = tarballPackageJson(tarball); + if (packageJson.name !== packageName) { + fail(TOOL, `${rel(tarball)} package name must be ${packageName}, got ${JSON.stringify(packageJson.name)}`); + } + if (packageJson.version !== version) { + fail(TOOL, `${rel(tarball)} package version must be ${version}, got ${JSON.stringify(packageJson.version)}`); + } + for (const member of requiredMembers) { + if (!members.has(member)) { + fail(TOOL, `${rel(tarball)} is missing ${member}`); + } + } + for (const member of executableMembers) { + if (!members.has(member)) { + fail(TOOL, `${rel(tarball)} is missing executable ${member}`); + } + const mode = runArchiveCommand(["tar", "-tvzf", tarball, member], `inspect ${member} in ${rel(tarball)}`).trim().split(/\s+/u)[0] ?? ""; + if (!/[xst]/u.test(mode)) { + fail(TOOL, `${rel(tarball)} ${member} must be executable`); + } + } +} + +function packedIcuPackageContains(tarball, packageName, version) { + const members = new Set(tarballMembers(tarball)); + if (!members.has("package/package.json")) { + fail(TOOL, `${rel(tarball)} is missing package/package.json`); + } + const packageJson = tarballPackageJson(tarball); + if (packageJson.name !== packageName) { + fail(TOOL, `${rel(tarball)} package name must be ${packageName}, got ${JSON.stringify(packageJson.name)}`); + } + if (packageJson.version !== version) { + fail(TOOL, `${rel(tarball)} package version must be ${version}, got ${JSON.stringify(packageJson.version)}`); + } + const metadata = packageJson.oliphaunt; + if ( + metadata?.product !== "oliphaunt-icu" || + metadata?.kind !== "icu-data" || + metadata?.target !== "portable" || + metadata?.dataRelativePath !== "share/icu" + ) { + fail(TOOL, `${rel(tarball)} package.json must declare portable oliphaunt-icu metadata`); + } + if (!members.has("package/OliphauntICU.podspec")) { + fail(TOOL, `${rel(tarball)} is missing package/OliphauntICU.podspec`); + } + const hasIcuData = [...members].some((member) => { + if (!member.startsWith("package/share/icu/")) { + return false; + } + const relative = member.slice("package/share/icu/".length).split("/").filter(Boolean); + return relative.length > 0 && relative[0].startsWith("icudt"); + }); + if (!hasIcuData) { + fail(TOOL, `${rel(tarball)} is missing package/share/icu/icudt* data files`); + } +} + +function npmPackAndValidate(packageName, packageDir, version, tarballRoot, { requiredMembers, executableMembers = [], target = null }) { + validateNpmPackageMetadata(packageName, packageDir, version, { target }); + const tarball = pnpmPackForNpmPublish(packageDir, tarballRoot); + packedPackageContains(tarball, packageName, version, requiredMembers, { executableMembers }); + return tarball; +} + +function stageLiboliphauntNpmPayloads(version, stageRoot, { targetSet = null } = {}) { + const assetDir = path.join(ROOT, "target/liboliphaunt/release-assets"); + const packages = artifactNpmPackageTargets( + "liboliphaunt-native", + "native-runtime", + "typescript-native-direct", + path.join(ROOT, "src/runtimes/liboliphaunt/native/packages"), + ); + const stages = new Map(); + for (const [packageName, packageDir, target] of packages) { + if (targetSet !== null && !targetSet.has(target.target)) { + continue; + } + if (typeof target.libraryRelativePath !== "string" || target.libraryRelativePath.length === 0) { + fail(TOOL, `${target.id} must declare libraryRelativePath for npm artifact package publication`); + } + const stage = stageNpmPackageDescriptor(packageName, packageDir, stageRoot, version, { target: target.target }); + const archive = path.join(assetDir, target.asset.replaceAll("{version}", version)); + extractArchiveMember(archive, target.libraryRelativePath, path.join(stage, target.libraryRelativePath)); + extractArchiveTree(archive, "runtime", path.join(stage, "runtime")); + ensureNativeToolsAbsentFromRuntime(stage, target.target); + runNativePayloadOptimizer(stage, target.target, "runtime"); + stages.set(packageName, stage); + } + return stages; +} + +function stageLiboliphauntToolsNpmPayloads(version, stageRoot, { targetSet = null } = {}) { + const assetDir = path.join(ROOT, "target/liboliphaunt/release-assets"); + const packages = artifactNpmPackageTargets( + "liboliphaunt-native", + "native-tools", + "typescript-native-direct", + path.join(ROOT, "src/runtimes/liboliphaunt/native/tools-packages"), + ); + const stages = new Map(); + for (const [packageName, packageDir, target] of packages) { + if (targetSet !== null && !targetSet.has(target.target)) { + continue; + } + const stage = stageNpmPackageDescriptor(packageName, packageDir, stageRoot, version, { target: target.target }); + const archive = path.join(assetDir, target.asset.replaceAll("{version}", version)); + for (const tool of requiredToolsPackageTools(target.target)) { + const member = `runtime/bin/${tool}`; + extractArchiveMember(archive, member, path.join(stage, member), { mode: archive.endsWith(".zip") ? 0o755 : null }); + } + runNativePayloadOptimizer(stage, target.target, "tools"); + stages.set(packageName, stage); + } + return stages; +} + +function stageLiboliphauntIcuNpmPayload(version, stageRoot) { + const packageName = "@oliphaunt/icu"; + const stage = stageNpmPackageDescriptor( + packageName, + path.join(ROOT, "src/runtimes/liboliphaunt/native/icu-npm"), + stageRoot, + version, + { extraDescriptors: ["OliphauntICU.podspec"], target: "portable" }, + ); + extractArchiveTree( + path.join(ROOT, "target/liboliphaunt/release-assets", `liboliphaunt-${version}-icu-data.tar.gz`), + "share/icu", + path.join(stage, "share/icu"), + ); + return stage; +} + +function liboliphauntNpmTarballs(version, stageRoot, tarballRoot, { targetSet = null, includeIcu = true } = {}) { + const packages = []; + const runtimeStages = stageLiboliphauntNpmPayloads(version, stageRoot, { targetSet }); + const toolsStages = stageLiboliphauntToolsNpmPayloads(version, stageRoot, { targetSet }); + for (const [packageName, , target] of artifactNpmPackageTargets( + "liboliphaunt-native", + "native-runtime", + "typescript-native-direct", + path.join(ROOT, "src/runtimes/liboliphaunt/native/packages"), + )) { + if (targetSet !== null && !targetSet.has(target.target)) { + continue; + } + const runtimeMembers = requiredRuntimeMemberPaths(target.target, "package/runtime/bin"); + const requiredMembers = [`package/${target.libraryRelativePath}`, ...runtimeMembers]; + packages.push([ + packageName, + npmPackAndValidate(packageName, runtimeStages.get(packageName), version, tarballRoot, { + requiredMembers, + executableMembers: runtimeMembers, + target: target.target, + }), + ]); + } + for (const [packageName, , target] of artifactNpmPackageTargets( + "liboliphaunt-native", + "native-tools", + "typescript-native-direct", + path.join(ROOT, "src/runtimes/liboliphaunt/native/tools-packages"), + )) { + if (targetSet !== null && !targetSet.has(target.target)) { + continue; + } + const runtimeMembers = requiredToolsMemberPaths(target.target, "package/runtime/bin"); + packages.push([ + packageName, + npmPackAndValidate(packageName, toolsStages.get(packageName), version, tarballRoot, { + requiredMembers: runtimeMembers, + executableMembers: runtimeMembers, + target: target.target, + }), + ]); + } + if (includeIcu) { + const packageName = "@oliphaunt/icu"; + const stage = stageLiboliphauntIcuNpmPayload(version, stageRoot); + const tarball = pnpmPackForNpmPublish(stage, tarballRoot); + packedIcuPackageContains(tarball, packageName, version); + packages.push([packageName, tarball]); + } + return packages; +} + +function stageBrokerNpmPayloads(version, stageRoot, { targetSet = null } = {}) { + const assetDir = path.join(ROOT, "target/oliphaunt-broker/release-assets"); + const packages = artifactNpmPackageTargets( + "oliphaunt-broker", + "broker-helper", + "typescript-broker", + path.join(ROOT, "src/runtimes/broker/packages"), + ); + const stages = new Map(); + for (const [packageName, packageDir, target] of packages) { + if (targetSet !== null && !targetSet.has(target.target)) { + continue; + } + if (typeof target.executableRelativePath !== "string" || target.executableRelativePath.length === 0) { + fail(TOOL, `${target.id} must declare executableRelativePath for npm artifact package publication`); + } + const stage = stageNpmPackageDescriptor(packageName, packageDir, stageRoot, version, { target: target.target }); + const archive = path.join(assetDir, target.asset.replaceAll("{version}", version)); + extractArchiveMember(archive, target.executableRelativePath, path.join(stage, target.executableRelativePath), { + mode: archive.endsWith(".zip") ? 0o755 : null, + }); + stages.set(packageName, stage); + } + return stages; +} + +function brokerNpmTarballs(version, stageRoot, tarballRoot, { targetSet = null } = {}) { + const packages = []; + const stages = stageBrokerNpmPayloads(version, stageRoot, { targetSet }); + for (const [packageName, , target] of artifactNpmPackageTargets( + "oliphaunt-broker", + "broker-helper", + "typescript-broker", + path.join(ROOT, "src/runtimes/broker/packages"), + )) { + if (targetSet !== null && !targetSet.has(target.target)) { + continue; + } + const requiredMembers = [`package/${target.executableRelativePath}`]; + packages.push([ + packageName, + npmPackAndValidate(packageName, stages.get(packageName), version, tarballRoot, { + requiredMembers, + executableMembers: requiredMembers, + target: target.target, + }), + ]); + } + return packages; +} + +function stageReleaseAssetNpmPackages(roots, registryRoot, result, strict) { + const outputRoot = path.join(registryRoot, "npm-generated", "release-asset-packages"); + const stageRoot = path.join(outputRoot, "sources"); + const tarballRoot = path.join(outputRoot, "tarballs"); + rmSync(outputRoot, { recursive: true, force: true }); + mkdirSync(stageRoot, { recursive: true }); + mkdirSync(tarballRoot, { recursive: true }); + + const tarballs = []; + const target = hostNpmTarget(); + const targetSet = target === null ? null : new Set([target]); + + const libVersion = currentProductVersionSync("liboliphaunt-native", TOOL); + const libAssetDir = path.join(ROOT, "target/liboliphaunt/release-assets"); + const copiedLibAssets = target === null + ? [] + : copyReleaseAssetSet(roots, libAssetDir, nativeNpmReleaseAssetNames(libVersion, target)); + if (target === null) { + result.skipped.push("current host does not map to a supported native npm artifact target"); + } else if ( + copiedLibAssets.length > 0 || + (releaseAssetDirSelected(roots, libAssetDir) && releaseAssetDirHasFiles(libAssetDir, [ + `liboliphaunt-${libVersion}-*`, + `oliphaunt-tools-${libVersion}-*`, + ])) + ) { + const { ready, missing } = nativeNpmReleaseAssetsReady(libAssetDir, libVersion, target); + if (!ready) { + const message = nativeNpmReleaseAssetMissingMessage(libAssetDir, libVersion, target, missing); + result.skipped.push(message); + if (strict) { + fail(TOOL, message); + } + } else { + if (copiedLibAssets.length > 0) { + result.staged.push(`staged ${copiedLibAssets.length} liboliphaunt release asset(s)`); + } + tarballs.push(...liboliphauntNpmTarballs(libVersion, stageRoot, tarballRoot, { targetSet }).map(([, tarball]) => tarball)); + } + } else { + result.skipped.push("no liboliphaunt release assets found for native npm artifact packages"); + } + + const brokerVersion = currentProductVersionSync("oliphaunt-broker", TOOL); + const brokerAssetDir = path.join(ROOT, "target/oliphaunt-broker/release-assets"); + const copiedBrokerAssets = copyReleaseAssets(roots, brokerAssetDir, [ + "oliphaunt-broker-*.tar.gz", + "oliphaunt-broker-*.zip", + ]); + if ( + copiedBrokerAssets.length > 0 || + (releaseAssetDirSelected(roots, brokerAssetDir) && releaseAssetDirHasFiles(brokerAssetDir, [ + "oliphaunt-broker-*.tar.gz", + "oliphaunt-broker-*.zip", + ])) + ) { + if (copiedBrokerAssets.length > 0) { + result.staged.push(`staged ${copiedBrokerAssets.length} broker release asset(s)`); + } + tarballs.push(...brokerNpmTarballs(brokerVersion, stageRoot, tarballRoot, { targetSet }).map(([, tarball]) => tarball)); + } else { + result.skipped.push("no broker release assets found for broker npm artifact packages"); + } + + if (tarballs.length > 0) { + result.staged.push(`generated ${tarballs.length} release-asset npm package(s)`); + } + return tarballs; +} + function stageExtensionNpmPackagesDryRun(roots, target, result) { const manifests = discoverExtensionManifests(roots); if (manifests.length === 0) { @@ -863,25 +1486,7 @@ function publishNpmDryRun(roots, registryRoot, strict, port) { } function npmTarballsRequirePythonGeneration(roots) { - const manifests = discoverExtensionManifests(roots); - if (manifests.length > 0) { - return true; - } - for (const root of roots) { - if (!statSync(root).isDirectory()) { - continue; - } - for (const file of walkFiles(root)) { - const name = path.basename(file); - if ( - /^(liboliphaunt|oliphaunt-tools)-[^/]+\.(tar\.gz|zip)$/u.test(name) || - /^oliphaunt-broker-[^/]+\.(tar\.gz|zip)$/u.test(name) - ) { - return true; - } - } - } - return false; + return discoverExtensionManifests(roots).length > 0; } function writeVerdaccioConfig(root, port) { @@ -1109,13 +1714,12 @@ function runNpmPublishCommand(args) { async function publishNpmTarballs(roots, registryRoot, strict, port) { const result = surfaceResult("npm"); - result.skipped.push("no liboliphaunt release assets found for native npm artifact packages"); - result.skipped.push("no broker release assets found for broker npm artifact packages"); + const generatedTarballs = stageReleaseAssetNpmPackages(roots, registryRoot, result, strict); if (discoverExtensionManifests(roots).length === 0) { result.skipped.push("no extension-artifacts.json manifests found for npm extension packages"); } - const tarballs = selectNpmTarballs(discoverFiles(roots, [".tgz"]), registryRoot, result); + const tarballs = selectNpmTarballs([...discoverFiles(roots, [".tgz"]), ...generatedTarballs], registryRoot, result); if (tarballs.length === 0) { addSkip(result, "no npm .tgz artifacts found", strict); return result; From b8754d21ec7219b12c050ec79f0f5815cdf9a8ed Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 02:43:49 +0000 Subject: [PATCH 250/308] chore: keep extension npm staging in bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 13 +- tools/policy/check-tooling-stack.sh | 10 +- tools/policy/python-entrypoints.allowlist | 2 +- tools/release/check_release_metadata.py | 6 +- tools/release/local-registry-publish.mjs | 504 +++++++++++++++++- 5 files changed, 526 insertions(+), 9 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 3b3d0d79..0d948429 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -206,8 +206,19 @@ until the current-state gates here are checked with fresh local evidence. liboliphaunt runtime packages, split `oliphaunt-tools` packages, native ICU, and broker helper packages from release assets, validates runtime/tool payload membership through the shared native optimizer policy, prefers generated - tarballs over stale artifact roots, and leaves only native extension npm + tarballs over stale artifact roots, and leaves native extension Cargo package synthesis on the explicit Python fallback. +- 2026-06-28: Ported local-registry native extension npm package synthesis into + `tools/release/local-registry-publish.mjs`. Bun now generates the + `@oliphaunt/extension-*` meta package, host target selector, and split + payload packages from `extension-artifacts.json` plus release manifests, + recursively splits payload packages below the 10 MiB npm limit, and removes + npm extension roots from the Python publish fallback. Fresh PostGIS evidence: + a strict local-registry npm publish generated and published + `@oliphaunt/extension-postgis`, the Linux x64 target selector, and two + payload packages at 6.27 MiB and 3.95 MiB; a scratch npm consumer installed + the meta package from Verdaccio and resolved both payload packages with + `postgis-3.so`, extension SQL/control files, and PROJ data present. - 2026-06-27: Ported the WASIX Cargo artifact packager from `tools/release/package_liboliphaunt_wasix_cargo_artifacts.py` to the Bun entrypoint `tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs`. diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index f29a300b..cc92a62e 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -429,7 +429,7 @@ grep -Fq 'if (options.help)' tools/release/local-registry-publish.mjs || grep -Fq '(surface === "cargo" && (options.dryRun || !cargoCratesRequirePythonGeneration(options, roots)))' tools/release/local-registry-publish.mjs || fail "local-registry Cargo real publish must use Bun for supported crate, release-asset, and source-staging roots" grep -Fq '(surface === "npm" && (options.dryRun || !npmTarballsRequirePythonGeneration(roots)))' tools/release/local-registry-publish.mjs || - fail "local-registry npm real publish must use Bun for supported tarball and release-asset package roots" + fail "local-registry npm real publish must use Bun for supported tarball, release-asset, and extension package roots" grep -Fq '["python3", "tools/release/local_registry_publish.py", "publish", ...argv]' tools/release/local-registry-publish.mjs || fail "local-registry real publish generation fallback must stay explicit to the publish command" if grep -Fq '["python3", "tools/release/local_registry_publish.py", ...Bun.argv.slice(2)]' tools/release/local-registry-publish.mjs; then @@ -439,6 +439,14 @@ grep -Fq 'async function publishNpmTarballs(' tools/release/local-registry-publi fail "local-registry npm tarball/release-asset publish loop must run in the Bun entrypoint" grep -Fq 'function stageReleaseAssetNpmPackages(' tools/release/local-registry-publish.mjs || fail "local-registry npm release-asset package staging must run in the Bun entrypoint" +grep -Fq 'function stageExtensionNpmPackages(' tools/release/local-registry-publish.mjs || + fail "local-registry npm extension package staging must run in the Bun entrypoint" +grep -Fq 'function stageExtensionPayloadGroups(' tools/release/local-registry-publish.mjs || + fail "local-registry npm extension payload splitting must run in the Bun entrypoint" +grep -Fq 'function extensionNpmPayloadPackage(' tools/release/local-registry-publish.mjs || + fail "local-registry npm extension payload package names must be generated in the Bun entrypoint" +grep -Fq $'function npmTarballsRequirePythonGeneration(roots) {\n return false;\n}' tools/release/local-registry-publish.mjs || + fail "local-registry npm publish must not fall back to Python after extension package staging moved to Bun" grep -Fq 'function liboliphauntNpmTarballs(' tools/release/local-registry-publish.mjs || fail "local-registry native runtime/tools npm package generation must run in the Bun entrypoint" grep -Fq 'function stageLiboliphauntToolsNpmPayloads(' tools/release/local-registry-publish.mjs || diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index 94989aa7..f27d3b6a 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -4,5 +4,5 @@ src/extensions/tools/check-extension-model.py extensions defer-extension-model-port generates and validates multi-language extension catalog, SDK metadata, docs, and evidence from one model tools/release/check_consumer_shape.py release-consumer-shape defer-release-graph-port validates cross-SDK package/runtime/install shape from generated release fixtures and source invariants tools/release/check_release_metadata.py release-metadata defer-release-graph-port validates release metadata and publish-step wiring through cached Bun release graph query rows -tools/release/local_registry_publish.py local-registry defer-local-registry-port remaining fallback for unported local-registry native extension Cargo and npm package synthesis +tools/release/local_registry_publish.py local-registry defer-local-registry-port remaining fallback for unported local-registry native extension Cargo synthesis tools/release/release.py release-orchestrator defer-release-graph-port owns protected release planning, validation, registry checks, publish dry-runs, and publish dispatch diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 65b9d4d2..e6a44fb6 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -766,6 +766,9 @@ def validate_graph_files() -> None: or "nativeSplitReleaseAssetNames(" not in local_registry_publish or "nativeNpmReleaseAssetNames(" not in local_registry_publish or "function stageReleaseAssetNpmPackages(" not in local_registry_publish + or "function stageExtensionNpmPackages(" not in local_registry_publish + or "function stageExtensionPayloadGroups(" not in local_registry_publish + or "function extensionNpmPayloadPackage(" not in local_registry_publish or "function liboliphauntNpmTarballs(" not in local_registry_publish or "function stageLiboliphauntToolsNpmPayloads(" not in local_registry_publish or "function stageLiboliphauntIcuNpmPayload(" not in local_registry_publish @@ -795,6 +798,7 @@ def validate_graph_files() -> None: or "if (options.help)" not in local_registry_publish or '(surface === "cargo" && (options.dryRun || !cargoCratesRequirePythonGeneration(options, roots)))' not in local_registry_publish or '(surface === "npm" && (options.dryRun || !npmTarballsRequirePythonGeneration(roots)))' not in local_registry_publish + or "function npmTarballsRequirePythonGeneration(roots) {\n return false;\n}" not in local_registry_publish or '["python3", "tools/release/local_registry_publish.py", "publish", ...argv]' not in local_registry_publish or '["python3", "tools/release/local_registry_publish.py", "status"' in local_registry_publish or '["python3", "tools/release/local_registry_publish.py", ...Bun.argv.slice(2)]' in local_registry_publish @@ -803,7 +807,7 @@ def validate_graph_files() -> None: or "python3 tools/release/local_registry_publish.py" in examples_readme or "tools/dev/bun.sh tools/release/local-registry-publish.mjs" not in examples_local_registries ): - fail("example local-registry setup must use the Bun local-registry command surface and stage Cargo plus npm release/source packages") + fail("example local-registry setup must use the Bun local-registry command surface and stage Cargo plus npm release/source/extension packages") if ( "publish-step-target-coverage [--product PRODUCT]" not in release_graph_query or "export function publishStepTargetCoverageRows(" not in release_graph_source diff --git a/tools/release/local-registry-publish.mjs b/tools/release/local-registry-publish.mjs index b046b143..0703f85f 100644 --- a/tools/release/local-registry-publish.mjs +++ b/tools/release/local-registry-publish.mjs @@ -755,6 +755,14 @@ function extensionNpmPackage(sqlName) { return `@oliphaunt/extension-${sqlName.replaceAll("_", "-")}`; } +function extensionNpmTargetPackage(sqlName, target) { + return `${extensionNpmPackage(sqlName)}-${target}`; +} + +function extensionNpmPayloadPackage(sqlName, target, index) { + return `${extensionNpmTargetPackage(sqlName, target)}-payload-${index}`; +} + function npmPackageIdentity(tarball) { const members = tryCommandOutput(["tar", "-tzf", tarball]); if (members === null) { @@ -1444,6 +1452,488 @@ function stageReleaseAssetNpmPackages(roots, registryRoot, result, strict) { return tarballs; } +function npmPlatformConstraints(target) { + 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 {}; +} + +function writeJsonFile(file, value) { + writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`); +} + +function extensionReleaseManifest(extensionDir, product, version) { + const manifestPath = path.join(extensionDir, "release-assets", `${product}-${version}-manifest.json`); + return isFile(manifestPath) ? readJsonFile(manifestPath) : {}; +} + +function extensionRuntimeAsset(extensionDir, manifest, target) { + const assets = Array.isArray(manifest?.assets) ? manifest.assets : []; + for (const asset of assets) { + if ( + asset?.family === "native" && + asset?.kind === "runtime" && + asset?.target === target && + typeof asset?.name === "string" && + asset.name.length > 0 + ) { + const assetPath = path.join(extensionDir, "release-assets", asset.name); + if (isFile(assetPath)) { + return assetPath; + } + } + } + return null; +} + +function checkedArchiveMemberPath(name, archive) { + const normalized = String(name).replaceAll("\\", "/"); + if (!normalized || normalized === "." || normalized === "./" || normalized.startsWith("/") || normalized.includes("\0")) { + fail(TOOL, `${rel(archive)} contains unsafe archive member ${JSON.stringify(name)}`); + } + const parts = normalized.split("/").filter((part) => part && part !== "."); + if (parts.length === 0 || parts.includes("..")) { + fail(TOOL, `${rel(archive)} contains unsafe archive member ${JSON.stringify(name)}`); + } + return parts.join("/"); +} + +function extractExtensionRuntime(asset, runtimeDir) { + const members = runArchiveCommand(["tar", "-tf", asset], `list ${rel(asset)}`) + .split(/\r?\n/u) + .map((line) => line.trim()) + .filter(Boolean); + for (const member of members) { + const checked = checkedArchiveMemberPath(member, asset); + if (checked !== "files" && !checked.startsWith("files/") && checked !== "manifest.properties") { + fail(TOOL, `${rel(asset)} contains unexpected extension runtime member ${checked}`); + } + } + const temp = archiveTempDir(); + try { + runArchiveCommand(["tar", "-xf", asset, "-C", temp, "files"], `extract extension runtime from ${rel(asset)}`); + copyExtractedTree(path.join(temp, "files"), runtimeDir); + } finally { + rmSync(temp, { recursive: true, force: true }); + } +} + +function extensionModuleDirectory(runtimeDir) { + const postgresLib = path.join(runtimeDir, "lib", "postgresql"); + if (!isDirectory(postgresLib)) { + return null; + } + for (const file of readdirSync(postgresLib).sort(compareText)) { + const fullPath = path.join(postgresLib, file); + if (isFile(fullPath) && [".so", ".dylib", ".dll"].includes(path.extname(file).toLowerCase())) { + return postgresLib; + } + } + return null; +} + +function stripExtensionModules(runtimeDir, target) { + const moduleDir = extensionModuleDirectory(runtimeDir); + if (moduleDir === null || !target.startsWith("linux-") || !executableExists("strip")) { + return; + } + for (const file of readdirSync(moduleDir).sort(compareText)) { + const fullPath = path.join(moduleDir, file); + if (!isFile(fullPath) || path.extname(file) !== ".so") { + continue; + } + spawnSync("strip", ["--strip-unneeded", fullPath], { + cwd: ROOT, + stdio: "ignore", + }); + } +} + +function writeExtensionReadme(packageDir, packageName, sqlName, target) { + const targetText = target === null ? "" : ` for \`${target}\``; + writeFileSync( + path.join(packageDir, "README.md"), + [ + `# ${packageName}`, + "", + `Oliphaunt registry package for the \`${sqlName}\` PostgreSQL extension${targetText}.`, + "", + "This package is consumed by `@oliphaunt/ts` when an application opens a database with", + `\`extensions: ['${sqlName}']\`.`, + "", + ].join("\n"), + ); +} + +function writeExtensionMetaPackage(packageDir, { product, version, sqlName, target }) { + const packageName = extensionNpmPackage(sqlName); + const targetPackage = extensionNpmTargetPackage(sqlName, target); + mkdirSync(packageDir, { recursive: true }); + writeExtensionReadme(packageDir, packageName, sqlName, null); + writeJsonFile(path.join(packageDir, "package.json"), { + name: packageName, + version, + description: `Oliphaunt extension package for PostgreSQL ${sqlName}.`, + license: "MIT AND Apache-2.0 AND PostgreSQL", + type: "module", + optionalDependencies: { [targetPackage]: version }, + oliphaunt: { + product, + kind: "exact-extension", + sqlName, + targetPackageNames: { [target]: targetPackage }, + }, + publishConfig: { access: "public", provenance: false }, + files: ["README.md"], + exports: { "./package.json": "./package.json" }, + }); +} + +function writeExtensionTargetPackage(packageDir, { + product, + version, + sqlName, + target, + liboliphauntVersion, + payloadPackageNames, +}) { + const packageName = extensionNpmTargetPackage(sqlName, target); + mkdirSync(packageDir, { recursive: true }); + writeExtensionReadme(packageDir, packageName, sqlName, target); + writeJsonFile(path.join(packageDir, "package.json"), { + name: packageName, + version, + description: `${target} Oliphaunt extension package selector for PostgreSQL ${sqlName}.`, + license: "MIT AND Apache-2.0 AND PostgreSQL", + type: "module", + ...npmPlatformConstraints(target), + optional: true, + optionalDependencies: Object.fromEntries(payloadPackageNames.map((name) => [name, version])), + oliphaunt: { + product, + kind: "exact-extension-target", + sqlName, + target, + liboliphauntVersion, + payloadPackageNames, + }, + publishConfig: { access: "public", provenance: false }, + files: ["README.md"], + exports: { "./package.json": "./package.json" }, + }); +} + +function copyRuntimeEntries(runtimeDir, payloadRuntimeDir, entries) { + for (const entry of entries) { + const relative = path.relative(runtimeDir, entry); + const destination = path.join(payloadRuntimeDir, relative); + if (isDirectory(entry)) { + cpSync(entry, destination, { recursive: true }); + } else if (isFile(entry)) { + mkdirSync(path.dirname(destination), { recursive: true }); + copyFileSync(entry, destination); + } + } +} + +function writeExtensionPayloadPackage(packageDir, { + packageName, + product, + version, + sqlName, + target, + liboliphauntVersion, +}) { + const runtimeDir = path.join(packageDir, "runtime"); + const moduleDir = extensionModuleDirectory(runtimeDir); + const metadata = { + product, + kind: "exact-extension-payload", + sqlName, + target, + runtimeRelativePath: "runtime", + liboliphauntVersion, + }; + if (moduleDir !== null) { + metadata.moduleRelativePath = path.relative(packageDir, moduleDir).split(path.sep).join("/"); + } + writeExtensionReadme(packageDir, packageName, sqlName, target); + writeJsonFile(path.join(packageDir, "package.json"), { + name: packageName, + version, + description: `${target} Oliphaunt extension runtime payload for PostgreSQL ${sqlName}.`, + license: "MIT AND Apache-2.0 AND PostgreSQL", + type: "module", + ...npmPlatformConstraints(target), + optional: true, + oliphaunt: metadata, + publishConfig: { access: "public", provenance: false }, + files: ["runtime", "README.md"], + exports: { "./package.json": "./package.json" }, + }); +} + +function npmPackageSizeOk(tarball, result) { + const size = statSync(tarball).size; + if (size <= NPM_PACKAGE_SIZE_LIMIT_BYTES) { + return true; + } + result.skipped.push(`${rel(tarball)} is ${size} bytes, exceeding the 10 MiB npm package limit`); + rmSync(tarball, { force: true }); + return false; +} + +function immediateRuntimeEntries(runtimeDir) { + if (!isDirectory(runtimeDir)) { + return []; + } + return readdirSync(runtimeDir) + .sort(compareText) + .map((entry) => path.join(runtimeDir, entry)); +} + +function stageExtensionPayloadGroup({ + runtimeDir, + entries, + packageRoot, + tarballRoot, + product, + version, + sqlName, + target, + liboliphauntVersion, + payloadIndex, + result, +}) { + const packageName = extensionNpmPayloadPackage(sqlName, target, payloadIndex); + const packageDir = path.join(packageRoot, safeNpmPackageFilenamePrefix(packageName)); + rmSync(packageDir, { recursive: true, force: true }); + const payloadRuntimeDir = path.join(packageDir, "runtime"); + mkdirSync(payloadRuntimeDir, { recursive: true }); + copyRuntimeEntries(runtimeDir, payloadRuntimeDir, entries); + writeExtensionPayloadPackage(packageDir, { + packageName, + product, + version, + sqlName, + target, + liboliphauntVersion, + }); + const tarball = pnpmPackForNpmPublish(packageDir, tarballRoot); + if (statSync(tarball).size <= NPM_PACKAGE_SIZE_LIMIT_BYTES) { + return { packageNames: [packageName], tarballs: [tarball] }; + } + + rmSync(tarball, { force: true }); + rmSync(packageDir, { recursive: true, force: true }); + if (entries.length === 1 && isDirectory(entries[0])) { + const childEntries = readdirSync(entries[0]) + .sort(compareText) + .map((entry) => path.join(entries[0], entry)); + if (childEntries.length > 0) { + return stageExtensionPayloadGroups({ + runtimeDir, + groups: childEntries.map((entry) => [entry]), + packageRoot, + tarballRoot, + product, + version, + sqlName, + target, + liboliphauntVersion, + startIndex: payloadIndex, + result, + }); + } + } + if (entries.length > 1) { + return stageExtensionPayloadGroups({ + runtimeDir, + groups: entries.map((entry) => [entry]), + packageRoot, + tarballRoot, + product, + version, + sqlName, + target, + liboliphauntVersion, + startIndex: payloadIndex, + result, + }); + } + + result.skipped.push(`${packageName} cannot be split below the 10 MiB npm package limit; largest entry is ${rel(entries[0])}`); + return { packageNames: [], tarballs: [] }; +} + +function stageExtensionPayloadGroups({ + runtimeDir, + groups, + packageRoot, + tarballRoot, + product, + version, + sqlName, + target, + liboliphauntVersion, + startIndex, + result, +}) { + const packageNames = []; + const tarballs = []; + let payloadIndex = startIndex; + for (const entries of groups) { + const staged = stageExtensionPayloadGroup({ + runtimeDir, + entries, + packageRoot, + tarballRoot, + product, + version, + sqlName, + target, + liboliphauntVersion, + payloadIndex, + result, + }); + if (staged.packageNames.length === 0) { + continue; + } + packageNames.push(...staged.packageNames); + tarballs.push(...staged.tarballs); + payloadIndex += staged.packageNames.length; + } + return { packageNames, tarballs }; +} + +function stageExtensionPayloadPackages({ + runtimeDir, + packageRoot, + tarballRoot, + product, + version, + sqlName, + target, + liboliphauntVersion, + result, +}) { + return stageExtensionPayloadGroups({ + runtimeDir, + groups: immediateRuntimeEntries(runtimeDir).map((entry) => [entry]), + packageRoot, + tarballRoot, + product, + version, + sqlName, + target, + liboliphauntVersion, + startIndex: 0, + result, + }); +} + +function stageExtensionNpmPackages(roots, stagingRoot, target, result) { + const manifests = discoverExtensionManifests(roots); + if (manifests.length === 0) { + result.skipped.push("no extension-artifacts.json manifests found for npm extension packages"); + return null; + } + if (target === null) { + result.skipped.push("current host does not map to a supported npm extension target"); + return null; + } + + rmSync(stagingRoot, { recursive: true, force: true }); + const packageRoot = path.join(stagingRoot, "packages"); + const tarballRoot = path.join(stagingRoot, "tarballs"); + const workRoot = path.join(stagingRoot, "work"); + let stagedAny = false; + + for (const manifestPath of manifests) { + const manifest = readJsonFile(manifestPath); + const extensionDir = path.dirname(manifestPath); + const { product, version, sqlName } = manifest; + if (![product, version, sqlName].every((value) => typeof value === "string" && value.length > 0)) { + result.skipped.push(`${rel(manifestPath)} is missing product, version, or sqlName`); + continue; + } + const releaseManifest = extensionReleaseManifest(extensionDir, product, version); + const asset = extensionRuntimeAsset(extensionDir, Object.keys(releaseManifest).length > 0 ? releaseManifest : manifest, target); + if (asset === null) { + result.skipped.push(`${product}@${version} has no ${target} native runtime asset`); + continue; + } + const compatibility = releaseManifest.compatibility ?? {}; + const liboliphauntVersion = compatibility.nativeRuntimeVersion ?? version; + if (typeof liboliphauntVersion !== "string" || liboliphauntVersion.length === 0) { + result.skipped.push(`${product}@${version} is missing native runtime compatibility`); + continue; + } + + const metaDir = path.join(packageRoot, safeNpmPackageFilenamePrefix(extensionNpmPackage(sqlName))); + const targetDir = path.join(packageRoot, safeNpmPackageFilenamePrefix(extensionNpmTargetPackage(sqlName, target))); + const runtimeWorkDir = path.join(workRoot, safeNpmPackageFilenamePrefix(extensionNpmTargetPackage(sqlName, target)), "runtime"); + extractExtensionRuntime(asset, runtimeWorkDir); + stripExtensionModules(runtimeWorkDir, target); + const { packageNames: payloadPackageNames, tarballs: payloadTarballs } = stageExtensionPayloadPackages({ + runtimeDir: runtimeWorkDir, + packageRoot, + tarballRoot, + product, + version, + sqlName, + target, + liboliphauntVersion, + result, + }); + if (payloadPackageNames.length === 0) { + continue; + } + writeExtensionMetaPackage(metaDir, { product, version, sqlName, target }); + writeExtensionTargetPackage(targetDir, { + product, + version, + sqlName, + target, + liboliphauntVersion, + payloadPackageNames, + }); + const targetTarball = pnpmPackForNpmPublish(targetDir, tarballRoot); + if (!npmPackageSizeOk(targetTarball, result)) { + for (const tarball of payloadTarballs) { + rmSync(tarball, { force: true }); + } + continue; + } + const metaTarball = pnpmPackForNpmPublish(metaDir, tarballRoot); + if (!npmPackageSizeOk(metaTarball, result)) { + rmSync(targetTarball, { force: true }); + for (const tarball of payloadTarballs) { + rmSync(tarball, { force: true }); + } + continue; + } + for (const tarball of payloadTarballs) { + result.staged.push(rel(tarball)); + } + result.staged.push(rel(targetTarball)); + result.staged.push(rel(metaTarball)); + stagedAny = true; + } + + return stagedAny ? tarballRoot : null; +} + function stageExtensionNpmPackagesDryRun(roots, target, result) { const manifests = discoverExtensionManifests(roots); if (manifests.length === 0) { @@ -1486,7 +1976,7 @@ function publishNpmDryRun(roots, registryRoot, strict, port) { } function npmTarballsRequirePythonGeneration(roots) { - return discoverExtensionManifests(roots).length > 0; + return false; } function writeVerdaccioConfig(root, port) { @@ -1715,11 +2205,15 @@ function runNpmPublishCommand(args) { async function publishNpmTarballs(roots, registryRoot, strict, port) { const result = surfaceResult("npm"); const generatedTarballs = stageReleaseAssetNpmPackages(roots, registryRoot, result, strict); - if (discoverExtensionManifests(roots).length === 0) { - result.skipped.push("no extension-artifacts.json manifests found for npm extension packages"); - } + const extensionTarballRoot = stageExtensionNpmPackages( + roots, + path.join(registryRoot, "npm-extension-packages"), + hostNpmTarget(), + result, + ); + const npmRoots = extensionTarballRoot === null ? roots : [...roots, extensionTarballRoot]; - const tarballs = selectNpmTarballs([...discoverFiles(roots, [".tgz"]), ...generatedTarballs], registryRoot, result); + const tarballs = selectNpmTarballs([...discoverFiles(npmRoots, [".tgz"]), ...generatedTarballs], registryRoot, result); if (tarballs.length === 0) { addSkip(result, "no npm .tgz artifacts found", strict); return result; From a17e6837f79681afca8722fecca9396167290423 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 03:06:46 +0000 Subject: [PATCH 251/308] chore: finish local registry publisher bun port --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 30 +- .../examples-ci-release-validation.md | 4 +- tools/policy/check-python-entrypoints.mjs | 1 - tools/policy/check-tooling-stack.sh | 20 +- tools/policy/python-entrypoints.allowlist | 1 - tools/release/check_consumer_shape.py | 37 +- tools/release/check_release_metadata.py | 50 +- tools/release/local-registry-publish.mjs | 752 +++- tools/release/local_registry_publish.py | 3082 ----------------- 9 files changed, 790 insertions(+), 3187 deletions(-) delete mode 100755 tools/release/local_registry_publish.py diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 0d948429..983da36d 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -161,8 +161,9 @@ until the current-state gates here are checked with fresh local evidence. invalidation when no native/extension npm package synthesis is required. Fresh smoke published `target/sdk-artifacts/oliphaunt-js/oliphaunt-ts-0.1.0.tgz` into a disposable Verdaccio registry on port 4891 and stopped the temporary - registry process. Full native runtime/tools and exact-extension npm package - synthesis still falls back to Python until those generators are ported. + registry process. At that checkpoint, full native runtime/tools and + exact-extension npm package synthesis still fell back to Python; later entries + below moved those generators into Bun. - 2026-06-28: Removed the last Python delegation from the local-registry `status` subcommand by adding Bun-native `status --help` output. The regular status report was already generated in Bun; metadata and tooling guards now @@ -173,21 +174,19 @@ until the current-state gates here are checked with fresh local evidence. `tools/release/local-registry-publish.mjs`. Top-level `--help`, `download --help`, `publish --help`, and `status --help` now return directly from Bun, and guards require the helper functions plus the `publish --help` - pre-fallback branch. The remaining Python fallback is limited to unported - real publish generation paths. + pre-publish branch. Later entries below removed the remaining real publish + generation fallback. - 2026-06-28: Removed the generic unknown-command Python fallback from `tools/release/local-registry-publish.mjs`. Unsupported local-registry - commands now fail in Bun with exit code 2, and metadata/tooling guards require - the publish fallback to stay explicit to `publish` while rejecting a catch-all - `local_registry_publish.py` dispatch. + commands now fail in Bun with exit code 2, and metadata/tooling guards reject + both catch-all and publish-specific `local_registry_publish.py` dispatch. - 2026-06-28: Ported the real local-registry Cargo publish loop for explicit prebuilt `.crate` artifact roots into `tools/release/local-registry-publish.mjs`. Bun now extracts crate metadata, writes the file-backed Cargo git index, translates local versus crates.io dependency registry fields, rejects crates over the 10 MiB package limit, writes the Cargo config snippet, clears the local Cargo cache, and emits `report.json`. Release-asset, source-crate, and - native-extension Cargo generation paths still use the explicit Python publish - fallback until those generators are ported. + native-extension Cargo generation now run through the Bun publish path. - 2026-06-28: Ported local-registry Cargo release-asset and source-crate staging into `tools/release/local-registry-publish.mjs`. Bun now stages native runtime plus `oliphaunt-tools` release assets together, stages WASIX @@ -206,8 +205,8 @@ until the current-state gates here are checked with fresh local evidence. liboliphaunt runtime packages, split `oliphaunt-tools` packages, native ICU, and broker helper packages from release assets, validates runtime/tool payload membership through the shared native optimizer policy, prefers generated - tarballs over stale artifact roots, and leaves native extension Cargo package - synthesis on the explicit Python fallback. + tarballs over stale artifact roots, and keeps npm release-asset staging on + the same Bun publish path as extension package synthesis. - 2026-06-28: Ported local-registry native extension npm package synthesis into `tools/release/local-registry-publish.mjs`. Bun now generates the `@oliphaunt/extension-*` meta package, host target selector, and split @@ -219,6 +218,15 @@ until the current-state gates here are checked with fresh local evidence. payload packages at 6.27 MiB and 3.95 MiB; a scratch npm consumer installed the meta package from Verdaccio and resolved both payload packages with `postgis-3.so`, extension SQL/control files, and PROJ data present. +- 2026-06-28: Ported local-registry native extension Cargo package synthesis into + `tools/release/local-registry-publish.mjs`. Bun now generates exact native + extension Cargo crates from `extension-artifacts.json` plus release manifests, + strips Linux extension modules when `strip` is available, splits payloads into + 7 MiB part crates once a package crosses the 9 MiB split threshold, and uses + a small aggregator crate to reconstruct payload manifests. The local-registry + publish command no longer dispatches any surface to Python, and the retired + `local_registry_publish.py` entrypoint was removed after the remaining + consumer-shape references moved to the Bun entrypoint. - 2026-06-27: Ported the WASIX Cargo artifact packager from `tools/release/package_liboliphaunt_wasix_cargo_artifacts.py` to the Bun entrypoint `tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs`. diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index c8099e1d..509c6256 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -204,8 +204,8 @@ the release/tooling surface after the runtime tool crate split. crates.io limit; the largest observed crate was 10,212,312 bytes. - On 2026-06-27, strict local Cargo and npm publication were rerun against the current split runtime/tools package surface with - `tools/release/local_registry_publish.py publish --surface cargo --strict` - and `tools/release/local_registry_publish.py publish --surface npm --strict`. + `tools/dev/bun.sh tools/release/local-registry-publish.mjs publish --surface cargo --strict` + and `tools/dev/bun.sh tools/release/local-registry-publish.mjs publish --surface npm --strict`. A generated crate sweep over `target/local-registries` found no `.crate` above the 10 MiB crates.io limit. - Native Linux x64 Cargo artifact generation now emits split payloads: diff --git a/tools/policy/check-python-entrypoints.mjs b/tools/policy/check-python-entrypoints.mjs index 8a0325f4..dbb90427 100644 --- a/tools/policy/check-python-entrypoints.mjs +++ b/tools/policy/check-python-entrypoints.mjs @@ -7,7 +7,6 @@ const PYTHON_PATHSPEC = ":(glob)**/*.py"; const args = process.argv.slice(2); const MIGRATION_DECISIONS = new Set([ "defer-extension-model-port", - "defer-local-registry-port", "defer-release-graph-port", "defer-wasix-packager-port", ]); diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index cc92a62e..814d2c7f 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -348,7 +348,6 @@ do done 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" || @@ -389,6 +388,14 @@ grep -Fq 'function stageReleaseAssetCargoPackages(' tools/release/local-registry fail "local-registry Cargo release-asset crate staging must run in the Bun entrypoint" grep -Fq 'function stageCargoSourceCrates(' tools/release/local-registry-publish.mjs || fail "local-registry Cargo source crate staging must run in the Bun entrypoint" +grep -Fq 'function packageNativeExtensionCargoCrates(' tools/release/local-registry-publish.mjs || + fail "local-registry native extension Cargo package staging must run in the Bun entrypoint" +grep -Fq 'function writeNativeExtensionCargoCrate(' tools/release/local-registry-publish.mjs || + fail "local-registry native extension Cargo crates must be generated in the Bun entrypoint" +grep -Fq 'function buildNativeExtensionPartCrates(' tools/release/local-registry-publish.mjs || + fail "local-registry native extension Cargo payload splitting must run in the Bun entrypoint" +grep -Fq 'function writeNativeExtensionSplitAggregatorCrate(' tools/release/local-registry-publish.mjs || + fail "local-registry native extension Cargo split aggregators must be generated in the Bun entrypoint" grep -Fq 'function pruneMissingLocalArtifactTargetDependencies(' tools/release/local-registry-publish.mjs || fail "local-registry Cargo source staging must prune unavailable non-host artifact dependencies" grep -Fq 'function nativeRuntimeArtifactManifests(' tools/release/local-registry-publish.mjs || @@ -404,7 +411,9 @@ grep -Fq 'export function manualCargoPackageSource(' tools/release/cargo-source- grep -Fq 'if (import.meta.main)' tools/release/package_oliphaunt_wasix_sdk_crate.mjs || fail "WASIX SDK crate packager must be import-safe for local-registry source staging" grep -Fq 'function cargoCratesRequirePythonGeneration(' tools/release/local-registry-publish.mjs || - fail "local-registry Cargo publish must keep generation paths on the explicit Python fallback" + fail "local-registry Cargo publish must declare its legacy fallback gate" +grep -Fq $'function cargoCratesRequirePythonGeneration(options, roots) {\n return false;\n}' tools/release/local-registry-publish.mjs || + fail "local-registry Cargo publish must not fall back to Python after native extension Cargo staging moved to Bun" grep -Fq 'function cargoIndexEntry(' tools/release/local-registry-publish.mjs || fail "local-registry Cargo index entries must be written by the Bun entrypoint for prebuilt crates" grep -Fq 'function clearLocalCargoHomeCache(' tools/release/local-registry-publish.mjs || @@ -425,13 +434,14 @@ if grep -Fq '["python3", "tools/release/local_registry_publish.py", "status"' to fail "local-registry status command must not delegate help or execution to Python" fi grep -Fq 'if (options.help)' tools/release/local-registry-publish.mjs || - fail "local-registry publish help must be handled before Python fallback" + fail "local-registry publish help must be handled before publish execution" grep -Fq '(surface === "cargo" && (options.dryRun || !cargoCratesRequirePythonGeneration(options, roots)))' tools/release/local-registry-publish.mjs || fail "local-registry Cargo real publish must use Bun for supported crate, release-asset, and source-staging roots" grep -Fq '(surface === "npm" && (options.dryRun || !npmTarballsRequirePythonGeneration(roots)))' tools/release/local-registry-publish.mjs || fail "local-registry npm real publish must use Bun for supported tarball, release-asset, and extension package roots" -grep -Fq '["python3", "tools/release/local_registry_publish.py", "publish", ...argv]' tools/release/local-registry-publish.mjs || - fail "local-registry real publish generation fallback must stay explicit to the publish command" +if grep -Fq '["python3", "tools/release/local_registry_publish.py", "publish", ...argv]' tools/release/local-registry-publish.mjs; then + fail "local-registry publish must not delegate to Python after all publish surfaces moved to Bun" +fi if grep -Fq '["python3", "tools/release/local_registry_publish.py", ...Bun.argv.slice(2)]' tools/release/local-registry-publish.mjs; then fail "local-registry command dispatch must not use a generic Python fallback" fi diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index f27d3b6a..a8b280e2 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -4,5 +4,4 @@ src/extensions/tools/check-extension-model.py extensions defer-extension-model-port generates and validates multi-language extension catalog, SDK metadata, docs, and evidence from one model tools/release/check_consumer_shape.py release-consumer-shape defer-release-graph-port validates cross-SDK package/runtime/install shape from generated release fixtures and source invariants tools/release/check_release_metadata.py release-metadata defer-release-graph-port validates release metadata and publish-step wiring through cached Bun release graph query rows -tools/release/local_registry_publish.py local-registry defer-local-registry-port remaining fallback for unported local-registry native extension Cargo synthesis tools/release/release.py release-orchestrator defer-release-graph-port owns protected release planning, validation, registry checks, publish dry-runs, and publish dispatch diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index eec03952..b3783bad 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -882,7 +882,7 @@ def check_liboliphaunt(findings: list[Finding]) -> None: native_macos_packager = read_text("tools/release/package-liboliphaunt-macos-assets.sh") native_windows_packager = read_text("tools/release/package-liboliphaunt-windows-assets.ps1") release_cli = read_text("tools/release/release.py") - local_registry_publisher = read_text("tools/release/local_registry_publish.py") + local_registry_publisher = read_text("tools/release/local-registry-publish.mjs") native_runtime_package_split_failures = native_npm_tool_split_failures( "src/runtimes/liboliphaunt/native/packages", tool_set="runtime", @@ -914,18 +914,18 @@ 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 "oliphaunt-tools-${libVersion}-*" in local_registry_publisher and "DEFAULT_CURRENT_ARTIFACT_ROOT" in local_registry_publisher - and "copy_release_asset_set" in local_registry_publisher - and "native_split_release_assets_ready" in local_registry_publisher - and "native_npm_release_assets_ready" in local_registry_publisher - and "native_split_release_asset_missing_message" in local_registry_publisher - and "native_npm_release_asset_missing_message" in local_registry_publisher - and "stage_release_asset_npm_packages(roots, registry_root, dry_run, result, strict)" in local_registry_publisher - and "cargo_dependency_name_matches_host_target" in local_registry_publisher + and "copyReleaseAssetSet" in local_registry_publisher + and "nativeSplitReleaseAssetsReady" in local_registry_publisher + and "nativeNpmReleaseAssetsReady" in local_registry_publisher + and "nativeSplitReleaseAssetMissingMessage" in local_registry_publisher + and "nativeNpmReleaseAssetMissingMessage" in local_registry_publisher + and "stageReleaseAssetNpmPackages(roots, registryRoot, result, strict)" in local_registry_publisher + and "cargoDependencyNameMatchesHostTarget" in local_registry_publisher and "host target artifact dependencies" in local_registry_publisher and "NON_PUBLISHABLE_LOCAL_CARGO_CRATE_PREFIXES" in local_registry_publisher - and "is_default_cargo_tmp_crate_artifact" in local_registry_publisher + and "isDefaultCargoTmpCrateArtifact" in local_registry_publisher and "ignored malformed Cargo scratch artifact" in local_registry_publisher and "NATIVE_RUNTIME_TOOL_STEMS" in native_optimizer and "NATIVE_TOOLS_TOOL_STEMS" in native_optimizer @@ -938,6 +938,7 @@ def check_liboliphaunt(findings: list[Finding]) -> None: "tools/release/package-liboliphaunt-macos-assets.sh", "tools/release/package-liboliphaunt-windows-assets.ps1", "tools/release/package-liboliphaunt-cargo-artifacts.mjs", + "tools/release/local-registry-publish.mjs", "tools/release/release.py", *native_runtime_package_split_failures, *native_tools_package_split_failures, @@ -2389,29 +2390,29 @@ 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") + local_registry_publisher = read_text("tools/release/local-registry-publish.mjs") 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, + and "if (strict) {\n fail(TOOL, 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", + "tools/release/local-registry-publish.mjs", severity="P0", ) require( findings, product, "wasix-local-registry-requires-target-artifacts", - "strict=strict" in local_registry_publisher + "strict)" in local_registry_publisher and "is missing local registry inputs for host target artifact dependencies" in local_registry_publisher - and "cargo_dependency_name_matches_host_target" in local_registry_publisher - and "prune_missing_feature_dependencies" in local_registry_publisher - and 'value.startswith("dep:")' in local_registry_publisher, + and "cargoDependencyNameMatchesHostTarget" in local_registry_publisher + and "pruneMissingFeatureDependencies" in local_registry_publisher + and 'value.startsWith("dep:")' in local_registry_publisher, "Strict local Cargo publishing must fail when release-shaped host target runtime/tools-AOT artifact crates are missing; non-host local pruning must also remove stale feature dep entries.", - "tools/release/local_registry_publish.py", + "tools/release/local-registry-publish.mjs", severity="P0", ) require( diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index e6a44fb6..0549a851 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -761,6 +761,10 @@ def validate_graph_files() -> None: or "function publishCargoCrates(" not in local_registry_publish or "function stageReleaseAssetCargoPackages(" not in local_registry_publish or "function stageCargoSourceCrates(" not in local_registry_publish + or "function packageNativeExtensionCargoCrates(" not in local_registry_publish + or "function writeNativeExtensionCargoCrate(" not in local_registry_publish + or "function buildNativeExtensionPartCrates(" not in local_registry_publish + or "function writeNativeExtensionSplitAggregatorCrate(" not in local_registry_publish or "function pruneMissingLocalArtifactTargetDependencies(" not in local_registry_publish or "function nativeRuntimeArtifactManifests(" not in local_registry_publish or "nativeSplitReleaseAssetNames(" not in local_registry_publish @@ -797,9 +801,10 @@ def validate_graph_files() -> None: or "tools/release/local_registry_metadata.mjs" not in local_registry_publish or "if (options.help)" not in local_registry_publish or '(surface === "cargo" && (options.dryRun || !cargoCratesRequirePythonGeneration(options, roots)))' not in local_registry_publish + or "function cargoCratesRequirePythonGeneration(options, roots) {\n return false;\n}" not in local_registry_publish or '(surface === "npm" && (options.dryRun || !npmTarballsRequirePythonGeneration(roots)))' not in local_registry_publish or "function npmTarballsRequirePythonGeneration(roots) {\n return false;\n}" not in local_registry_publish - or '["python3", "tools/release/local_registry_publish.py", "publish", ...argv]' not in local_registry_publish + or '["python3", "tools/release/local_registry_publish.py", "publish", ...argv]' in local_registry_publish or '["python3", "tools/release/local_registry_publish.py", "status"' in local_registry_publish or '["python3", "tools/release/local_registry_publish.py", ...Bun.argv.slice(2)]' in local_registry_publish or "tools/dev/bun.sh tools/release/local-registry-publish.mjs download" not in examples_readme @@ -807,7 +812,7 @@ def validate_graph_files() -> None: or "python3 tools/release/local_registry_publish.py" in examples_readme or "tools/dev/bun.sh tools/release/local-registry-publish.mjs" not in examples_local_registries ): - fail("example local-registry setup must use the Bun local-registry command surface and stage Cargo plus npm release/source/extension packages") + fail("example local-registry setup must use the Bun local-registry command surface and stage Cargo plus npm release/source/extension packages without Python publish fallback") if ( "publish-step-target-coverage [--product PRODUCT]" not in release_graph_query or "export function publishStepTargetCoverageRows(" not in release_graph_source @@ -1028,36 +1033,35 @@ def validate_release_setup_docs() -> None: 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: + publisher = read_text("tools/release/local-registry-publish.mjs") + if "const roots = artifactRoots.length > 0 ? artifactRoots : DEFAULT_ROOTS;" 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: + if "roots.push(" in publisher or "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: + if "stageLiboliphauntIcuNpmPayload" not in publisher or "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: + if "oliphaunt-tools-${libVersion}-*" 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 + or "if (strict) {\n fail(TOOL, 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 - ): + default_roots = publisher.split("const DEFAULT_ROOTS =", 1)[-1].split("];", 1)[0] + if "target/oliphaunt-wasix" in default_roots: 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: + if "function clearLocalCargoHomeCache(" 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 + "function stageReleaseAssetCargoPackages(" not in publisher or "package-liboliphaunt-cargo-artifacts.mjs" not in publisher or "package_broker_cargo_artifacts.mjs" not in publisher or "package_liboliphaunt_wasix_cargo_artifacts.mjs" not in publisher - or "host_cargo_release_target()" not in publisher - or "stage_release_asset_cargo_packages(roots, registry_root, dry_run, result, strict)" not in publisher - or "strict=strict" not in publisher - or "prune_missing_feature_dependencies" not in publisher + or "hostCargoReleaseTarget()" not in publisher + or "stageReleaseAssetCargoPackages(roots, registryRoot, result, strict)" not in publisher + or "strict)" not in publisher + or "pruneMissingFeatureDependencies" not in publisher ): fail("local registry Cargo publishing must generate runtime/tool artifact crates from staged release assets") artifacts = local_registry_metadata_json("local-publish-artifacts") @@ -1069,13 +1073,13 @@ def validate_local_registry_publisher() -> None: 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 "local_registry_metadata_json(\"local-publish-artifacts\"" not in publisher - or "local_registry_metadata_json(\"discover-extension-manifests\"" not in publisher + "function localPublishArtifacts(" not in publisher + or '"local-publish-artifacts"' not in publisher + or '"discover-extension-manifests"' not in publisher or "def extension_manifest_identity" in publisher - or "local_publish_artifact_names(aggregate_only=True)" not in publisher - or "local_publish_artifact_names()" not in publisher - or 'release_graph_rows(\n "artifact-targets"' not in publisher + or "local_publish_artifact_names(aggregate_only=True)" in publisher + or "local_publish_artifact_names()" in publisher + or "release_graph_rows(" in publisher or "import product_metadata" in publisher or "ci_aggregate_release_asset_artifact_name(\"liboliphaunt-native\")" in publisher or "ci_wasix_runtime_artifact_names()" in publisher diff --git a/tools/release/local-registry-publish.mjs b/tools/release/local-registry-publish.mjs index 0703f85f..eeab46aa 100644 --- a/tools/release/local-registry-publish.mjs +++ b/tools/release/local-registry-publish.mjs @@ -13,6 +13,7 @@ import { mkdirSync, openSync, readFileSync, + readSync, readdirSync, rmSync, statSync, @@ -45,6 +46,8 @@ const DEFAULT_CURRENT_ARTIFACT_ROOT = path.join(ROOT, "target/local-registry-cur const DEFAULT_ARTIFACT_ROOT = path.join(ROOT, "target/local-registry-artifacts"); const NPM_PACKAGE_SIZE_LIMIT_BYTES = 10 * 1024 * 1024; const CARGO_PACKAGE_SIZE_LIMIT_BYTES = 10 * 1024 * 1024; +const CARGO_EXTENSION_PART_BYTES = 7 * 1024 * 1024; +const CARGO_EXTENSION_SPLIT_THRESHOLD_BYTES = 9 * 1024 * 1024; const CRATES_IO_INDEX = "https://github.com/rust-lang/crates.io-index"; const LEGACY_WASIX_ARTIFACT_CRATES = new Set([ "oliphaunt-wasix-assets", @@ -763,6 +766,27 @@ function extensionNpmPayloadPackage(sqlName, target, index) { return `${extensionNpmTargetPackage(sqlName, target)}-payload-${index}`; } +function nativeExtensionCargoPackageName(product, target) { + return `${product}-${target}`; +} + +function nativeExtensionCargoLinksName(product, target) { + const stem = `extension_${product.replace(/^oliphaunt-extension-/u, "")}_${target}`; + return `oliphaunt_artifact_${stem.replaceAll("-", "_")}`; +} + +function nativeExtensionCargoPartPackageName(product, target, index) { + return `${nativeExtensionCargoPackageName(product, target)}-part-${String(index).padStart(3, "0")}`; +} + +function rustCrateIdent(crateName) { + return crateName.replaceAll("-", "_"); +} + +function tomlString(value) { + return JSON.stringify(value); +} + function npmPackageIdentity(tarball) { const members = tryCommandOutput(["tar", "-tzf", tarball]); if (members === null) { @@ -1979,6 +2003,679 @@ function npmTarballsRequirePythonGeneration(roots) { return false; } +function writeNativeExtensionCargoPartCrate(crateDir, { product, version, sqlName, target, index }) { + const name = nativeExtensionCargoPartPackageName(product, target, index); + mkdirSync(path.join(crateDir, "src"), { recursive: true }); + writeFileSync( + path.join(crateDir, "Cargo.toml"), + `[package] +name = "${name}" +version = "${version}" +edition = "2024" +rust-version = "1.93" +description = "Cargo payload part ${String(index).padStart(3, "0")} for the ${sqlName} 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] +`, + ); + writeFileSync( + path.join(crateDir, "README.md"), + `# ${name} + +Cargo payload part for the \`${sqlName}\` Oliphaunt native extension on \`${target}\`. +Applications do not depend on this crate directly. +`, + ); + writeFileSync( + path.join(crateDir, "src/lib.rs"), + `pub const PRODUCT: &str = "${product}"; +pub const KIND: &str = "extension-part"; +pub const SQL_NAME: &str = "${sqlName}"; +pub const RELEASE_TARGET: &str = "${target}"; +pub const PART_INDEX: usize = ${index}; +pub const PAYLOAD_ROOT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/payload"); +`, + ); +} + +function writeChunk(file, data) { + mkdirSync(path.dirname(file), { recursive: true }); + writeFileSync(file, data); +} + +function copyPayloadFile(source, destination) { + mkdirSync(path.dirname(destination), { recursive: true }); + copyFileSync(source, destination); +} + +function buildNativeExtensionPartCrates(runtimeDir, sourceRoot, { + product, + version, + sqlName, + target, + partBytes = CARGO_EXTENSION_PART_BYTES, +}) { + const partDirs = []; + let currentDir = null; + let currentSize = 0; + + const startPart = () => { + const index = partDirs.length; + const partDir = path.join(sourceRoot, nativeExtensionCargoPartPackageName(product, target, index)); + writeNativeExtensionCargoPartCrate(partDir, { product, version, sqlName, target, index }); + partDirs.push(partDir); + return partDir; + }; + + for (const source of walkFiles(runtimeDir)) { + const relative = path.relative(runtimeDir, source).split(path.sep).join("/"); + const size = statSync(source).size; + if (size > partBytes) { + currentDir = null; + currentSize = 0; + const fd = openSync(source, "r"); + try { + let partIndex = 0; + let offset = 0; + while (offset < size) { + const length = Math.min(partBytes, size - offset); + const buffer = Buffer.allocUnsafe(length); + const bytesRead = readSync(fd, buffer, 0, length, offset); + if (bytesRead <= 0) { + break; + } + const partDir = startPart(); + writeChunk( + path.join(partDir, "payload", "chunks", `${relative}.part${String(partIndex).padStart(3, "0")}`), + buffer.subarray(0, bytesRead), + ); + offset += bytesRead; + partIndex += 1; + } + } finally { + closeSync(fd); + } + continue; + } + if (currentDir === null || currentSize + size > partBytes) { + currentDir = startPart(); + currentSize = 0; + } + copyPayloadFile(source, path.join(currentDir, "payload", "files", relative)); + currentSize += size; + } + + if (partDirs.length === 0) { + throw new Error(`${product}@${version} generated no native extension Cargo part crates`); + } + return partDirs; +} + +const NATIVE_EXTENSION_AGGREGATOR_BUILD_RS = String.raw`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(std::path::MAIN_SEPARATOR, "/"); + 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(std::path::MAIN_SEPARATOR, "/"); + 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) +} +`; + +function writeNativeExtensionSplitAggregatorCrate(crateDir, { + product, + version, + sqlName, + target, + triple, + partDirs, +}) { + const name = nativeExtensionCargoPackageName(product, target); + const links = nativeExtensionCargoLinksName(product, target); + rmSync(path.join(crateDir, "payload"), { recursive: true, force: true }); + const dependencyLines = []; + const partRoots = []; + for (let index = 0; index < partDirs.length; index += 1) { + const dependencyName = nativeExtensionCargoPartPackageName(product, target, index); + const dependencyPath = path.relative(crateDir, partDirs[index]).split(path.sep).join("/"); + dependencyLines.push(`${dependencyName} = { version = "=${version}", path = "${dependencyPath}" }`); + partRoots.push(` ${rustCrateIdent(dependencyName)}::PAYLOAD_ROOT,`); + } + writeFileSync( + path.join(crateDir, "Cargo.toml"), + `[package] +name = "${name}" +version = "${version}" +edition = "2024" +rust-version = "1.93" +description = "Cargo artifact crate for the ${sqlName} 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" +links = "${links}" +build = "build.rs" +include = ["Cargo.toml", "README.md", "build.rs", "src/**"] + +[lib] +path = "src/lib.rs" + +[build-dependencies] +sha2 = "0.10" +${dependencyLines.join("\n")} + +[workspace] +`, + ); + writeFileSync( + path.join(crateDir, "build.rs"), + NATIVE_EXTENSION_AGGREGATOR_BUILD_RS + .replace("__SCHEMA__", tomlString("oliphaunt-artifact-manifest-v1")) + .replace("__PRODUCT__", tomlString(product)) + .replace("__TARGET__", tomlString(triple)) + .replace("__EXTENSION__", tomlString(sqlName)) + .replace("__PART_ROOTS__", partRoots.join("\n")), + ); +} + +function cargoPackage(crateDir, targetDir, { noVerify = false } = {}) { + const manifest = path.join(crateDir, "Cargo.toml"); + const { name, version } = readCargoPackageNameVersion(manifest, { fail: localFail, rel }); + const command = [ + "cargo", + "package", + "--manifest-path", + manifest, + "--target-dir", + targetDir, + "--allow-dirty", + ]; + if (noVerify) { + command.push("--no-verify"); + } + const result = spawnSync(command[0], command.slice(1), { + cwd: ROOT, + env: { ...process.env, OLIPHAUNT_ARTIFACT_CRATE_REQUIRE_PAYLOAD: "1" }, + stdio: "inherit", + }); + if (result.error) { + fail(TOOL, `${command[0]} failed to start: ${result.error.message}`); + } + if (result.status !== 0) { + process.exit(result.status ?? 1); + } + const cratePath = path.join(targetDir, "package", `${name}-${version}.crate`); + if (!isFile(cratePath)) { + fail(TOOL, `cargo package did not create ${rel(cratePath)}`); + } + return cratePath; +} + +function discardCargoPackageArtifact(cratePath) { + rmSync(cratePath, { force: true }); + rmSync(path.join(path.dirname(cratePath), "tmp-crate", path.basename(cratePath)), { force: true }); +} + +function writeNativeExtensionCargoCrate(crateDir, { + product, + version, + sqlName, + target, + triple, + asset, +}) { + const name = nativeExtensionCargoPackageName(product, target); + const links = nativeExtensionCargoLinksName(product, target); + const runtimeDir = path.join(crateDir, "payload"); + extractExtensionRuntime(asset, runtimeDir); + stripExtensionModules(runtimeDir, target); + if (walkFiles(runtimeDir).length === 0) { + throw new Error(`${rel(asset)} did not contain extension runtime files`); + } + mkdirSync(path.join(crateDir, "src"), { recursive: true }); + writeFileSync( + path.join(crateDir, "README.md"), + `# ${name} + +Cargo artifact crate for the \`${sqlName}\` Oliphaunt native extension on \`${target}\`. +`, + ); + writeFileSync( + path.join(crateDir, "Cargo.toml"), + `[package] +name = "${name}" +version = "${version}" +edition = "2024" +rust-version = "1.93" +description = "Cargo artifact crate for the ${sqlName} 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" +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] +`, + ); + writeFileSync( + path.join(crateDir, "src/lib.rs"), + `pub const PRODUCT: &str = "${product}"; +pub const KIND: &str = "extension"; +pub const SQL_NAME: &str = "${sqlName}"; +pub const RELEASE_TARGET: &str = "${target}"; +pub const CARGO_TARGET: &str = "${triple}"; +`, + ); + writeFileSync( + path.join(crateDir, "build.rs"), + `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.stringify(product)}; +const VERSION: &str = env!("CARGO_PKG_VERSION"); +const KIND: &str = "extension"; +const TARGET: &str = ${JSON.stringify(triple)}; +const EXTENSION: &str = ${JSON.stringify(sqlName)}; + +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") + .to_string_lossy() + .replace(std::path::MAIN_SEPARATOR, "/"); + let sha256 = sha256_file(&file); + text.push_str(&format!( + "\\n[[files]]\\nsource = {:?}\\nrelative = {:?}\\nsha256 = {sha256:?}\\nexecutable = false\\n", + file.display().to_string(), + relative, + )); + } + 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()) +} +`, + ); +} + +function packageNativeExtensionCargoCrates(roots, stagingRoot, target, strict, result) { + if (target === null) { + result.skipped.push("current host does not map to a supported native extension Cargo target"); + return []; + } + const triple = cargoTargetTriple(target); + if (triple === null) { + result.skipped.push(`unsupported native extension Cargo target ${target}`); + return []; + } + const manifests = discoverExtensionManifests(roots); + if (manifests.length === 0) { + result.skipped.push("no extension-artifacts.json manifests found for native extension Cargo crates"); + return []; + } + + const sourceRoot = path.join(stagingRoot, "native-extension-sources"); + const outputDir = path.join(stagingRoot, "native-extension-crates"); + const cargoTargetDir = path.join(stagingRoot, "native-extension-cargo-target"); + rmSync(sourceRoot, { recursive: true, force: true }); + rmSync(outputDir, { recursive: true, force: true }); + rmSync(cargoTargetDir, { recursive: true, force: true }); + mkdirSync(sourceRoot, { recursive: true }); + mkdirSync(outputDir, { recursive: true }); + + const outputs = []; + const packageOptions = { root: ROOT, fail: localFail, rel }; + for (const manifestPath of manifests) { + const manifest = readJsonFile(manifestPath); + const extensionDir = path.dirname(manifestPath); + const { product, version, sqlName } = manifest; + if (![product, version, sqlName].every((value) => typeof value === "string" && value.length > 0)) { + result.skipped.push(`${rel(manifestPath)} is missing product, version, or sqlName`); + continue; + } + const releaseManifest = extensionReleaseManifest(extensionDir, product, version); + const asset = extensionRuntimeAsset(extensionDir, Object.keys(releaseManifest).length > 0 ? releaseManifest : manifest, target); + if (asset === null) { + result.skipped.push(`${product}@${version} has no ${target} native runtime asset`); + continue; + } + const name = nativeExtensionCargoPackageName(product, target); + const crateDir = path.join(sourceRoot, name); + try { + writeNativeExtensionCargoCrate(crateDir, { + product, + version, + sqlName, + target, + triple, + asset, + }); + let cratePath = cargoPackage(crateDir, cargoTargetDir); + let size = statSync(cratePath).size; + if (size > CARGO_EXTENSION_SPLIT_THRESHOLD_BYTES) { + discardCargoPackageArtifact(cratePath); + const partDirs = buildNativeExtensionPartCrates(path.join(crateDir, "payload"), sourceRoot, { + product, + version, + sqlName, + target, + }); + writeNativeExtensionSplitAggregatorCrate(crateDir, { + product, + version, + sqlName, + target, + triple, + partDirs, + }); + let partFailed = false; + for (const partDir of partDirs) { + const partCratePath = cargoPackage(partDir, cargoTargetDir); + const partSize = statSync(partCratePath).size; + if (partSize > CARGO_PACKAGE_SIZE_LIMIT_BYTES) { + const message = `${rel(partCratePath)} is ${partSize} bytes, above the crates.io 10 MiB package limit`; + result.skipped.push(message); + if (strict) { + fail(TOOL, message); + } + partFailed = true; + continue; + } + const output = path.join(outputDir, path.basename(partCratePath)); + copyFileSync(partCratePath, output); + outputs.push(output); + } + if (partFailed) { + continue; + } + cratePath = manualCargoPackageSource( + path.join(crateDir, "Cargo.toml"), + path.join(cargoTargetDir, "manual-package"), + packageOptions, + ); + size = statSync(cratePath).size; + if (size > CARGO_PACKAGE_SIZE_LIMIT_BYTES) { + const message = `${rel(cratePath)} is ${size} bytes after splitting, above the crates.io 10 MiB package limit`; + result.skipped.push(message); + if (strict) { + fail(TOOL, message); + } + continue; + } + } + const output = path.join(outputDir, path.basename(cratePath)); + copyFileSync(cratePath, output); + outputs.push(output); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + result.skipped.push(message); + if (strict) { + throw error; + } + } + } + result.staged.push(...outputs.map(rel)); + return outputs; +} + function writeVerdaccioConfig(root, port) { const resolvedRoot = path.resolve(root); const config = path.join(resolvedRoot, "config.yaml"); @@ -2657,43 +3354,7 @@ async function stageCargoSourceCrates(roots, registryRoot, result, strict) { } function cargoCratesRequirePythonGeneration(options, roots) { - if (options.artifactRoots.length === 0) { - return true; - } - if (discoverExtensionManifests(roots).length > 0) { - return true; - } - let hasCargoInput = discoverFiles(roots, [".crate"]).length > 0; - for (const root of roots) { - const stats = statSync(root); - const rootName = path.basename(root); - if ( - stats.isFile() && - ( - /^(liboliphaunt|oliphaunt-tools)-[^/]+\.(tar\.gz|zip)$/u.test(rootName) || - /^oliphaunt-broker-[^/]+\.(tar\.gz|zip)$/u.test(rootName) || - /^liboliphaunt-wasix-[^/]+\.tar\.zst$/u.test(rootName) - ) - ) { - hasCargoInput = true; - continue; - } - if (!stats.isDirectory()) { - continue; - } - for (const file of walkFiles(root)) { - const name = path.basename(file); - if ( - /^(liboliphaunt|oliphaunt-tools)-[^/]+\.(tar\.gz|zip)$/u.test(name) || - /^oliphaunt-broker-[^/]+\.(tar\.gz|zip)$/u.test(name) || - /^liboliphaunt-wasix-[^/]+\.tar\.zst$/u.test(name) - ) { - hasCargoInput = true; - break; - } - } - } - return !hasCargoInput; + return false; } function cargoCratePriority(cratePath, registryRoot) { @@ -2879,11 +3540,15 @@ async function publishCargoCrates(roots, registryRoot, strict) { if (generatedRoots.length > 0) { roots = [...roots, ...generatedRoots]; } - const extensionTarget = hostCargoReleaseTarget(); - if (extensionTarget === null) { - result.skipped.push("current host does not map to a supported native extension Cargo target"); - } else { - result.skipped.push("no extension-artifacts.json manifests found for native extension Cargo crates"); + const extensionRoots = packageNativeExtensionCargoCrates( + roots, + path.join(registryRoot, "cargo-generated"), + hostCargoReleaseTarget(), + strict, + result, + ); + if (extensionRoots.length > 0) { + roots = [...roots, ...extensionRoots]; } const crates = discoverFiles(roots, [".crate"]); if (crates.length === 0) { @@ -3083,8 +3748,7 @@ async function publish(argv) { } const roots = discoverRoots(options.artifactRoots); if (!canPublishInBun(options, roots)) { - run(TOOL, ["python3", "tools/release/local_registry_publish.py", "publish", ...argv]); - return; + fail(TOOL, "publish surface is not implemented in the Bun local-registry entrypoint", 2); } mkdirSync(options.registryRoot, { recursive: true }); const results = []; diff --git a/tools/release/local_registry_publish.py b/tools/release/local_registry_publish.py deleted file mode 100755 index c5a2c06a..00000000 --- a/tools/release/local_registry_publish.py +++ /dev/null @@ -1,3082 +0,0 @@ -#!/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 gzip -import hashlib -import json -import os -import platform as host_platform -import re -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 functools import lru_cache -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_CURRENT_ARTIFACT_ROOT = ROOT / "target" / "local-registry-current" -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 -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", -} -NON_PUBLISHABLE_LOCAL_CARGO_CRATE_PREFIXES = ( - "oliphaunt-perf-", -) - - -def release_graph_json(command: str, args: tuple[str, ...] = ()) -> Any: - try: - completed = run( - ["tools/dev/bun.sh", "tools/release/release_graph_query.mjs", command, *args], - capture=True, - ) - except subprocess.CalledProcessError as error: - detail = (error.stderr or "").strip() - if detail: - raise RuntimeError(f"release graph {command} query failed: {detail}") from error - raise RuntimeError(f"release graph {command} query failed with exit code {error.returncode}") from error - try: - return json.loads(completed.stdout) - except json.JSONDecodeError as error: - raise RuntimeError(f"release graph {command} query did not return valid JSON: {error}") from error - - -def local_registry_metadata_json(command: str, args: tuple[str, ...] = ()) -> Any: - try: - completed = run( - ["tools/dev/bun.sh", "tools/release/local_registry_metadata.mjs", command, *args], - capture=True, - ) - except subprocess.CalledProcessError as error: - detail = (error.stderr or "").strip() - if detail: - raise RuntimeError(f"local registry metadata {command} query failed: {detail}") from error - raise RuntimeError(f"local registry metadata {command} query failed with exit code {error.returncode}") from error - try: - return json.loads(completed.stdout) - except json.JSONDecodeError as error: - raise RuntimeError(f"local registry metadata {command} query did not return valid JSON: {error}") from error - - -@lru_cache(maxsize=None) -def release_graph_rows(command: str, args: tuple[str, ...] = ()) -> tuple[dict[str, Any], ...]: - rows = release_graph_json(command, args) - if not isinstance(rows, list) or not all(isinstance(row, dict) for row in rows): - raise RuntimeError(f"release graph {command} query must return a JSON object list") - return tuple(rows) - - -def local_publish_aggregate_artifacts() -> list[str]: - return local_publish_artifact_names(aggregate_only=True) - - -def local_publish_artifacts() -> list[str]: - artifacts = local_publish_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 local_publish_artifact_names(*, aggregate_only: bool = False) -> list[str]: - args = ("--aggregate-only",) if aggregate_only else () - names = local_registry_metadata_json("local-publish-artifacts", args) - if not isinstance(names, list) or not all(isinstance(name, str) and name for name in names): - raise RuntimeError("local registry metadata local-publish-artifacts must return a non-empty string list") - if not names: - raise RuntimeError("local registry metadata returned no local-publish artifacts") - duplicates = sorted({name for name in names if names.count(name) > 1}) - if duplicates: - raise RuntimeError("local registry metadata returned duplicate local-publish artifacts: " + ", ".join(duplicates)) - return names - - -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(artifact_roots: Iterable[Path]) -> list[Path]: - explicit_roots = list(artifact_roots) - roots = explicit_roots or [ - DEFAULT_CURRENT_ARTIFACT_ROOT, - DEFAULT_ARTIFACT_ROOT, - 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" / "extension-artifacts", - ] - 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 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]: - selected: dict[str, tuple[Path, Path]] = {} - destination_resolved = destination.resolve() - for root in roots: - if not root.is_dir(): - continue - root_candidates: list[Path] = [] - for pattern in patterns: - for path in root.rglob(pattern): - if not path.is_file(): - continue - try: - path.resolve().relative_to(destination_resolved) - continue - except ValueError: - pass - root_candidates.append(path) - for path in sorted(root_candidates): - existing = selected.get(path.name) - if existing is None: - selected[path.name] = (path, root) - continue - existing_path, existing_root = existing - if existing_root.resolve() != root.resolve(): - continue - if file_sha256(existing_path) != file_sha256(path): - raise RuntimeError( - f"conflicting release asset {path.name} within {rel(root)}: " - f"{rel(existing_path)} and {rel(path)} differ" - ) - if not selected: - return [] - - shutil.rmtree(destination, ignore_errors=True) - destination.mkdir(parents=True, exist_ok=True) - copied: list[Path] = [] - for source, _root in sorted(selected.values(), key=lambda item: item[0].name): - target = destination / source.name - shutil.copy2(source, target) - copied.append(target) - return copied - - -def release_asset_candidate(root: Path, name: str, destination: Path) -> Path | None: - destination_resolved = destination.resolve() - if root.is_file() and root.name == name: - return root - if not root.is_dir(): - return None - - candidates: list[Path] = [] - for path in root.rglob(name): - if not path.is_file(): - continue - try: - path.resolve().relative_to(destination_resolved) - continue - except ValueError: - pass - candidates.append(path) - if not candidates: - return None - - selected = sorted(candidates)[0] - for candidate in candidates[1:]: - if file_sha256(candidate) != file_sha256(selected): - raise RuntimeError( - f"conflicting release asset {name} within {rel(root)}: " - f"{rel(selected)} and {rel(candidate)} differ" - ) - return selected - - -def copy_release_asset_set( - roots: list[Path], - destination: Path, - names: tuple[str, ...], -) -> list[Path]: - for root in roots: - selected: list[Path] = [] - for name in names: - candidate = release_asset_candidate(root, name, destination) - if candidate is None: - break - selected.append(candidate) - if len(selected) != len(names): - continue - - shutil.rmtree(destination, ignore_errors=True) - destination.mkdir(parents=True, exist_ok=True) - copied: list[Path] = [] - for source in selected: - target = destination / source.name - shutil.copy2(source, target) - copied.append(target) - return copied - return [] - - -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 release_asset_dir_has_exact_files(asset_dir: Path, names: tuple[str, ...]) -> bool: - return asset_dir.is_dir() and all((asset_dir / name).is_file() for name in names) - - -def missing_release_asset_names(asset_dir: Path, names: tuple[str, ...]) -> list[str]: - return [name for name in names if not (asset_dir / name).is_file()] - - -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 native_release_asset_name(version: str, target: str, kind: str) -> str: - matches: list[str] = [] - for artifact in release_graph_rows( - "artifact-targets", - ("--product", "liboliphaunt-native", "--kind", kind, "--published-only"), - ): - if artifact.get("target") != target: - continue - surfaces = artifact.get("surfaces") - if not isinstance(surfaces, list) or not all(isinstance(surface, str) for surface in surfaces): - raise RuntimeError(f"release graph artifact target {target}/{kind} surfaces must be a string list") - if "rust-native-direct" not in surfaces and "typescript-native-direct" not in surfaces: - continue - asset = artifact.get("asset") - if not isinstance(asset, str) or not asset: - raise RuntimeError(f"release graph artifact target {target}/{kind} asset must be a non-empty string") - matches.append(asset.format(version=version)) - if len(matches) != 1: - raise RuntimeError( - f"expected exactly one published liboliphaunt-native {kind} asset for {target}, got {matches}" - ) - return matches[0] - - -def native_split_release_asset_names(version: str, target: str) -> tuple[str, str]: - return ( - native_release_asset_name(version, target, "native-runtime"), - native_release_asset_name(version, target, "native-tools"), - ) - - -def native_npm_release_asset_names(version: str, target: str) -> tuple[str, str, str]: - return ( - *native_split_release_asset_names(version, target), - f"liboliphaunt-{version}-icu-data.tar.gz", - ) - - -def native_split_release_assets_ready(asset_dir: Path, version: str, target: str) -> tuple[bool, list[str]]: - required = native_split_release_asset_names(version, target) - missing = missing_release_asset_names(asset_dir, required) - return release_asset_dir_has_exact_files(asset_dir, required), missing - - -def native_npm_release_assets_ready(asset_dir: Path, version: str, target: str) -> tuple[bool, list[str]]: - required = native_npm_release_asset_names(version, target) - missing = missing_release_asset_names(asset_dir, required) - return release_asset_dir_has_exact_files(asset_dir, required), missing - - -def native_split_release_asset_missing_message(asset_dir: Path, version: str, target: str, missing: list[str]) -> str: - required = ", ".join(native_split_release_asset_names(version, target)) - return ( - f"native split release asset staging for {target} requires runtime and tools assets " - f"({required}) under {rel(asset_dir)}; missing {', '.join(missing)}" - ) - - -def native_npm_release_asset_missing_message(asset_dir: Path, version: str, target: str, missing: list[str]) -> str: - required = ", ".join(native_npm_release_asset_names(version, target)) - return ( - f"native npm artifact staging for {target} requires runtime, tools, and ICU assets " - f"({required}) under {rel(asset_dir)}; missing {', '.join(missing)}" - ) - - -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 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"]} - 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]: - args: list[str] = [] - for root in roots: - args.extend(["--root", str(root)]) - if not args: - return [] - values = local_registry_metadata_json("discover-extension-manifests", tuple(args)) - if not isinstance(values, list) or not all(isinstance(value, str) and value for value in values): - raise RuntimeError("local registry metadata discover-extension-manifests must return a string list") - return [ - Path(value) if Path(value).is_absolute() else ROOT / value - for value in values - ] - - -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]: - root = root.resolve() - 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}", - "max_body_size: 100mb", - "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 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(): - 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", - ) - if npm_auth_is_valid(registry_url, npmrc): - return npmrc - npmrc.unlink() - 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 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 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_CURRENT_ARTIFACT_ROOT, 60), - (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, - strict: bool, -) -> 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") - lib_patterns = (f"liboliphaunt-{lib_version}-*", f"oliphaunt-tools-{lib_version}-*") - copied_lib = ( - [] - if target is None - else copy_release_asset_set(roots, lib_asset_dir, native_npm_release_asset_names(lib_version, target)) - ) - if copied_lib or (release_asset_dir_selected(roots, lib_asset_dir) and release.liboliphaunt_release_assets_ready()): - if target is None: - result.add_skip("current host does not map to a supported native npm artifact target") - else: - ready, missing = native_npm_release_assets_ready(lib_asset_dir, lib_version, target) - if 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, - ) - ) - else: - message = native_npm_release_asset_missing_message( - lib_asset_dir, - lib_version, - target, - missing, - ) - result.add_skip(message) - if strict: - raise RuntimeError(message) - 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 ( - 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)") - 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, strict) - 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 = 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: - 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: - identity = npm_package_identity(tarball) - if dry_run: - 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]): - command = [ - "npm", - "unpublish", - f"{identity[0]}@{identity[1]}", - "--registry", - registry_url, - "--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) - 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 - - -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 cargo_dependency_name_matches_host_target(name: str) -> bool: - host_target = host_cargo_release_target() - if host_target is None: - return True - host_triple = cargo_target_triple(host_target) - host_markers = [host_target] - if host_triple is not None: - host_markers.append(host_triple) - return any( - name.endswith(f"-{marker}") - or f"-{marker}-" in name - or f"-aot-{marker}" in name - for marker in host_markers - ) - - -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() - 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 - missing_packages = sorted({package for _header, missing in removed for package in missing}) - if strict: - host_missing_packages = sorted( - package for package in missing_packages if cargo_dependency_name_matches_host_target(package) - ) - if not host_missing_packages: - strict = False - else: - raise RuntimeError( - f"{rel(manifest)} is missing local registry inputs for host target artifact dependencies: " - + ", ".join(host_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( - [ - "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, - strict: bool, -) -> 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" - 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( - oliphaunt_manifest, - available_package_names, - result, - strict=strict, - ) - generated.append(manual_cargo_package_source(oliphaunt_manifest, output_dir)) - - 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, - strict=strict, - ) - generated.append(manual_cargo_package_source(wasix_manifest, output_dir)) - - 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, *, include_parts: bool = False) -> list[Path]: - if not source_root.is_dir(): - 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) - name, _version = read_cargo_package_name_version(manifest) - if "-part-" in name and not include_parts: - continue - result.append(manifest) - return result - - -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 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 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, - *, - 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) - 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) - (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, - ) - 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, - 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) - 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: - 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], local_package_names: set[str]) -> dict[str, Any]: - registry = dep.get("registry") - if dep["name"] in local_package_names: - registry = None - elif registry is None: - registry = CRATES_IO_INDEX - 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, 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, local_package_names) - 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 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 - 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), - (DEFAULT_CURRENT_ARTIFACT_ROOT, 60), - (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 is_default_cargo_tmp_crate_artifact(path: Path) -> bool: - try: - path.resolve().relative_to((ROOT / "target/package/tmp-crate").resolve()) - except ValueError: - return False - return True - - -def stage_release_asset_cargo_packages( - roots: list[Path], - registry_root: Path, - dry_run: bool, - result: SurfaceResult, - strict: bool, -) -> 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}-*", f"oliphaunt-tools-{lib_version}-*") - lib_asset_dir = ROOT / "target" / "liboliphaunt" / "release-assets" - copied_lib_assets = ( - [] - if host_target is None - else copy_release_asset_set(roots, lib_asset_dir, native_split_release_asset_names(lib_version, host_target)) - ) - 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_selected(roots, lib_asset_dir) - and release_asset_dir_has_files(lib_asset_dir, lib_patterns) - ): - ready, missing = native_split_release_assets_ready(lib_asset_dir, lib_version, host_target) - if not ready: - message = native_split_release_asset_missing_message( - lib_asset_dir, - lib_version, - host_target, - missing, - ) - result.add_skip(message) - if strict: - raise RuntimeError(message) - else: - if copied_lib_assets: - result.staged.append( - f"staged {len(copied_lib_assets)} liboliphaunt release asset(s) for Cargo" - ) - run( - [ - "tools/dev/bun.sh", - "tools/release/package-liboliphaunt-cargo-artifacts.mjs", - "--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_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" - ) - run( - [ - str(ROOT / "tools/dev/bun.sh"), - "tools/release/package_broker_cargo_artifacts.mjs", - "--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_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" - ) - run( - [ - "tools/dev/bun.sh", - "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", - "--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: - registry_root = registry_root.resolve() - result = SurfaceResult("cargo") - release_asset_roots = stage_release_asset_cargo_packages(roots, registry_root, dry_run, result, strict) - if release_asset_roots: - roots = [*roots, *release_asset_roots] - generated_roots = stage_cargo_source_crates(roots, registry_root, dry_run, result, strict) - 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") - 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", - ) - - 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)): - if crate_path.name.startswith(NON_PUBLISHABLE_LOCAL_CARGO_CRATE_PREFIXES): - result.add_skip(f"ignored non-publishable local Cargo crate artifact {crate_path.name}") - continue - try: - package = cargo_metadata_for_crate(crate_path) - except RuntimeError as error: - if is_default_cargo_tmp_crate_artifact(crate_path) and "does not contain Cargo.toml" in str(error): - result.add_skip(f"ignored malformed Cargo scratch artifact {rel(crate_path)}") - continue - result.add_skip(str(error)) - if strict: - raise - continue - if package.get("name") in LEGACY_WASIX_ARTIFACT_CRATES: - 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) - - 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) - 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", - ) - 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 - - -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() From 4cbd310aedc78ad6e509383bfd35d85e1d43c0fe Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 03:14:57 +0000 Subject: [PATCH 252/308] chore: remove dead python release helpers --- docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 8 ++++++++ src/extensions/tools/check-extension-model.py | 4 ---- tools/release/check_consumer_shape.py | 4 ---- tools/release/release.py | 11 ----------- 4 files changed, 8 insertions(+), 19 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 983da36d..7a90aac3 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,14 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-28: Removed four confirmed-dead Python helpers: + `cargo_package_args` and `supported_publish_targets` from + `tools/release/release.py`, `product_string_list` from + `tools/release/check_consumer_shape.py`, and `format_toml_string_list` from + `src/extensions/tools/check-extension-model.py`. A repo-wide reference scan + showed no callers for any of these symbols; `cargo_package_args` was a stale + twin of the still-used `cargo_publish_args`, and the publish-target helper + remains only where it is actually used in `check_release_metadata.py`. - 2026-06-27: Moved the active release metadata check orchestration to the Bun entrypoint `tools/release/release-check.mjs`. Moon `release-tools:check`, `release-tools:release-check`, and the release workflow now call the Bun diff --git a/src/extensions/tools/check-extension-model.py b/src/extensions/tools/check-extension-model.py index 778771b5..59028f98 100755 --- a/src/extensions/tools/check-extension-model.py +++ b/src/extensions/tools/check-extension-model.py @@ -1811,10 +1811,6 @@ def public_extensions(catalog: dict) -> list[dict]: return rows -def format_toml_string_list(values: list[str]) -> str: - return "[" + ", ".join(json.dumps(value) for value in values) + "]" - - def write_evidence_files(catalog: dict) -> None: public_rows = public_extensions(catalog) matrix_lines = [ diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index b3783bad..4ed037a3 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -278,10 +278,6 @@ def package_path(product: str) -> str: return path -def product_string_list(product: str, key: str) -> list[str]: - return string_list(product_config(product).get(key, []), f"{product}.{key}") - - @lru_cache(maxsize=1) def product_version_rows() -> tuple[dict[str, Any], ...]: rows = release_graph_rows("product-versions") diff --git a/tools/release/release.py b/tools/release/release.py index b523c7a4..4a9d9aed 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -872,10 +872,6 @@ def cargo_publish_args(allow_dirty: bool) -> list[str]: return ["--allow-dirty"] if allow_dirty else [] -def cargo_package_args(allow_dirty: bool) -> list[str]: - return ["--allow-dirty"] if allow_dirty else [] - - def passthrough_value(args: list[str], name: str) -> str | None: index = 0 while index < len(args): @@ -937,13 +933,6 @@ def publish_step_target_coverage(product: str) -> dict[str, set[str]]: return coverage -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_config(product) value = config.get("extension_sql_name") From 028cc61f167f1c7f56086d0909ef325fee2488ba Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 03:20:23 +0000 Subject: [PATCH 253/308] chore: route extension model checks through bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 7 +++++++ .../examples-ci-release-validation.md | 2 +- src/extensions/artifacts/native/moon.yml | 2 +- src/extensions/artifacts/wasix/moon.yml | 2 +- src/extensions/model/moon.yml | 2 +- .../tools/check-extension-model.mjs | 21 +++++++++++++++++++ .../assertions/assert-source-inputs.mjs | 3 ++- tools/release/check_release_metadata.py | 8 +++++++ tools/release/sync-release-pr.mjs | 4 ++-- 9 files changed, 44 insertions(+), 7 deletions(-) create mode 100755 src/extensions/tools/check-extension-model.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 7a90aac3..1d46ec2c 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,13 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-28: Added the Bun extension-model command surface + `src/extensions/tools/check-extension-model.mjs` and moved active Moon + checks, source-input assertions, release PR evidence sync, and maintained + validation docs off direct `python3 src/extensions/tools/check-extension-model.py` + invocations. The Python implementation remains explicit behind the wrapper + until the full generator/validator port lands, and release metadata guards + now reject direct Python extension-model calls in active automation. - 2026-06-28: Removed four confirmed-dead Python helpers: `cargo_package_args` and `supported_publish_targets` from `tools/release/release.py`, `product_string_list` from diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index 509c6256..c312ece3 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -90,7 +90,7 @@ the release/tooling surface after the runtime tool crate split. public open, native-direct engine open, or Android runtime asset selection, and keeps the generated source under extension-model and source-architecture policy checks. Fresh checks passed: - `python3 src/extensions/tools/check-extension-model.py --check`, + `tools/dev/bun.sh src/extensions/tools/check-extension-model.mjs --check`, `ANDROID_HOME=/home/sid/android-sdk ANDROID_SDK_ROOT=/home/sid/android-sdk bash src/sdks/kotlin/tools/check-sdk.sh test-unit`, `ANDROID_HOME=/home/sid/android-sdk ANDROID_SDK_ROOT=/home/sid/android-sdk bash src/sdks/kotlin/tools/check-sdk.sh check-static`, `bash tools/policy/check-sdk-parity.sh`, diff --git a/src/extensions/artifacts/native/moon.yml b/src/extensions/artifacts/native/moon.yml index ef142674..acf2eff0 100644 --- a/src/extensions/artifacts/native/moon.yml +++ b/src/extensions/artifacts/native/moon.yml @@ -23,7 +23,7 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-model.py --check" + command: "tools/dev/bun.sh src/extensions/tools/check-extension-model.mjs --check" deps: - "extension-model:check" inputs: diff --git a/src/extensions/artifacts/wasix/moon.yml b/src/extensions/artifacts/wasix/moon.yml index 6cc5e94b..b6a137cb 100644 --- a/src/extensions/artifacts/wasix/moon.yml +++ b/src/extensions/artifacts/wasix/moon.yml @@ -23,7 +23,7 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-model.py --check" + command: "tools/dev/bun.sh src/extensions/tools/check-extension-model.mjs --check" deps: - "extension-model:check" inputs: diff --git a/src/extensions/model/moon.yml b/src/extensions/model/moon.yml index 0a7c6881..ac57e62a 100644 --- a/src/extensions/model/moon.yml +++ b/src/extensions/model/moon.yml @@ -14,7 +14,7 @@ project: tasks: check: tags: ["quality", "static"] - command: "python3 src/extensions/tools/check-extension-model.py --check" + command: "tools/dev/bun.sh src/extensions/tools/check-extension-model.mjs --check" inputs: - "/src/extensions/**/*" - "/src/shared/extension-runtime-contract/**/*" diff --git a/src/extensions/tools/check-extension-model.mjs b/src/extensions/tools/check-extension-model.mjs new file mode 100755 index 00000000..49943615 --- /dev/null +++ b/src/extensions/tools/check-extension-model.mjs @@ -0,0 +1,21 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const TOOL = "check-extension-model.mjs"; +const ROOT = fileURLToPath(new URL("../../..", import.meta.url)); + +const result = spawnSync("python3", [ + "src/extensions/tools/check-extension-model.py", + ...Bun.argv.slice(2), +], { + cwd: ROOT, + stdio: "inherit", +}); + +if (result.error !== undefined) { + console.error(`${TOOL}: ${result.error.message}`); + process.exit(1); +} + +process.exit(result.status ?? 1); diff --git a/tools/policy/assertions/assert-source-inputs.mjs b/tools/policy/assertions/assert-source-inputs.mjs index 87085f54..8a9bbdcb 100755 --- a/tools/policy/assertions/assert-source-inputs.mjs +++ b/tools/policy/assertions/assert-source-inputs.mjs @@ -143,12 +143,13 @@ function checkExtensions() { 'src/extensions/generated/mobile/static-registry.json', 'src/extensions/generated/mobile/static-extensions.tsv', 'src/extensions/generated/wasix/extensions.json', + 'src/extensions/tools/check-extension-model.mjs', 'src/extensions/tools/check-extension-model.py', ]) { requireFile(path); } - const result = spawnSync('python3', ['src/extensions/tools/check-extension-model.py', '--check'], { + const result = spawnSync('tools/dev/bun.sh', ['src/extensions/tools/check-extension-model.mjs', '--check'], { stdio: 'inherit', }); if (result.error !== undefined) { diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 0549a851..67eff8b7 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -649,9 +649,11 @@ def validate_graph_files() -> None: check_artifact_targets = read_text("tools/release/check_artifact_targets.mjs") check_consumer_shape = read_text("tools/release/check_consumer_shape.py") extension_model = read_text("src/extensions/tools/check-extension-model.py") + extension_model_entrypoint = read_text("src/extensions/tools/check-extension-model.mjs") extension_model_moon = read_text("src/extensions/model/moon.yml") extension_artifacts_native_moon = read_text("src/extensions/artifacts/native/moon.yml") extension_artifacts_wasix_moon = read_text("src/extensions/artifacts/wasix/moon.yml") + source_inputs_assertion = read_text("tools/policy/assertions/assert-source-inputs.mjs") release_policy = read_text("tools/policy/check-release-policy.mjs") check_release_metadata_source = read_text("tools/release/check_release_metadata.py") if re.search(r"(?m)^import product_metadata$", check_release_metadata_source): @@ -676,6 +678,12 @@ def validate_graph_files() -> None: or "import product_metadata" in check_consumer_shape or "import product_metadata" in extension_model or 'release_graph_rows("extension-metadata")' not in extension_model + or 'src/extensions/tools/check-extension-model.py' not in extension_model_entrypoint + or 'tools/dev/bun.sh", "src/extensions/tools/check-extension-model.mjs"' not in sync_release_pr + or "tools/dev/bun.sh', ['src/extensions/tools/check-extension-model.mjs', '--check']" not in source_inputs_assertion + or "python3 src/extensions/tools/check-extension-model.py --check" in extension_model_moon + or "python3 src/extensions/tools/check-extension-model.py --check" in extension_artifacts_native_moon + or "python3 src/extensions/tools/check-extension-model.py --check" in extension_artifacts_wasix_moon or any( required not in moon_source for moon_source in [ diff --git a/tools/release/sync-release-pr.mjs b/tools/release/sync-release-pr.mjs index 89179a90..9cf10322 100644 --- a/tools/release/sync-release-pr.mjs +++ b/tools/release/sync-release-pr.mjs @@ -651,9 +651,9 @@ function syncAssetInputFingerprint(changes, { write }) { } function syncExtensionEvidence(changes, { write }) { - const command = ["src/extensions/tools/check-extension-model.py", write ? "--write-evidence" : "--check"]; + const command = ["tools/dev/bun.sh", "src/extensions/tools/check-extension-model.mjs", write ? "--write-evidence" : "--check"]; const before = Object.fromEntries(EXTENSION_EVIDENCE_PATHS.map((file) => [file, readOptionalText(file)])); - const result = spawnSync("python3", command, { + const result = spawnSync(command[0], command.slice(1), { cwd: ROOT, encoding: "utf8", }); From 129ca3e661aa8c26fec6896ba35a35600a9cb8ee Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 03:35:29 +0000 Subject: [PATCH 254/308] chore: route release validators through bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 9 ++++++++ .../consumer-dx-release-blueprint.md | 9 ++++---- .../examples-ci-release-validation.md | 5 +++-- tools/policy/check-tooling-stack.sh | 14 +++++++++++++ tools/policy/python-entrypoints.allowlist | 4 ++-- tools/release/check-consumer-shape.mjs | 21 +++++++++++++++++++ tools/release/check-release-metadata.mjs | 21 +++++++++++++++++++ tools/release/check_release_metadata.py | 10 +++++++-- tools/release/release-check.mjs | 2 +- tools/release/release-consumer-shape.mjs | 2 +- tools/release/sync-release-pr.mjs | 2 +- 11 files changed, 86 insertions(+), 13 deletions(-) create mode 100755 tools/release/check-consumer-shape.mjs create mode 100755 tools/release/check-release-metadata.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 1d46ec2c..e9d41c06 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,15 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-28: Added Bun command surfaces for the remaining active release + metadata and consumer-shape validator implementations: + `tools/release/check-release-metadata.mjs` and + `tools/release/check-consumer-shape.mjs`. `release-check.mjs` and + `release-consumer-shape.mjs` now call those entrypoints instead of invoking + Python implementation files directly, and tooling/release metadata guards now + reject reintroducing direct active Python calls. The Python implementations + remain inventoried behind those wrappers until the full release-graph validator + ports land. - 2026-06-28: Added the Bun extension-model command surface `src/extensions/tools/check-extension-model.mjs` and moved active Moon checks, source-input assertions, release PR evidence sync, and maintained diff --git a/docs/maintainers/consumer-dx-release-blueprint.md b/docs/maintainers/consumer-dx-release-blueprint.md index 6ab94f0e..60fdbca6 100644 --- a/docs/maintainers/consumer-dx-release-blueprint.md +++ b/docs/maintainers/consumer-dx-release-blueprint.md @@ -538,13 +538,14 @@ Keep these gates: Current enforced blockers: -- `tools/release/check_consumer_shape.py` and - `tools/release/check_release_metadata.py` fail Kotlin/Android while the +- `tools/dev/bun.sh tools/release/release-consumer-shape.mjs` and + `tools/dev/bun.sh tools/release/check-release-metadata.mjs` fail + Kotlin/Android while the Gradle plugin or SDK build logic constructs GitHub release URLs, opens remote streams, exposes `assetBaseUrl`, or keeps a release-asset cache as the normal consumer build path. -- `tools/release/check_consumer_shape.py` and - `tools/release/check_release_metadata.py` fail WASIX while +- `tools/dev/bun.sh tools/release/release-consumer-shape.mjs` and + `tools/dev/bun.sh tools/release/check-release-metadata.mjs` fail WASIX while `oliphaunt-wasix` exposes `OLIPHAUNT_WASM_RUNTIME_ARCHIVE`, `OLIPHAUNT_WASM_AOT_ARCHIVE`, `OLIPHAUNT_WASM_AOT_DIR`, or the inert `bundled` feature. diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index c312ece3..2fb35732 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -101,7 +101,7 @@ the release/tooling surface after the runtime tool crate split. rechecked against current source. Fresh checks passed: `bash tools/policy/check-sdk-parity.sh`, `tools/dev/bun.sh tools/release/check_artifact_targets.mjs`, - `python3 tools/release/check_release_metadata.py`, + `tools/dev/bun.sh tools/release/check-release-metadata.mjs`, `tools/dev/bun.sh tools/policy/assertions/assert-ci-workflows.mjs`, and `tools/dev/bun.sh examples/tools/check-examples.mjs`. The SDK parity gate covers native and WASIX artifact resolution, split native/WASIX tool @@ -285,7 +285,8 @@ the release/tooling surface after the runtime tool crate split. 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.mjs`. Native tools are + `tools/dev/bun.sh tools/release/release-consumer-shape.mjs`, and + `check_artifact_targets.mjs`. 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 diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 814d2c7f..3e3a3c70 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -366,6 +366,20 @@ grep -Fq 'tools/dev/bun.sh tools/release/prepare-rust-release-source.mjs' src/sd if grep -Fq '"prepare-rust-release-source"' tools/release/release.py; then fail "release.py must not retain the Rust SDK prepare-rust-release-source command surface after it moved to Bun" fi +grep -Fq 'tools/release/check-release-metadata.mjs' tools/release/release-check.mjs || + fail "release-check must route release metadata validation through the Bun entrypoint" +grep -Fq 'tools/release/check_release_metadata.py' tools/release/check-release-metadata.mjs || + fail "release metadata Bun entrypoint must explicitly own the remaining Python implementation bridge" +if grep -Fq '["python3", "tools/release/check_release_metadata.py"]' tools/release/release-check.mjs; then + fail "release-check must not call the release metadata Python implementation directly" +fi +grep -Fq 'tools/release/check-consumer-shape.mjs' tools/release/release-consumer-shape.mjs || + fail "release-consumer-shape must route consumer-shape validation through the Bun entrypoint" +grep -Fq 'tools/release/check_consumer_shape.py' tools/release/check-consumer-shape.mjs || + fail "consumer-shape Bun entrypoint must explicitly own the remaining Python implementation bridge" +if grep -Fq '["tools/release/check_consumer_shape.py"' tools/release/release-consumer-shape.mjs; then + fail "release-consumer-shape must not call the consumer-shape Python implementation directly" +fi grep -Fq 'tools/dev/bun.sh tools/release/local-registry-publish.mjs download' examples/README.md || fail "example local-registry setup docs must use the Bun local-registry command" grep -Fq 'tools/dev/bun.sh tools/release/local-registry-publish.mjs publish' examples/README.md || diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index a8b280e2..10620b7a 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -2,6 +2,6 @@ # Format: pathdomainmigration-decisionrationale # New Python files should be ported to Bun or deliberately added here with a specific migration decision. src/extensions/tools/check-extension-model.py extensions defer-extension-model-port generates and validates multi-language extension catalog, SDK metadata, docs, and evidence from one model -tools/release/check_consumer_shape.py release-consumer-shape defer-release-graph-port validates cross-SDK package/runtime/install shape from generated release fixtures and source invariants -tools/release/check_release_metadata.py release-metadata defer-release-graph-port validates release metadata and publish-step wiring through cached Bun release graph query rows +tools/release/check_consumer_shape.py release-consumer-shape defer-release-graph-port implementation behind the Bun consumer-shape entrypoint validating cross-SDK package/runtime/install shape +tools/release/check_release_metadata.py release-metadata defer-release-graph-port implementation behind the Bun release-metadata entrypoint validating release metadata and publish-step wiring tools/release/release.py release-orchestrator defer-release-graph-port owns protected release planning, validation, registry checks, publish dry-runs, and publish dispatch diff --git a/tools/release/check-consumer-shape.mjs b/tools/release/check-consumer-shape.mjs new file mode 100755 index 00000000..b71394d2 --- /dev/null +++ b/tools/release/check-consumer-shape.mjs @@ -0,0 +1,21 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; + +import { ROOT } from "./release-cli-utils.mjs"; + +const TOOL = "check-consumer-shape.mjs"; + +const result = spawnSync("python3", [ + "tools/release/check_consumer_shape.py", + ...Bun.argv.slice(2), +], { + cwd: ROOT, + stdio: "inherit", +}); + +if (result.error !== undefined) { + console.error(`${TOOL}: ${result.error.message}`); + process.exit(1); +} + +process.exit(result.status ?? 1); diff --git a/tools/release/check-release-metadata.mjs b/tools/release/check-release-metadata.mjs new file mode 100755 index 00000000..f2a5355c --- /dev/null +++ b/tools/release/check-release-metadata.mjs @@ -0,0 +1,21 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; + +import { ROOT } from "./release-cli-utils.mjs"; + +const TOOL = "check-release-metadata.mjs"; + +const result = spawnSync("python3", [ + "tools/release/check_release_metadata.py", + ...Bun.argv.slice(2), +], { + cwd: ROOT, + stdio: "inherit", +}); + +if (result.error !== undefined) { + console.error(`${TOOL}: ${result.error.message}`); + process.exit(1); +} + +process.exit(result.status ?? 1); diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 67eff8b7..993fa62c 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -639,6 +639,8 @@ def validate_graph_files() -> None: release_check_registries = read_text("tools/release/release-check-registries.mjs") release_consumer_shape = read_text("tools/release/release-consumer-shape.mjs") release_verify = read_text("tools/release/release-verify.mjs") + release_metadata_entrypoint = read_text("tools/release/check-release-metadata.mjs") + consumer_shape_entrypoint = read_text("tools/release/check-consumer-shape.mjs") prepare_rust_release_source = read_text("tools/release/prepare-rust-release-source.mjs") local_registry_publish = read_text("tools/release/local-registry-publish.mjs") cargo_source_package = read_text("tools/release/cargo-source-package.mjs") @@ -727,11 +729,15 @@ def validate_graph_files() -> None: or '"tools/release/release-consumer-shape.mjs", *args' not in release_source or '"tools/release/release-verify.mjs", *args' not in release_source or "tools/release/check_release_pr_coverage.mjs" not in release_check - or "tools/release/check_release_metadata.py" not in release_check + or "tools/release/check-release-metadata.mjs" not in release_check + or '["python3", "tools/release/check_release_metadata.py"]' in release_check + or "tools/release/check_release_metadata.py" not in release_metadata_entrypoint or "tools/release/release-consumer-shape.mjs" not in release_check or "tools/release/check_release_versions.mjs" not in release_check_registries or "tools/release/check_registry_publication.mjs" not in release_check_registries - or "tools/release/check_consumer_shape.py" not in release_consumer_shape + or "tools/release/check-consumer-shape.mjs" not in release_consumer_shape + or '["tools/release/check_consumer_shape.py"' in release_consumer_shape + or "tools/release/check_consumer_shape.py" not in consumer_shape_entrypoint or "tools/release/check_release_versions.mjs" not in release_verify or "tools/release/release-consumer-shape.mjs" not in release_verify or "tools/release/verify_github_release_attestations.mjs" not in release_verify diff --git a/tools/release/release-check.mjs b/tools/release/release-check.mjs index 5d3235a2..4cd75347 100644 --- a/tools/release/release-check.mjs +++ b/tools/release/release-check.mjs @@ -25,7 +25,7 @@ function main(argv) { run(TOOL, ["tools/dev/bun.sh", "tools/release/check_artifact_targets.mjs"]); run(TOOL, ["tools/dev/bun.sh", "tools/release/sync-release-pr.mjs", "--check"]); run(TOOL, ["tools/dev/bun.sh", "tools/release/check_release_pr_coverage.mjs"]); - run(TOOL, ["python3", "tools/release/check_release_metadata.py"]); + run(TOOL, ["tools/dev/bun.sh", "tools/release/check-release-metadata.mjs"]); run(TOOL, ["tools/dev/bun.sh", "tools/release/release-consumer-shape.mjs", "--format", "json", "--require-ready"]); run(TOOL, [ "tools/dev/bun.sh", diff --git a/tools/release/release-consumer-shape.mjs b/tools/release/release-consumer-shape.mjs index 959bc0e5..50c1a937 100644 --- a/tools/release/release-consumer-shape.mjs +++ b/tools/release/release-consumer-shape.mjs @@ -3,4 +3,4 @@ import { run } from "./release-cli-utils.mjs"; const TOOL = "release-consumer-shape.mjs"; -run(TOOL, ["tools/release/check_consumer_shape.py", ...Bun.argv.slice(2)]); +run(TOOL, ["tools/dev/bun.sh", "tools/release/check-consumer-shape.mjs", ...Bun.argv.slice(2)]); diff --git a/tools/release/sync-release-pr.mjs b/tools/release/sync-release-pr.mjs index 9cf10322..adcd9678 100644 --- a/tools/release/sync-release-pr.mjs +++ b/tools/release/sync-release-pr.mjs @@ -669,7 +669,7 @@ function syncExtensionEvidence(changes, { write }) { } return; } - fail(`\`python3 ${command.join(" ")}\` failed:\n${output}`); + fail(`\`${command.join(" ")}\` failed:\n${output}`); } if (!write) { return; From 87c2895ca2e5885caad779288100d26a3bad4847 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 03:44:31 +0000 Subject: [PATCH 255/308] chore: route release publish through bun --- .github/workflows/release.yml | 40 ++++++++-------- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 7 +++ docs/maintainers/development.md | 4 +- docs/maintainers/release-setup.md | 2 +- docs/maintainers/release.md | 4 +- docs/maintainers/tooling.md | 5 +- tools/policy/check-release-policy.mjs | 2 +- tools/policy/check-repo-structure.sh | 1 + tools/policy/check-tooling-stack.sh | 12 +++++ tools/policy/python-entrypoints.allowlist | 2 +- tools/release/check_release_metadata.py | 10 ++++ tools/release/release-publish.mjs | 46 +++++++++++++++++++ 12 files changed, 106 insertions(+), 29 deletions(-) create mode 100755 tools/release/release-publish.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 72761a14..1a9fbe14 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -471,7 +471,7 @@ jobs: OLIPHAUNT_BROKER_RELEASE_ASSET_INPUT_DIRS: ${{ github.workspace }}/target/oliphaunt-broker/release-assets OLIPHAUNT_NODE_ADDON_ASSET_INPUT_DIRS: ${{ github.workspace }}/target/oliphaunt-node-direct/release-assets PRODUCTS_JSON: ${{ steps.release_plan.outputs.products_json }} - run: tools/release/release.py publish-dry-run --products-json "${PRODUCTS_JSON}" --head-ref "$RELEASE_HEAD_SHA" + run: tools/dev/bun.sh tools/release/release-publish.mjs publish-dry-run --products-json "${PRODUCTS_JSON}" --head-ref "$RELEASE_HEAD_SHA" - name: Create release-please target branch if: ${{ inputs.operation == 'publish' && steps.release_plan.outputs.has_release_changes == 'true' && steps.release_head.outputs.uses_temporary_target_branch == 'true' }} @@ -505,14 +505,14 @@ jobs: if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_liboliphaunt_native == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: tools/release/release.py publish --product liboliphaunt-native --step github-release-assets --head-ref "$RELEASE_HEAD_SHA" + run: tools/dev/bun.sh tools/release/release-publish.mjs publish --product liboliphaunt-native --step github-release-assets --head-ref "$RELEASE_HEAD_SHA" - name: Publish selected extension GitHub release assets if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.has_extension_products == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PRODUCTS_JSON: ${{ steps.release_plan.outputs.extension_products_json }} - run: tools/release/release.py publish --step github-release-assets --products-json "${PRODUCTS_JSON}" --head-ref "$RELEASE_HEAD_SHA" + run: tools/dev/bun.sh tools/release/release-publish.mjs publish --step github-release-assets --products-json "${PRODUCTS_JSON}" --head-ref "$RELEASE_HEAD_SHA" - name: Publish selected extension Android artifacts to Maven Central if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.has_extension_products == 'true' }} @@ -524,7 +524,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/release.py publish --step maven-central --products-json "${PRODUCTS_JSON}" --head-ref "$RELEASE_HEAD_SHA" + run: tools/dev/bun.sh tools/release/release-publish.mjs publish --step maven-central --products-json "${PRODUCTS_JSON}" --head-ref "$RELEASE_HEAD_SHA" - name: Attest selected extension release assets if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.has_extension_products == 'true' }} @@ -553,13 +553,13 @@ jobs: if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_liboliphaunt_native == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: tools/release/release.py publish --product liboliphaunt-native --step crates-io --head-ref "$RELEASE_HEAD_SHA" + run: tools/dev/bun.sh tools/release/release-publish.mjs publish --product liboliphaunt-native --step crates-io --head-ref "$RELEASE_HEAD_SHA" - name: Publish liboliphaunt artifact packages to npm if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_liboliphaunt_native == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: tools/release/release.py publish --product liboliphaunt-native --step npm --head-ref "$RELEASE_HEAD_SHA" + run: tools/dev/bun.sh tools/release/release-publish.mjs publish --product liboliphaunt-native --step npm --head-ref "$RELEASE_HEAD_SHA" - name: Publish liboliphaunt Android runtime artifacts to Maven Central if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_liboliphaunt_native == 'true' }} @@ -570,13 +570,13 @@ 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/release.py publish --product liboliphaunt-native --step maven-central --head-ref "$RELEASE_HEAD_SHA" + run: tools/dev/bun.sh tools/release/release-publish.mjs publish --product liboliphaunt-native --step maven-central --head-ref "$RELEASE_HEAD_SHA" - name: Publish Swift SDK GitHub release and SwiftPM tags if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_swift == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: tools/release/release.py publish --product oliphaunt-swift --step github-release --head-ref "$RELEASE_HEAD_SHA" + run: tools/dev/bun.sh tools/release/release-publish.mjs publish --product oliphaunt-swift --step github-release --head-ref "$RELEASE_HEAD_SHA" - name: Publish Kotlin SDK to Maven Central if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_kotlin == 'true' }} @@ -587,20 +587,20 @@ 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/release.py publish --product oliphaunt-kotlin --step maven-central --head-ref "$RELEASE_HEAD_SHA" + run: tools/dev/bun.sh tools/release/release-publish.mjs publish --product oliphaunt-kotlin --step maven-central --head-ref "$RELEASE_HEAD_SHA" - name: Publish React Native package to npm if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_react_native == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: tools/release/release.py publish --product oliphaunt-react-native --step npm --head-ref "$RELEASE_HEAD_SHA" + run: tools/dev/bun.sh tools/release/release-publish.mjs publish --product oliphaunt-react-native --step npm --head-ref "$RELEASE_HEAD_SHA" - name: Publish broker GitHub release assets if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_broker == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} OLIPHAUNT_BROKER_RELEASE_ASSET_INPUT_DIRS: ${{ github.workspace }}/target/oliphaunt-broker/release-assets - run: tools/release/release.py publish --product oliphaunt-broker --step github-release-assets --head-ref "$RELEASE_HEAD_SHA" + run: tools/dev/bun.sh tools/release/release-publish.mjs publish --product oliphaunt-broker --step github-release-assets --head-ref "$RELEASE_HEAD_SHA" - name: Attest broker release assets if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_broker == 'true' }} @@ -615,26 +615,26 @@ jobs: if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_broker == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: tools/release/release.py publish --product oliphaunt-broker --step crates-io --head-ref "$RELEASE_HEAD_SHA" + run: tools/dev/bun.sh tools/release/release-publish.mjs publish --product oliphaunt-broker --step crates-io --head-ref "$RELEASE_HEAD_SHA" - name: Publish broker artifact packages to npm if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_broker == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: tools/release/release.py publish --product oliphaunt-broker --step npm --head-ref "$RELEASE_HEAD_SHA" + run: tools/dev/bun.sh tools/release/release-publish.mjs publish --product oliphaunt-broker --step npm --head-ref "$RELEASE_HEAD_SHA" - name: Publish Rust SDK to crates.io if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_rust == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: tools/release/release.py publish --product oliphaunt-rust --step crates-io --head-ref "$RELEASE_HEAD_SHA" + run: tools/dev/bun.sh tools/release/release-publish.mjs publish --product oliphaunt-rust --step crates-io --head-ref "$RELEASE_HEAD_SHA" - name: Publish Node direct GitHub release assets if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_node_direct == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} OLIPHAUNT_NODE_ADDON_ASSET_INPUT_DIRS: ${{ github.workspace }}/target/oliphaunt-node-direct/release-assets - run: tools/release/release.py publish --product oliphaunt-node-direct --step github-release-assets --head-ref "$RELEASE_HEAD_SHA" + run: tools/dev/bun.sh tools/release/release-publish.mjs publish --product oliphaunt-node-direct --step github-release-assets --head-ref "$RELEASE_HEAD_SHA" - name: Attest Node direct release assets if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_node_direct == 'true' }} @@ -649,19 +649,19 @@ jobs: if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_node_direct == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: tools/release/release.py publish --product oliphaunt-node-direct --step npm --head-ref "$RELEASE_HEAD_SHA" + run: tools/dev/bun.sh tools/release/release-publish.mjs publish --product oliphaunt-node-direct --step npm --head-ref "$RELEASE_HEAD_SHA" - name: Publish TypeScript packages to npm and JSR if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_js == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: tools/release/release.py publish --product oliphaunt-js --step npm-jsr --head-ref "$RELEASE_HEAD_SHA" + run: tools/dev/bun.sh tools/release/release-publish.mjs publish --product oliphaunt-js --step npm-jsr --head-ref "$RELEASE_HEAD_SHA" - name: Upload WASIX GitHub release assets if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_liboliphaunt_wasix == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: tools/release/release.py publish --product liboliphaunt-wasix --step github-release-assets --head-ref "$RELEASE_HEAD_SHA" + run: tools/dev/bun.sh tools/release/release-publish.mjs publish --product liboliphaunt-wasix --step github-release-assets --head-ref "$RELEASE_HEAD_SHA" - name: Attest WASIX release assets if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_liboliphaunt_wasix == 'true' }} @@ -675,13 +675,13 @@ jobs: if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_liboliphaunt_wasix == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: tools/release/release.py publish --product liboliphaunt-wasix --step crates-io --head-ref "$RELEASE_HEAD_SHA" + run: tools/dev/bun.sh tools/release/release-publish.mjs publish --product liboliphaunt-wasix --step crates-io --head-ref "$RELEASE_HEAD_SHA" - name: Publish WASIX Rust binding to crates.io if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' && steps.release_plan.outputs.product_oliphaunt_wasix_rust == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: tools/release/release.py publish --product oliphaunt-wasix-rust --step crates-io --head-ref "$RELEASE_HEAD_SHA" + run: tools/dev/bun.sh tools/release/release-publish.mjs publish --product oliphaunt-wasix-rust --step crates-io --head-ref "$RELEASE_HEAD_SHA" - name: Verify published release if: ${{ steps.release_plan.outputs.has_release_changes == 'true' && inputs.operation == 'publish' }} diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index e9d41c06..042d76e3 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,13 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-28: Added the Bun publish command surface + `tools/release/release-publish.mjs` for active release workflow + `publish-dry-run` and `publish` calls. The workflow now invokes publish + operations through `tools/dev/bun.sh tools/release/release-publish.mjs`, while + the existing protected `release.py` implementation remains behind that + entrypoint until publish dispatch is ported. Release metadata and tooling + guards now reject direct workflow `release.py publish*` calls. - 2026-06-28: Added Bun command surfaces for the remaining active release metadata and consumer-shape validator implementations: `tools/release/check-release-metadata.mjs` and diff --git a/docs/maintainers/development.md b/docs/maintainers/development.md index 850d5618..3647d5e3 100644 --- a/docs/maintainers/development.md +++ b/docs/maintainers/development.md @@ -160,7 +160,7 @@ The validation entrypoint is split by maintainer workflow: - `moon run :check && moon run :test && moon run :smoke`: fast contributor lane for repo, lint, source tests, and examples; - `moon run :regression`: broader SQL, protocol, extension, and runtime regression suites; -- `tools/release/release.py publish-dry-run --wasm`: release-workspace package checks plus publish +- `tools/dev/bun.sh tools/release/release-publish.mjs publish-dry-run --wasm`: release-workspace package checks plus publish dry-runs for internal crates after CI-generated AOT artifacts have been downloaded. @@ -345,7 +345,7 @@ workflow SHA: ```sh cargo run -p xtask -- assets download --sha --all-targets -tools/release/release.py publish-dry-run --wasm +tools/dev/bun.sh tools/release/release-publish.mjs publish-dry-run --wasm ``` Developers should not be expected to build every target locally. Local runtime diff --git a/docs/maintainers/release-setup.md b/docs/maintainers/release-setup.md index 268b71c2..77f32d4f 100644 --- a/docs/maintainers/release-setup.md +++ b/docs/maintainers/release-setup.md @@ -389,7 +389,7 @@ moon run dev-tools:doctor tools/dev/bun.sh tools/release/release-check.mjs tools/dev/bun.sh tools/release/release_plan.mjs --from-product-tags --include-current-tags --head-ref HEAD tools/dev/bun.sh tools/release/release-check-registries.mjs --products-json '' --head-ref HEAD -tools/release/release.py publish-dry-run --products-json '' --head-ref HEAD +tools/dev/bun.sh tools/release/release-publish.mjs publish-dry-run --products-json '' --head-ref HEAD tools/dev/bun.sh tools/release/release-consumer-shape.mjs --require-ready --format markdown ``` diff --git a/docs/maintainers/release.md b/docs/maintainers/release.md index dbb54a73..b4b5d022 100644 --- a/docs/maintainers/release.md +++ b/docs/maintainers/release.md @@ -60,8 +60,8 @@ Use these commands while preparing or checking releases: tools/dev/bun.sh tools/release/release_plan.mjs tools/dev/bun.sh tools/release/release-check.mjs tools/dev/bun.sh tools/release/release-check-registries.mjs -tools/release/release.py publish-dry-run -tools/release/release.py publish +tools/dev/bun.sh tools/release/release-publish.mjs publish-dry-run +tools/dev/bun.sh tools/release/release-publish.mjs publish tools/dev/bun.sh tools/release/release-verify.mjs tools/dev/bun.sh tools/release/release-consumer-shape.mjs ``` diff --git a/docs/maintainers/tooling.md b/docs/maintainers/tooling.md index 06998127..65f8e5d6 100644 --- a/docs/maintainers/tooling.md +++ b/docs/maintainers/tooling.md @@ -15,8 +15,9 @@ predictable without hiding ecosystem-native behavior. - Product-local `targets/*.toml` files own platform artifact metadata. - Product-native build tools own product behavior: Cargo, SwiftPM/Xcode, Gradle, npm/JSR, Expo, React Native Codegen, and PostgreSQL build scripts. -- `tools/release/release.py` owns protected publish operations, registry - checks, checksums, attestations, and GitHub release asset verification. +- `tools/release/release.py` currently owns the protected implementation behind + Bun release check, verify, and publish entrypoints: registry checks, + checksums, attestations, and GitHub release asset verification. Do not add a second source graph, release graph, or root alias layer over Moon. Do not add a repo-wide tool because it is popular in one language ecosystem. diff --git a/tools/policy/check-release-policy.mjs b/tools/policy/check-release-policy.mjs index 8c548a66..c6cc393c 100644 --- a/tools/policy/check-release-policy.mjs +++ b/tools/policy/check-release-policy.mjs @@ -1147,7 +1147,7 @@ function checkReleaseWorkflowPolicy() { "pnpm install --frozen-lockfile", "target/oliphaunt-broker/release-assets", "target/oliphaunt-node-direct/release-assets", - "tools/release/release.py publish-dry-run --products-json", + "tools/dev/bun.sh tools/release/release-publish.mjs publish-dry-run --products-json", '--head-ref "$RELEASE_HEAD_SHA"', ]) { if (!publishBlock.includes(snippet)) { diff --git a/tools/policy/check-repo-structure.sh b/tools/policy/check-repo-structure.sh index a8f78851..f7b04c3c 100755 --- a/tools/policy/check-repo-structure.sh +++ b/tools/policy/check-repo-structure.sh @@ -207,6 +207,7 @@ require_file pnpm-workspace.yaml require_file release-please-config.json require_file .release-please-manifest.json require_file tools/release/release.py +require_file tools/release/release-publish.mjs require_file tools/dev/bun.sh require_file tools/dev/doctor.sh require_file tools/policy/check-policy-tools.sh diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 3e3a3c70..6aeb868c 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -380,6 +380,18 @@ grep -Fq 'tools/release/check_consumer_shape.py' tools/release/check-consumer-sh if grep -Fq '["tools/release/check_consumer_shape.py"' tools/release/release-consumer-shape.mjs; then fail "release-consumer-shape must not call the consumer-shape Python implementation directly" fi +grep -Fq 'const COMMANDS = new Set(["publish", "publish-dry-run"]);' tools/release/release-publish.mjs || + fail "release publish and dry-run commands must share the Bun release-publish entrypoint" +grep -Fq 'tools/dev/bun.sh tools/release/release-publish.mjs publish-dry-run' .github/workflows/release.yml || + fail "release workflow publish dry-runs must use the Bun release-publish entrypoint" +grep -Fq 'tools/dev/bun.sh tools/release/release-publish.mjs publish ' .github/workflows/release.yml || + fail "release workflow publish steps must use the Bun release-publish entrypoint" +if grep -Fq 'tools/release/release.py publish-dry-run' .github/workflows/release.yml; then + fail "release workflow must not call release.py publish-dry-run directly" +fi +if grep -Fq 'tools/release/release.py publish --' .github/workflows/release.yml; then + fail "release workflow must not call release.py publish directly" +fi grep -Fq 'tools/dev/bun.sh tools/release/local-registry-publish.mjs download' examples/README.md || fail "example local-registry setup docs must use the Bun local-registry command" grep -Fq 'tools/dev/bun.sh tools/release/local-registry-publish.mjs publish' examples/README.md || diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index 10620b7a..655f5bd1 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -4,4 +4,4 @@ src/extensions/tools/check-extension-model.py extensions defer-extension-model-port generates and validates multi-language extension catalog, SDK metadata, docs, and evidence from one model tools/release/check_consumer_shape.py release-consumer-shape defer-release-graph-port implementation behind the Bun consumer-shape entrypoint validating cross-SDK package/runtime/install shape tools/release/check_release_metadata.py release-metadata defer-release-graph-port implementation behind the Bun release-metadata entrypoint validating release metadata and publish-step wiring -tools/release/release.py release-orchestrator defer-release-graph-port owns protected release planning, validation, registry checks, publish dry-runs, and publish dispatch +tools/release/release.py release-orchestrator defer-release-graph-port protected release implementation behind Bun check/verify/publish entrypoints during release-graph port diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 993fa62c..44aa505f 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -972,10 +972,20 @@ def validate_exact_extension_registry_shape() -> None: def validate_publish_target_coverage() -> None: workflow = read_text(".github/workflows/release.yml") release_source = read_text("tools/release/release.py") + release_publish = read_text("tools/release/release-publish.mjs") 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 ( + "tools/dev/bun.sh tools/release/release-publish.mjs publish-dry-run" not in workflow + or "tools/dev/bun.sh tools/release/release-publish.mjs publish " not in workflow + or "tools/release/release.py publish-dry-run" in workflow + or "tools/release/release.py publish --" in workflow + or 'const COMMANDS = new Set(["publish", "publish-dry-run"]);' not in release_publish + or 'spawnSync("tools/release/release.py", argv' not in release_publish + ): + fail("Release workflow publish and publish-dry-run commands must use the Bun release-publish entrypoint while release.py keeps the protected implementation") 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 diff --git a/tools/release/release-publish.mjs b/tools/release/release-publish.mjs new file mode 100755 index 00000000..1a757cce --- /dev/null +++ b/tools/release/release-publish.mjs @@ -0,0 +1,46 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; + +import { ROOT } from "./release-cli-utils.mjs"; + +const TOOL = "release-publish.mjs"; +const COMMANDS = new Set(["publish", "publish-dry-run"]); + +function usage() { + console.log(`usage: tools/release/release-publish.mjs [release.py passthrough args] + +Runs protected release publish and publish dry-run operations through the Bun +release command surface. The current implementation delegates to release.py +while publish dispatch is ported. +`); +} + +function fail(message, exitCode = 2) { + console.error(`${TOOL}: ${message}`); + process.exit(exitCode); +} + +const argv = Bun.argv.slice(2); +const command = argv[0]; + +if (command === "-h" || command === "--help") { + usage(); + process.exit(0); +} + +if (!COMMANDS.has(command)) { + usage(); + fail(`expected publish or publish-dry-run, got ${command ?? ""}`); +} + +const result = spawnSync("tools/release/release.py", argv, { + cwd: ROOT, + stdio: "inherit", +}); + +if (result.error !== undefined) { + console.error(`${TOOL}: ${result.error.message}`); + process.exit(1); +} + +process.exit(result.status ?? 1); From 418ae42c3b27374850793d9b3af5570d0a771c33 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 03:58:43 +0000 Subject: [PATCH 256/308] chore: route root release check through bun --- docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 6 ++++++ docs/maintainers/examples-ci-release-validation.md | 4 ++-- docs/maintainers/release-setup.md | 4 ++-- docs/maintainers/release.md | 2 +- moon.yml | 2 +- tools/policy/check-tooling-stack.sh | 5 +++++ tools/release/check_release_metadata.py | 5 ++++- 7 files changed, 21 insertions(+), 7 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 042d76e3..450b1492 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -2748,3 +2748,9 @@ until the current-state gates here are checked with fresh local evidence. SDK package rows, product config paths, Moon release metadata, and current versions; the release metadata checker now rejects reintroducing the adapter in the artifact-target checker. +- On 2026-06-28, the root Moon `release-check` task was retargeted from + `tools/release/release.py check` to + `tools/dev/bun.sh tools/release/release-check.mjs`, matching the release + workflow and `tools/release/moon.yml`. `check_release_metadata.py` and + `check-tooling-stack.sh` now reject reintroducing the Python compatibility + entrypoint on the active root Moon surface. diff --git a/docs/maintainers/examples-ci-release-validation.md b/docs/maintainers/examples-ci-release-validation.md index 2fb35732..8ba428c2 100644 --- a/docs/maintainers/examples-ci-release-validation.md +++ b/docs/maintainers/examples-ci-release-validation.md @@ -192,14 +192,14 @@ the release/tooling surface after the runtime tool crate split. compile, release graph output, targeted product metadata reads, release metadata, artifact targets, focused consumer-shape checks, release policy, tooling-stack policy, - `tools/release/release.py check`, strict local Cargo publication, strict + `tools/dev/bun.sh tools/release/release-check.mjs`, strict local Cargo publication, strict local npm publication, docs policy, and `git diff --check`. - On 2026-06-27, the stale direct `tools/release/product_metadata.py version` CLI was retired before the compatibility module was deleted. Product version reads remain on the Bun helper `tools/release/product-version.mjs`. Fresh validation passed for the Bun version helper, the expected failing Python guidance path, Python compile, tooling inventory, policy tooling, docs, - `tools/release/release.py check`, and strict local Cargo/npm publication. A + `tools/dev/bun.sh tools/release/release-check.mjs`, and strict local Cargo/npm publication. A sweep of 836 generated `.crate` files found no crate above the 10 MiB crates.io limit; the largest observed crate was 10,212,312 bytes. - On 2026-06-27, strict local Cargo and npm publication were rerun against the diff --git a/docs/maintainers/release-setup.md b/docs/maintainers/release-setup.md index 77f32d4f..827852a4 100644 --- a/docs/maintainers/release-setup.md +++ b/docs/maintainers/release-setup.md @@ -69,9 +69,9 @@ checks out that PR branch, runs compatibility files and lockfile updates back to the same PR when needed. If no release PR exists, the sync step exits cleanly. Run `tools/dev/bun.sh tools/release/sync-release-pr.mjs --check` locally after -manual version experiments; it is also part of `tools/release/release.py check`. -The active CI path calls the Bun orchestrator directly: +manual version experiments; it is also part of `tools/dev/bun.sh tools/release/release-check.mjs`. +Active CI and Moon paths call that Bun orchestrator directly. The publish job still needs the repository-scoped `GITHUB_TOKEN` for GitHub release asset uploads, artifact attestations, release-please release creation, diff --git a/docs/maintainers/release.md b/docs/maintainers/release.md index b4b5d022..cf6d33bf 100644 --- a/docs/maintainers/release.md +++ b/docs/maintainers/release.md @@ -201,7 +201,7 @@ asset, and exact-extension release asset must be covered by: - GitHub artifact attestations; - product-local target metadata; - package-size evidence where applicable; -- `tools/release/release.py verify-release`. +- `tools/dev/bun.sh tools/release/release-verify.mjs`. Package-native publication remains package-native: Cargo publishes Rust crates, npm publishes JavaScript/React Native packages, Gradle/Vanniktech publishes diff --git a/moon.yml b/moon.yml index 6d24b239..e34223f9 100644 --- a/moon.yml +++ b/moon.yml @@ -343,7 +343,7 @@ tasks: runFromWorkspaceRoot: true release-check: tags: ["release", "package"] - command: "tools/release/release.py check" + command: "tools/dev/bun.sh tools/release/release-check.mjs" inputs: - "/.github/**/*" - "/benchmarks/**/*" diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 6aeb868c..6b404f17 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -368,6 +368,11 @@ if grep -Fq '"prepare-rust-release-source"' tools/release/release.py; then fi grep -Fq 'tools/release/check-release-metadata.mjs' tools/release/release-check.mjs || fail "release-check must route release metadata validation through the Bun entrypoint" +grep -Fq 'command: "tools/dev/bun.sh tools/release/release-check.mjs"' moon.yml || + fail "root Moon release-check task must call the Bun release-check orchestrator directly" +if grep -Fq 'command: "tools/release/release.py check"' moon.yml; then + fail "root Moon release-check task must not call the Python compatibility entrypoint" +fi grep -Fq 'tools/release/check_release_metadata.py' tools/release/check-release-metadata.mjs || fail "release metadata Bun entrypoint must explicitly own the remaining Python implementation bridge" if grep -Fq '["python3", "tools/release/check_release_metadata.py"]' tools/release/release-check.mjs; then diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 44aa505f..58f77925 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -720,6 +720,7 @@ def validate_graph_files() -> None: release_source = read_text("tools/release/release.py") release_workflow = read_text(".github/workflows/release.yml") release_moon = read_text("tools/release/moon.yml") + root_moon = read_text("moon.yml") rust_sdk_check = read_text("src/sdks/rust/tools/check-sdk.sh") examples_readme = read_text("examples/README.md") examples_local_registries = read_text("examples/tools/with-local-registries.sh") @@ -747,8 +748,10 @@ def validate_graph_files() -> None: or "tools/dev/bun.sh tools/release/release-verify.mjs" not in release_workflow or "tools/dev/bun.sh tools/release/release-check.mjs" not in release_moon or "tools/dev/bun.sh tools/release/release-consumer-shape.mjs" not in release_moon + or 'command: "tools/dev/bun.sh tools/release/release-check.mjs"' not in root_moon + or 'command: "tools/release/release.py check"' in root_moon ): - fail("active release check, registry-check, verify, and consumer-shape orchestration must live in Bun helpers while release.py keeps compatibility delegators") + fail("active release check, registry-check, verify, and consumer-shape orchestration must live in Bun helpers; release.py is only the protected publish implementation and compatibility bridge") if ( "tools/dev/bun.sh tools/release/prepare-rust-release-source.mjs" not in rust_sdk_check or '"prepare-rust-release-source"' in release_source From 260203f22037db190a2d969d19e5b4d684bf1ad2 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 04:05:42 +0000 Subject: [PATCH 257/308] chore: route moon release metadata through bun --- docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 5 +++++ moon.yml | 3 ++- tools/policy/check-tooling-stack.sh | 5 +++++ tools/release/check_release_metadata.py | 2 ++ 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 450b1492..3392a33a 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -94,6 +94,11 @@ until the current-state gates here are checked with fresh local evidence. reject reintroducing direct active Python calls. The Python implementations remain inventoried behind those wrappers until the full release-graph validator ports land. +- 2026-06-28: Routed the root Moon `release-metadata` task through + `tools/dev/bun.sh tools/release/check-release-metadata.mjs` instead of the + Python implementation file. The task now tracks `tools/dev/bun.sh`, and + tooling/release metadata guards reject reintroducing the direct + `tools/release/check_release_metadata.py` Moon command. - 2026-06-28: Added the Bun extension-model command surface `src/extensions/tools/check-extension-model.mjs` and moved active Moon checks, source-input assertions, release PR evidence sync, and maintained diff --git a/moon.yml b/moon.yml index e34223f9..7727ebc2 100644 --- a/moon.yml +++ b/moon.yml @@ -144,7 +144,7 @@ tasks: runFromWorkspaceRoot: true release-metadata: tags: ["policy", "assertion", "quality", "static"] - command: "tools/release/check_release_metadata.py" + command: "tools/dev/bun.sh tools/release/check-release-metadata.mjs" inputs: - "/README.md" - "/docs/**/*" @@ -162,6 +162,7 @@ tasks: - "!/src/**/out/**" - "!/src/**/Pods/**" - "!/src/**/DerivedData/**" + - "/tools/dev/bun.sh" - "/tools/release/**/*" options: cache: true diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 6b404f17..b75d7595 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -373,6 +373,11 @@ grep -Fq 'command: "tools/dev/bun.sh tools/release/release-check.mjs"' moon.yml if grep -Fq 'command: "tools/release/release.py check"' moon.yml; then fail "root Moon release-check task must not call the Python compatibility entrypoint" fi +grep -Fq 'command: "tools/dev/bun.sh tools/release/check-release-metadata.mjs"' moon.yml || + fail "root Moon release-metadata task must call the Bun release metadata entrypoint directly" +if grep -Fq 'command: "tools/release/check_release_metadata.py"' moon.yml; then + fail "root Moon release-metadata task must not call the Python implementation directly" +fi grep -Fq 'tools/release/check_release_metadata.py' tools/release/check-release-metadata.mjs || fail "release metadata Bun entrypoint must explicitly own the remaining Python implementation bridge" if grep -Fq '["python3", "tools/release/check_release_metadata.py"]' tools/release/release-check.mjs; then diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 58f77925..a7fd0a57 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -749,7 +749,9 @@ def validate_graph_files() -> None: or "tools/dev/bun.sh tools/release/release-check.mjs" not in release_moon or "tools/dev/bun.sh tools/release/release-consumer-shape.mjs" not in release_moon or 'command: "tools/dev/bun.sh tools/release/release-check.mjs"' not in root_moon + or 'command: "tools/dev/bun.sh tools/release/check-release-metadata.mjs"' not in root_moon or 'command: "tools/release/release.py check"' in root_moon + or 'command: "tools/release/check_release_metadata.py"' in root_moon ): fail("active release check, registry-check, verify, and consumer-shape orchestration must live in Bun helpers; release.py is only the protected publish implementation and compatibility bridge") if ( From 30cd9147e59c5149141cb27067db385c55b0064c Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 04:20:37 +0000 Subject: [PATCH 258/308] chore: retire release check compatibility commands --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 18 +++++---- docs/internal/IMPLEMENTATION_CHECKLIST.md | 27 +++++++------ tools/policy/check-tooling-stack.sh | 17 ++++++++ tools/release/check_release_metadata.py | 19 ++++++--- tools/release/release-check.mjs | 4 +- tools/release/release.py | 40 ++----------------- 6 files changed, 62 insertions(+), 63 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 3392a33a..48945a8a 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -114,19 +114,23 @@ until the current-state gates here are checked with fresh local evidence. showed no callers for any of these symbols; `cargo_package_args` was a stale twin of the still-used `cargo_publish_args`, and the publish-target helper remains only where it is actually used in `check_release_metadata.py`. +- 2026-06-28: Retired the non-publish `tools/release/release.py` + compatibility subcommands (`check`, `check-registries`, `consumer-shape`, + and `verify-release`). The direct command surface for those gates is now the + Bun helper set; `release.py publish-dry-run` still invokes `release-check.mjs` + and `release-check-registries.mjs` internally before protected publish + dry-runs. - 2026-06-27: Moved the active release metadata check orchestration to the Bun entrypoint `tools/release/release-check.mjs`. Moon `release-tools:check`, `release-tools:release-check`, and the release workflow now call the Bun - helper directly, while `tools/release/release.py check` remains only a - compatibility delegator. The new helper runs release policy, - release-please config, artifact target, release PR sync/coverage, - release-metadata, and consumer-shape readiness checks in the same order as - the previous Python command. + helper directly. The new helper runs release policy, release-please config, + artifact target, release PR sync/coverage, release-metadata, and + consumer-shape readiness checks in the same order as the previous Python + command. - 2026-06-27: Moved the remaining non-publish release workflow command surfaces to Bun helpers: `release-check-registries.mjs`, `release-verify.mjs`, and `release-consumer-shape.mjs`. The release workflow - and Moon consumer-shape task now use those helpers directly; `release.py` - keeps compatibility delegators for existing local command habits while active + and Moon consumer-shape task now use those helpers directly while active CI/release orchestration is no longer routed through Python for these gates. - 2026-06-27: Moved the Rust SDK generated publish-source preparation command from `tools/release/release.py prepare-rust-release-source` to the Bun diff --git a/docs/internal/IMPLEMENTATION_CHECKLIST.md b/docs/internal/IMPLEMENTATION_CHECKLIST.md index 156870bd..70b6dae6 100644 --- a/docs/internal/IMPLEMENTATION_CHECKLIST.md +++ b/docs/internal/IMPLEMENTATION_CHECKLIST.md @@ -585,8 +585,8 @@ Run before claiming this architecture complete: The emitted AOT matrix contains the single friendly target id `linux-x64-gnu`. - [x] `tools/dev/bun.sh tools/release/release_plan.mjs` -- [x] `tools/release/release.py check` -- [x] `tools/release/release.py consumer-shape --format json --require-ready +- [x] `tools/dev/bun.sh tools/release/release-check.mjs` +- [x] `tools/dev/bun.sh tools/release/release-consumer-shape.mjs --format json --require-ready --products-json '["oliphaunt-swift"]'` - [x] `tools/release/release.py publish-dry-run --products-json '["oliphaunt-extension-vector"]' --head-ref HEAD` fails closed when the @@ -774,7 +774,7 @@ Run before claiming this architecture complete: touched Python release/graph modules, `bash tools/policy/check-sdk-mobile-extension-surface.sh`, `python3 tools/release/artifact_target_matrix.py extension-artifacts-native`, - and `tools/release/release.py consumer-shape --format json --require-ready + and `tools/dev/bun.sh tools/release/release-consumer-shape.mjs --format json --require-ready --products-json '["oliphaunt-extension-vector"]'`. - [x] GitHub Builds run `27383810080` on `d7ad6eca` proved the next CI-only blockers: the WASIX runtime committed asset-input fingerprint was stale, @@ -992,10 +992,11 @@ Run before claiming this architecture complete: follow-up bumps both products to `0.6.0`, updates the WASIX runtime asset/AOT 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.mjs --check`, `cargo metadata - --locked --format-version 1 --no-deps`, `tools/release/release.py - check-registries --products-json "$(cat + dependency. Local checks passed after the bump: `tools/dev/bun.sh + tools/release/release-check.mjs`, + `tools/release/sync-example-lockfiles.mjs --check`, `cargo metadata + --locked --format-version 1 --no-deps`, `tools/dev/bun.sh + tools/release/release-check-registries.mjs --products-json "$(cat target/release-dry-run-local/products.json)" --head-ref HEAD`, and `git diff --check`. - [x] The WASIX Rust publishing surface now uses the WASIX product name instead @@ -1005,8 +1006,8 @@ Run before claiming this architecture complete: 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 - packages, `tools/release/release.py check` passes, and - `tools/release/release.py check-registries --products-json "$(cat + packages, `tools/dev/bun.sh tools/release/release-check.mjs` passes, and + `tools/dev/bun.sh tools/release/release-check-registries.mjs --products-json "$(cat target/release-dry-run-local/products.json)" --head-ref HEAD` reports `crates:oliphaunt-wasix@0.6.0` plus the renamed internal WASIX crates. - [x] GitHub Builds run `27434296236` on `cf0ef3f2` proved the WASIX rename @@ -1133,7 +1134,7 @@ Run before claiming this architecture complete: the aggregate `E2E` gate, the aggregate `Builds` gate, and `Required`. - [ ] Release workflow dry-run green for selected products. Current local blocker after the WASIX `0.6.0` bump is registry identity bootstrap, not - version freshness: `tools/release/release.py check-registries --products-json + version freshness: `tools/dev/bun.sh tools/release/release-check-registries.mjs --products-json "$(cat target/release-dry-run-local/products.json)" --head-ref HEAD --require-identities` fails because first-public-release package identities are still missing for crates.io, Maven Central, npm, and JSR packages, @@ -1149,8 +1150,8 @@ Run before claiming this architecture complete: platform npm packages publish with provenance and OS/CPU/libc constraints, release metadata declares exactly those optional packages, and the TypeScript SDK can keep selecting Node direct by exact optional platform packages. - Evidence: `tools/release/release.py consumer-shape --require-ready --product - oliphaunt-node-direct` and `tools/release/release.py consumer-shape + Evidence: `tools/dev/bun.sh tools/release/release-consumer-shape.mjs --require-ready --product + oliphaunt-node-direct` and `tools/dev/bun.sh tools/release/release-consumer-shape.mjs --require-ready --products-json "$(cat target/release-dry-run-local/products.json)"` pass. - [x] Windows native exact-extension coverage has a producer path for all nine @@ -1167,7 +1168,7 @@ Run before claiming this architecture complete: products. Local evidence after this patch passed: `python3 src/extensions/tools/check-extension-model.py --write-evidence`, `python3 src/extensions/tools/check-extension-model.py --check`, - `python3 tools/release/release.py check`, + `tools/dev/bun.sh tools/release/release-check.mjs`, `python3 tools/release/artifact_target_matrix.py extension-artifacts-native`, and `git diff --check`. GitHub CI run `27744307637` then passed `Builds / extension-native (windows-x64-msvc)`, proving the expanded MSVC producers on diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index b75d7595..49605c10 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -366,6 +366,23 @@ grep -Fq 'tools/dev/bun.sh tools/release/prepare-rust-release-source.mjs' src/sd if grep -Fq '"prepare-rust-release-source"' tools/release/release.py; then fail "release.py must not retain the Rust SDK prepare-rust-release-source command surface after it moved to Bun" fi +for retired_release_command in \ + 'def command_check(' \ + 'def command_check_registries(' \ + 'def command_consumer_shape(' \ + 'def command_verify_release(' \ + 'command == "check"' \ + 'command == "check-registries"' \ + 'command == "consumer-shape"' \ + 'command == "verify-release"' \ + '"check-registries",' \ + '"consumer-shape",' \ + '"verify-release",' +do + if grep -Fq "$retired_release_command" tools/release/release.py; then + fail "release.py must not retain non-publish release check command surface: $retired_release_command" + fi +done grep -Fq 'tools/release/check-release-metadata.mjs' tools/release/release-check.mjs || fail "release-check must route release metadata validation through the Bun entrypoint" grep -Fq 'command: "tools/dev/bun.sh tools/release/release-check.mjs"' moon.yml || diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index a7fd0a57..235f85bc 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -725,10 +725,19 @@ def validate_graph_files() -> None: examples_readme = read_text("examples/README.md") examples_local_registries = read_text("examples/tools/with-local-registries.sh") if ( - '"tools/release/release-check.mjs", *args' not in release_source - or '"tools/release/release-check-registries.mjs", *args' not in release_source - or '"tools/release/release-consumer-shape.mjs", *args' not in release_source - or '"tools/release/release-verify.mjs", *args' not in release_source + '"tools/release/release-check.mjs"' not in release_source + or '"tools/release/release-check-registries.mjs", *passthrough' not in release_source + or "def command_check(" in release_source + or "def command_check_registries(" in release_source + or "def command_consumer_shape(" in release_source + or "def command_verify_release(" in release_source + or '"check-registries",' in release_source + or '"consumer-shape",' in release_source + or '"verify-release",' in release_source + or 'command == "check"' in release_source + or 'command == "check-registries"' in release_source + or 'command == "consumer-shape"' in release_source + or 'command == "verify-release"' in release_source or "tools/release/check_release_pr_coverage.mjs" not in release_check or "tools/release/check-release-metadata.mjs" not in release_check or '["python3", "tools/release/check_release_metadata.py"]' in release_check @@ -753,7 +762,7 @@ def validate_graph_files() -> None: or 'command: "tools/release/release.py check"' in root_moon or 'command: "tools/release/check_release_metadata.py"' in root_moon ): - fail("active release check, registry-check, verify, and consumer-shape orchestration must live in Bun helpers; release.py is only the protected publish implementation and compatibility bridge") + fail("active release check, registry-check, verify, and consumer-shape orchestration must live in Bun helpers; release.py must keep only the protected publish and publish-dry-run implementation") if ( "tools/dev/bun.sh tools/release/prepare-rust-release-source.mjs" not in rust_sdk_check or '"prepare-rust-release-source"' in release_source diff --git a/tools/release/release-check.mjs b/tools/release/release-check.mjs index 4cd75347..16a72fa3 100644 --- a/tools/release/release-check.mjs +++ b/tools/release/release-check.mjs @@ -7,11 +7,11 @@ function parseArgs(argv) { for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === "-h" || arg === "--help") { - console.log(`usage: tools/release/release-check.mjs [release.py check passthrough args] + console.log(`usage: tools/release/release-check.mjs [legacy passthrough args] Runs the repository release metadata, release-please, artifact target, release PR, and consumer-shape readiness checks. Current passthrough flags are -accepted for compatibility with release.py check and release workflow callers. +accepted for compatibility with release workflow and Moon callers. `); process.exit(0); } diff --git a/tools/release/release.py b/tools/release/release.py index 4a9d9aed..bc22566b 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -2172,22 +2172,6 @@ def run_product_publish_dry_runs(products: list[str], *, allow_dirty: bool, head fail(f"no publish dry-run handler for {product}") -def command_check(args: list[str]) -> None: - run(["tools/dev/bun.sh", "tools/release/release-check.mjs", *args]) - - -def command_check_registries(args: list[str]) -> None: - run(["tools/dev/bun.sh", "tools/release/release-check-registries.mjs", *args]) - - -def command_consumer_shape(args: list[str]) -> None: - run(["tools/dev/bun.sh", "tools/release/release-consumer-shape.mjs", *args]) - - -def command_verify_release(args: list[str]) -> None: - run(["tools/dev/bun.sh", "tools/release/release-verify.mjs", *args]) - - def publish_liboliphaunt_github_assets(head_ref: str) -> None: verify_release_tag("liboliphaunt-native", head_ref) ensure_liboliphaunt_release_assets() @@ -3608,10 +3592,10 @@ def command_publish_product_step(args: argparse.Namespace) -> None: def command_publish_dry_run(args: argparse.Namespace, passthrough: list[str]) -> None: - command_check([]) + run(["tools/dev/bun.sh", "tools/release/release-check.mjs"]) products = selected_products_from_passthrough(passthrough) if products: - command_check_registries(passthrough) + run(["tools/dev/bun.sh", "tools/release/release-check-registries.mjs", *passthrough]) run_product_publish_dry_runs( products, allow_dirty=args.allow_dirty, @@ -3621,7 +3605,7 @@ def command_publish_dry_run(args: argparse.Namespace, passthrough: list[str]) -> if args.wasm: run_wasm_release_dry_run(args.allow_dirty) if passthrough: - command_check_registries(passthrough) + run(["tools/dev/bun.sh", "tools/release/release-check-registries.mjs", *passthrough]) def command_publish(args: argparse.Namespace, passthrough: list[str]) -> None: @@ -3645,14 +3629,6 @@ def main(argv: list[str]) -> int: parser = argparse.ArgumentParser(description=__doc__) subparsers = parser.add_subparsers(dest="command", required=True) - for name in [ - "check", - "check-registries", - "consumer-shape", - "verify-release", - ]: - subparsers.add_parser(name, add_help=False) - dry_run = subparsers.add_parser("publish-dry-run") dry_run.add_argument("--wasm", action="store_true") dry_run.add_argument("--allow-dirty", action="store_true") @@ -3667,15 +3643,7 @@ def main(argv: list[str]) -> int: args, passthrough = parser.parse_known_args(argv) command = args.command - if command == "check": - command_check(passthrough) - elif command == "check-registries": - command_check_registries(passthrough) - elif command == "consumer-shape": - command_consumer_shape(passthrough) - elif command == "verify-release": - command_verify_release(passthrough) - elif command == "publish-dry-run": + if command == "publish-dry-run": command_publish_dry_run(args, passthrough) elif command == "publish": command_publish(args, passthrough) From d2275b54b3bec5bc23838293d6c92edb26578c07 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 04:28:43 +0000 Subject: [PATCH 259/308] chore: run no-product release dry-run in bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 16 +++++++++------- docs/internal/IMPLEMENTATION_CHECKLIST.md | 5 +++-- tools/policy/check-tooling-stack.sh | 4 ++++ tools/release/check_release_metadata.py | 4 +++- tools/release/release-publish.mjs | 18 ++++++++++++++---- 5 files changed, 33 insertions(+), 14 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 48945a8a..4b51a86a 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -81,10 +81,12 @@ until the current-state gates here are checked with fresh local evidence. - 2026-06-28: Added the Bun publish command surface `tools/release/release-publish.mjs` for active release workflow `publish-dry-run` and `publish` calls. The workflow now invokes publish - operations through `tools/dev/bun.sh tools/release/release-publish.mjs`, while - the existing protected `release.py` implementation remains behind that - entrypoint until publish dispatch is ported. Release metadata and tooling - guards now reject direct workflow `release.py publish*` calls. + operations through `tools/dev/bun.sh tools/release/release-publish.mjs`. + The no-product publish dry-run now runs `release-check.mjs` directly in Bun; + product-scoped dry-runs and publish dispatch remain behind the protected + `release.py` implementation until publish dispatch is ported. Release + metadata and tooling guards reject direct workflow `release.py publish*` + calls. - 2026-06-28: Added Bun command surfaces for the remaining active release metadata and consumer-shape validator implementations: `tools/release/check-release-metadata.mjs` and @@ -117,9 +119,9 @@ until the current-state gates here are checked with fresh local evidence. - 2026-06-28: Retired the non-publish `tools/release/release.py` compatibility subcommands (`check`, `check-registries`, `consumer-shape`, and `verify-release`). The direct command surface for those gates is now the - Bun helper set; `release.py publish-dry-run` still invokes `release-check.mjs` - and `release-check-registries.mjs` internally before protected publish - dry-runs. + Bun helper set; product-scoped `release.py publish-dry-run` still invokes + `release-check.mjs` and `release-check-registries.mjs` internally before + protected publish dry-runs. - 2026-06-27: Moved the active release metadata check orchestration to the Bun entrypoint `tools/release/release-check.mjs`. Moon `release-tools:check`, `release-tools:release-check`, and the release workflow now call the Bun diff --git a/docs/internal/IMPLEMENTATION_CHECKLIST.md b/docs/internal/IMPLEMENTATION_CHECKLIST.md index 70b6dae6..16faf07e 100644 --- a/docs/internal/IMPLEMENTATION_CHECKLIST.md +++ b/docs/internal/IMPLEMENTATION_CHECKLIST.md @@ -588,7 +588,7 @@ Run before claiming this architecture complete: - [x] `tools/dev/bun.sh tools/release/release-check.mjs` - [x] `tools/dev/bun.sh tools/release/release-consumer-shape.mjs --format json --require-ready --products-json '["oliphaunt-swift"]'` -- [x] `tools/release/release.py publish-dry-run --products-json +- [x] `tools/dev/bun.sh tools/release/release-publish.mjs publish-dry-run --products-json '["oliphaunt-extension-vector"]' --head-ref HEAD` fails closed when the staged exact-extension package is incomplete or missing. - [x] `python3 tools/release/artifact_target_matrix.py @@ -695,7 +695,8 @@ Run before claiming this architecture complete: - [x] `./gradlew :oliphaunt-android-gradle-plugin:compileJava :oliphaunt:tasks --no-daemon` - [x] `swift test --package-path src/sdks/swift --scratch-path target/swift-test-extension-resolver-2` -- [x] `tools/release/release.py publish-dry-run` passes in public no-product +- [x] `tools/dev/bun.sh tools/release/release-publish.mjs publish-dry-run` + passes in public no-product policy/metadata mode. Product-scoped dry-runs still require staged builder artifacts from the same-SHA `Builds` workflow and remain covered by the release workflow evidence items below. diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 49605c10..326c0875 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -409,6 +409,10 @@ if grep -Fq '["tools/release/check_consumer_shape.py"' tools/release/release-con fi grep -Fq 'const COMMANDS = new Set(["publish", "publish-dry-run"]);' tools/release/release-publish.mjs || fail "release publish and dry-run commands must share the Bun release-publish entrypoint" +grep -Fq 'function isNoProductPublishDryRun(' tools/release/release-publish.mjs || + fail "release publish dry-run wrapper must own the no-product dry-run path in Bun" +grep -Fq 'run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check.mjs"]);' tools/release/release-publish.mjs || + fail "release publish dry-run wrapper must run release-check directly for no-product dry-runs" grep -Fq 'tools/dev/bun.sh tools/release/release-publish.mjs publish-dry-run' .github/workflows/release.yml || fail "release workflow publish dry-runs must use the Bun release-publish entrypoint" grep -Fq 'tools/dev/bun.sh tools/release/release-publish.mjs publish ' .github/workflows/release.yml || diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 235f85bc..7b2e2ebd 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -997,9 +997,11 @@ def validate_publish_target_coverage() -> None: or "tools/release/release.py publish-dry-run" in workflow or "tools/release/release.py publish --" in workflow or 'const COMMANDS = new Set(["publish", "publish-dry-run"]);' not in release_publish + or 'function isNoProductPublishDryRun(' not in release_publish + or 'run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check.mjs"]);' not in release_publish or 'spawnSync("tools/release/release.py", argv' not in release_publish ): - fail("Release workflow publish and publish-dry-run commands must use the Bun release-publish entrypoint while release.py keeps the protected implementation") + fail("Release workflow publish commands must use the Bun release-publish entrypoint, and no-product publish dry-runs must run release-check without launching release.py") 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 diff --git a/tools/release/release-publish.mjs b/tools/release/release-publish.mjs index 1a757cce..6874bbbc 100755 --- a/tools/release/release-publish.mjs +++ b/tools/release/release-publish.mjs @@ -1,17 +1,18 @@ #!/usr/bin/env bun import { spawnSync } from "node:child_process"; -import { ROOT } from "./release-cli-utils.mjs"; +import { ROOT, run } from "./release-cli-utils.mjs"; const TOOL = "release-publish.mjs"; const COMMANDS = new Set(["publish", "publish-dry-run"]); function usage() { - console.log(`usage: tools/release/release-publish.mjs [release.py passthrough args] + console.log(`usage: tools/release/release-publish.mjs [publish args] Runs protected release publish and publish dry-run operations through the Bun -release command surface. The current implementation delegates to release.py -while publish dispatch is ported. +release command surface. The public no-product publish dry-run is handled in +Bun; product, WASIX, and publish dispatch still delegate to release.py while the +protected implementation is ported. `); } @@ -33,6 +34,15 @@ if (!COMMANDS.has(command)) { fail(`expected publish or publish-dry-run, got ${command ?? ""}`); } +function isNoProductPublishDryRun(command, args) { + return command === "publish-dry-run" && args.every((arg) => arg === "--allow-dirty"); +} + +if (isNoProductPublishDryRun(command, argv.slice(1))) { + run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check.mjs"]); + process.exit(0); +} + const result = spawnSync("tools/release/release.py", argv, { cwd: ROOT, stdio: "inherit", From 2f40cacb7f32a0ad03aaf45ba570492e5c143798 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 04:37:26 +0000 Subject: [PATCH 260/308] chore: refine helper dead-code scanner --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 7 +++++++ tools/policy/check-tooling-stack.sh | 4 ++++ .../list-helper-reference-candidates.mjs | 19 +++++++++++++++---- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 4b51a86a..31526637 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,13 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-28: Tightened the helper dead-code scanner so + `tools/policy/list-helper-reference-candidates.mjs` only treats JavaScript + files as helper entrypoints when they have a shebang or explicit + `Bun.argv`/`process.argv` handling. This removes shared modules and config + files from the candidate list, including `tools/test/release-fixture-utils.mjs` + and `src/docs/postcss.config.mjs`, while keeping real CLI scripts visible for + review. - 2026-06-28: Added the Bun publish command surface `tools/release/release-publish.mjs` for active release workflow `publish-dry-run` and `publish` calls. The workflow now invokes publish diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 326c0875..6eb8aaff 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -274,6 +274,10 @@ bun tools/policy/check-python-entrypoints.mjs bun tools/policy/check-rust-helper-crates.mjs bun tools/policy/check-sdk-manifest.mjs bun tools/policy/list-helper-reference-candidates.mjs --max-refs 0 --active-only +grep -Fq 'function helperLooksLikeEntrypoint(' tools/policy/list-helper-reference-candidates.mjs || + fail "helper reference candidate scanner must distinguish entrypoint-shaped JavaScript helpers from shared modules" +grep -Fq 'entrypoint-shaped JavaScript helpers' tools/policy/list-helper-reference-candidates.mjs || + fail "helper reference candidate scanner help must describe its JavaScript entrypoint filtering" bun tools/policy/list-source-reference-candidates.mjs --max-refs 0 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" diff --git a/tools/policy/list-helper-reference-candidates.mjs b/tools/policy/list-helper-reference-candidates.mjs index 26282ce1..cab771bf 100644 --- a/tools/policy/list-helper-reference-candidates.mjs +++ b/tools/policy/list-helper-reference-candidates.mjs @@ -14,10 +14,12 @@ function fail(message) { function usage() { console.log(`usage: tools/policy/list-helper-reference-candidates.mjs [--max-refs N] [--active-only] [--include-allowlisted] [--json] -Lists tracked shell, Python, and JavaScript helper entrypoints with few textual -references. The output is advisory: each candidate still needs manual review -before removal because some entrypoints are intentionally invoked by humans or -external tools. +Lists tracked shell and Python helpers plus entrypoint-shaped JavaScript helpers +with few textual references. JavaScript modules must have a shebang or explicit +Bun.argv/process.argv handling to be treated as entrypoints, so shared modules +and config files do not drown out real cleanup candidates. The output is +advisory: each candidate still needs manual review before removal because some +entrypoints are intentionally invoked by humans or external tools. Use --active-only to ignore Markdown/docs references and focus on code, CI, and tooling callers. By default, entries in ${ALLOWLIST} are hidden; pass @@ -91,11 +93,20 @@ function trackedHelpers() { .split("\0") .filter(Boolean) .filter((path) => isFile(path)) + .filter((path) => helperLooksLikeEntrypoint(path)) .filter((path) => !path.includes("/node_modules/")) .filter((path) => !path.startsWith("target/")) .sort(); } +function helperLooksLikeEntrypoint(path) { + if (!path.endsWith(".mjs")) { + return true; + } + const text = readFileSync(path, "utf8"); + return text.startsWith("#!") || /\b(?:Bun|process)\.argv\b/u.test(text); +} + function parseAllowlist() { const entries = new Map(); const text = readFileSync(ALLOWLIST, "utf8"); From bd57e5d7b7cb774adda64bf271e856214e05edb4 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 04:42:52 +0000 Subject: [PATCH 261/308] docs: route release dry-run through bun --- CONTRIBUTING.md | 2 +- docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 5 +++++ tools/policy/check-tooling-stack.sh | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 34ae0e3c..891f1d11 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ pnpm fmt:check pnpm check pnpm test pnpm release-check -tools/release/release.py publish-dry-run +tools/dev/bun.sh tools/release/release-publish.mjs publish-dry-run ``` The runtime smoke starts embedded Postgres and is intentionally slower than unit tests. diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 31526637..587bc500 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,11 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-28: Updated the contributor local release dry-run command from direct + `tools/release/release.py publish-dry-run` to + `tools/dev/bun.sh tools/release/release-publish.mjs publish-dry-run`, and + added a tooling-stack guard so public contributor docs do not regress to the + protected Python implementation path. - 2026-06-28: Tightened the helper dead-code scanner so `tools/policy/list-helper-reference-candidates.mjs` only treats JavaScript files as helper entrypoints when they have a shebang or explicit diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 6eb8aaff..a6a14af7 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -427,6 +427,9 @@ fi if grep -Fq 'tools/release/release.py publish --' .github/workflows/release.yml; then fail "release workflow must not call release.py publish directly" fi +if grep -Fq 'tools/release/release.py publish-dry-run' CONTRIBUTING.md; then + fail "contributing docs must use the Bun release-publish entrypoint for publish dry-runs" +fi grep -Fq 'tools/dev/bun.sh tools/release/local-registry-publish.mjs download' examples/README.md || fail "example local-registry setup docs must use the Bun local-registry command" grep -Fq 'tools/dev/bun.sh tools/release/local-registry-publish.mjs publish' examples/README.md || From 8b69c3c0df07afce608967c74f7e7032f5cad742 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 04:48:03 +0000 Subject: [PATCH 262/308] chore: route extension model guidance through bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 6 ++++ ...2026-06-07-transitional-catalog-smoke.json | 2 +- .../generated/mobile/static-extensions.tsv | 2 +- src/extensions/tools/check-extension-model.py | 30 +++++++++++-------- src/sdks/js/src/generated/extensions.ts | 2 +- .../dev/oliphaunt/GeneratedExtensions.kt | 2 +- .../react-native/src/generated/extensions.ts | 2 +- src/sdks/rust/src/generated/extensions.rs | 2 +- .../check-sdk-mobile-extension-surface.sh | 2 +- tools/policy/check-tooling-stack.sh | 5 ++++ 10 files changed, 35 insertions(+), 20 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 587bc500..ebef3c7d 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,12 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-28: Updated extension-model generated headers, stale-file repair + messages, and transitional evidence collector metadata to point at the Bun + `src/extensions/tools/check-extension-model.mjs` command surface instead of + the Python implementation file. The generator now centralizes the wrapper + command strings, and tooling-stack guards reject stale-file messages that send + contributors back to direct Python. - 2026-06-28: Updated the contributor local release dry-run command from direct `tools/release/release.py publish-dry-run` to `tools/dev/bun.sh tools/release/release-publish.mjs publish-dry-run`, 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 c67f4ac0..4efa115e 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 @@ -1,5 +1,5 @@ { - "collector": "src/extensions/tools/check-extension-model.py --write-evidence", + "collector": "tools/dev/bun.sh src/extensions/tools/check-extension-model.mjs --write-evidence", "evidenceTier": "transitional-catalog-smoke", "id": "2026-06-07-transitional-catalog-smoke", "notes": "Transitional evidence imported from extensions.smoke.toml while per-recipe evidence runs are introduced.", diff --git a/src/extensions/generated/mobile/static-extensions.tsv b/src/extensions/generated/mobile/static-extensions.tsv index 19cb885c..355fff7c 100644 --- a/src/extensions/generated/mobile/static-extensions.tsv +++ b/src/extensions/generated/mobile/static-extensions.tsv @@ -1,4 +1,4 @@ -# @generated by src/extensions/tools/check-extension-model.py --write +# @generated by src/extensions/tools/check-extension-model.mjs --write sql-name native-module-stem source-kind source-rel mobile-static-dependencies ios-static-dependencies android-static-dependencies include-dependencies include-dirs cflags hash-source-dependencies ios-hash-source-dependencies android-hash-source-dependencies hash-dirs source-files source-recursive-dirs amcheck amcheck contrib contrib/amcheck auto_explain auto_explain contrib contrib/auto_explain diff --git a/src/extensions/tools/check-extension-model.py b/src/extensions/tools/check-extension-model.py index 59028f98..0236aca3 100755 --- a/src/extensions/tools/check-extension-model.py +++ b/src/extensions/tools/check-extension-model.py @@ -47,6 +47,10 @@ GENERATED_MOBILE_STATIC_SPECS = ROOT / "src/extensions/generated/mobile/static-extensions.tsv" GENERATED_WASIX_METADATA = ROOT / "src/extensions/generated/wasix/extensions.json" BIOME_VERSION = "2.4.16" +CHECK_EXTENSION_MODEL_PATH = "src/extensions/tools/check-extension-model.mjs" +CHECK_EXTENSION_MODEL_COMMAND = f"tools/dev/bun.sh {CHECK_EXTENSION_MODEL_PATH}" +CHECK_EXTENSION_MODEL_WRITE_COMMAND = f"{CHECK_EXTENSION_MODEL_COMMAND} --write" +CHECK_EXTENSION_MODEL_WRITE_EVIDENCE_COMMAND = f"{CHECK_EXTENSION_MODEL_COMMAND} --write-evidence" RUST_INTERNAL_EXTENSION_CANDIDATES = [ { @@ -1043,7 +1047,7 @@ def camel(row: dict) -> dict: rows = [camel(row) for row in metadata.get("extensions", [])] source = ( - "// This file is generated by src/extensions/tools/check-extension-model.py.\n" + f"// This file is generated by {CHECK_EXTENSION_MODEL_PATH}.\n" "// Do not edit by hand.\n\n" "export type GeneratedExtensionMetadata = {\n" " readonly id: string;\n" @@ -1091,7 +1095,7 @@ def generated_kotlin_extension_module(metadata: dict) -> str: names = sorted(str(row["sql-name"]) for row in metadata.get("extensions", [])) body = "\n".join(f" {json.dumps(name)}," for name in names) return ( - "// This file is generated by src/extensions/tools/check-extension-model.py.\n" + f"// This file is generated by {CHECK_EXTENSION_MODEL_PATH}.\n" "// Do not edit by hand.\n\n" "package dev.oliphaunt\n\n" "internal val generatedExtensionSqlNames: Set = setOf(\n" @@ -1313,7 +1317,7 @@ def generated_rust_extension_module(catalog: dict) -> str: ) text = [ - "// @generated by src/extensions/tools/check-extension-model.py --write", + f"// @generated by {CHECK_EXTENSION_MODEL_PATH} --write", "// Do not edit by hand.", "", "use super::{", @@ -1532,9 +1536,9 @@ def validate_generated_text_file(path: Path, expected: str, write: bool) -> None path.write_text(expected, encoding="utf-8") return if not path.exists(): - fail(f"{rel(path)} is missing; run src/extensions/tools/check-extension-model.py --write") + fail(f"{rel(path)} is missing; run {CHECK_EXTENSION_MODEL_WRITE_COMMAND}") if path.read_text(encoding="utf-8") != expected: - fail(f"{rel(path)} is stale; run src/extensions/tools/check-extension-model.py --write") + fail(f"{rel(path)} is stale; run {CHECK_EXTENSION_MODEL_WRITE_COMMAND}") def generated_mobile_registry(catalog: dict) -> dict: @@ -1630,7 +1634,7 @@ def generated_mobile_static_specs(catalog: dict, build_plan: dict) -> str: ) rows.sort(key=lambda row: row[0]) lines = [ - "# @generated by src/extensions/tools/check-extension-model.py --write", + f"# @generated by {CHECK_EXTENSION_MODEL_PATH} --write", ( "sql-name\tnative-module-stem\tsource-kind\tsource-rel" "\tmobile-static-dependencies\tios-static-dependencies\tandroid-static-dependencies" @@ -1684,9 +1688,9 @@ def validate_generated_file(path: Path, expected: dict, write: bool) -> None: path.write_text(text, encoding="utf-8") return if not path.exists(): - fail(f"{rel(path)} is missing; run src/extensions/tools/check-extension-model.py --write") + fail(f"{rel(path)} is missing; run {CHECK_EXTENSION_MODEL_WRITE_COMMAND}") if path.read_text(encoding="utf-8") != text: - fail(f"{rel(path)} is stale; run src/extensions/tools/check-extension-model.py --write") + fail(f"{rel(path)} is stale; run {CHECK_EXTENSION_MODEL_WRITE_COMMAND}") parsed = read_json(path) if parsed.get("format-version") != 1: fail(f"{rel(path)} must use format-version 1") @@ -1790,10 +1794,10 @@ def validate_support_table(catalog: dict, write: bool) -> None: SUPPORT_TABLE.write_text(expected, encoding="utf-8") return if not SUPPORT_TABLE.exists(): - fail(f"{rel(SUPPORT_TABLE)} is missing; run src/extensions/tools/check-extension-model.py --write") + fail(f"{rel(SUPPORT_TABLE)} is missing; run {CHECK_EXTENSION_MODEL_WRITE_COMMAND}") actual = SUPPORT_TABLE.read_text(encoding="utf-8") if actual != expected: - fail(f"{rel(SUPPORT_TABLE)} is stale; run src/extensions/tools/check-extension-model.py --write") + fail(f"{rel(SUPPORT_TABLE)} is stale; run {CHECK_EXTENSION_MODEL_WRITE_COMMAND}") table = read_json(SUPPORT_TABLE) if table.get("format-version") != 1: fail(f"{rel(SUPPORT_TABLE)} must use format-version 1") @@ -1865,7 +1869,7 @@ def write_evidence_files(catalog: dict) -> None: "sourceDigest": source_digest(), "sourceDigestInputs": source_digest_inputs(), "observedAt": "2026-06-07T00:00:00Z", - "collector": "src/extensions/tools/check-extension-model.py --write-evidence", + "collector": CHECK_EXTENSION_MODEL_WRITE_EVIDENCE_COMMAND, "notes": ( "Transitional evidence imported from extensions.smoke.toml while " "per-recipe evidence runs are introduced." @@ -2026,10 +2030,10 @@ def validate_evidence_table(catalog: dict, write: bool) -> None: EVIDENCE_TABLE.write_text(expected, encoding="utf-8") return if not EVIDENCE_TABLE.exists(): - fail(f"{rel(EVIDENCE_TABLE)} is missing; run src/extensions/tools/check-extension-model.py --write") + fail(f"{rel(EVIDENCE_TABLE)} is missing; run {CHECK_EXTENSION_MODEL_WRITE_COMMAND}") actual = EVIDENCE_TABLE.read_text(encoding="utf-8") if actual != expected: - fail(f"{rel(EVIDENCE_TABLE)} is stale; run src/extensions/tools/check-extension-model.py --write") + fail(f"{rel(EVIDENCE_TABLE)} is stale; run {CHECK_EXTENSION_MODEL_WRITE_COMMAND}") table = read_json(EVIDENCE_TABLE) if table.get("format-version") != 1: fail(f"{rel(EVIDENCE_TABLE)} must use format-version 1") diff --git a/src/sdks/js/src/generated/extensions.ts b/src/sdks/js/src/generated/extensions.ts index 28992e07..0d7dd737 100644 --- a/src/sdks/js/src/generated/extensions.ts +++ b/src/sdks/js/src/generated/extensions.ts @@ -1,4 +1,4 @@ -// This file is generated by src/extensions/tools/check-extension-model.py. +// This file is generated by src/extensions/tools/check-extension-model.mjs. // Do not edit by hand. export type GeneratedExtensionMetadata = { diff --git a/src/sdks/kotlin/oliphaunt/src/commonMain/kotlin/dev/oliphaunt/GeneratedExtensions.kt b/src/sdks/kotlin/oliphaunt/src/commonMain/kotlin/dev/oliphaunt/GeneratedExtensions.kt index 0dfa59b3..a3378357 100644 --- a/src/sdks/kotlin/oliphaunt/src/commonMain/kotlin/dev/oliphaunt/GeneratedExtensions.kt +++ b/src/sdks/kotlin/oliphaunt/src/commonMain/kotlin/dev/oliphaunt/GeneratedExtensions.kt @@ -1,4 +1,4 @@ -// This file is generated by src/extensions/tools/check-extension-model.py. +// This file is generated by src/extensions/tools/check-extension-model.mjs. // Do not edit by hand. package dev.oliphaunt diff --git a/src/sdks/react-native/src/generated/extensions.ts b/src/sdks/react-native/src/generated/extensions.ts index 28992e07..0d7dd737 100644 --- a/src/sdks/react-native/src/generated/extensions.ts +++ b/src/sdks/react-native/src/generated/extensions.ts @@ -1,4 +1,4 @@ -// This file is generated by src/extensions/tools/check-extension-model.py. +// This file is generated by src/extensions/tools/check-extension-model.mjs. // Do not edit by hand. export type GeneratedExtensionMetadata = { diff --git a/src/sdks/rust/src/generated/extensions.rs b/src/sdks/rust/src/generated/extensions.rs index 706ff9bc..f59d8194 100644 --- a/src/sdks/rust/src/generated/extensions.rs +++ b/src/sdks/rust/src/generated/extensions.rs @@ -1,4 +1,4 @@ -// @generated by src/extensions/tools/check-extension-model.py --write +// @generated by src/extensions/tools/check-extension-model.mjs --write // Do not edit by hand. use super::{ diff --git a/tools/policy/check-sdk-mobile-extension-surface.sh b/tools/policy/check-sdk-mobile-extension-surface.sh index 0c641b7b..41f34028 100755 --- a/tools/policy/check-sdk-mobile-extension-surface.sh +++ b/tools/policy/check-sdk-mobile-extension-surface.sh @@ -228,7 +228,7 @@ require_text src/sdks/rust/src/extension.rs "generated_extensions::NATIVE_EXTENS "Rust SDK native extension manifest must delegate to generated metadata" require_text src/sdks/rust/src/extension.rs "generated_extensions::extension_data_files" \ "Rust SDK extension data files must delegate to generated metadata" -require_text src/sdks/rust/src/generated/extensions.rs "@generated by src/extensions/tools/check-extension-model.py" \ +require_text src/sdks/rust/src/generated/extensions.rs "@generated by src/extensions/tools/check-extension-model.mjs" \ "Rust SDK generated extension metadata must record its generator" require_text src/sdks/rust/src/generated/extensions.rs "pub enum Extension" \ "Rust SDK generated extension metadata must own the public Extension enum" diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index a6a14af7..6597b98d 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -219,6 +219,11 @@ 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" +grep -Fq 'CHECK_EXTENSION_MODEL_WRITE_COMMAND' src/extensions/tools/check-extension-model.py || + fail "extension model stale-file messages must point at the Bun wrapper command" +if grep -Fq 'run src/extensions/tools/check-extension-model.py --write' src/extensions/tools/check-extension-model.py; then + fail "extension model stale-file messages must not point contributors at the Python implementation" +fi grep -Fq 'command: "bun src/runtimes/liboliphaunt/native/tools/build-ci-target.mjs' src/runtimes/liboliphaunt/native/moon.yml && grep -Fq 'OLIPHAUNT_CI_TARGET' src/runtimes/liboliphaunt/native/moon.yml || fail "native CI target release task must use the Bun build-ci-target wrapper" From c3398f91f16f5ceb578cee0710cc78e140cd3416 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 04:50:23 +0000 Subject: [PATCH 263/308] chore: expose rust helper inventory as json --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 6 +++ tools/policy/check-rust-helper-crates.mjs | 38 ++++++++++++++----- tools/policy/check-tooling-stack.sh | 1 + 3 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 ebef3c7d..5d25fbe8 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,12 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-28: Harmonized the Rust helper crate inventory checker with the + Python entrypoint inventory by adding `--json` output to + `tools/policy/check-rust-helper-crates.mjs`. The JSON includes package name, + domain, migration decision, rationale, and manifest size for the two + intentionally retained Rust helper crates, so future non-Bun tooling audits + can consume both inventories mechanically. - 2026-06-28: Updated extension-model generated headers, stale-file repair messages, and transitional evidence collector metadata to point at the Bun `src/extensions/tools/check-extension-model.mjs` command surface instead of diff --git a/tools/policy/check-rust-helper-crates.mjs b/tools/policy/check-rust-helper-crates.mjs index 81006cc2..a573754a 100644 --- a/tools/policy/check-rust-helper-crates.mjs +++ b/tools/policy/check-rust-helper-crates.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env bun import { spawnSync } from "node:child_process"; -import { readFileSync } from "node:fs"; +import { readFileSync, statSync } from "node:fs"; const ALLOWLIST = "tools/policy/rust-helper-crates.allowlist"; const RUST_HELPER_PATHSPEC = ":(glob)tools/**/Cargo.toml"; @@ -13,13 +13,16 @@ function fail(message) { } function usage() { - console.log("usage: tools/policy/check-rust-helper-crates.mjs [--list]"); + console.log("usage: tools/policy/check-rust-helper-crates.mjs [--list] [--json]"); } let list = false; +let json = false; for (const arg of args) { if (arg === "--list") { list = true; + } else if (arg === "--json") { + json = true; } else if (arg === "--help" || arg === "-h") { usage(); process.exit(0); @@ -128,14 +131,31 @@ for (const path of trackedRustHelpers) { assertHelperCratePolicy(path); } -if (list) { +function inventoryEntry(path) { + const entry = allowlistedEntries.find((candidate) => candidate.path === path); + if (entry === undefined) { + fail(`internal error: ${path} missing from parsed allowlist`); + } + const manifest = Bun.TOML.parse(readFileSync(path, "utf8")); + const packageName = manifest?.package?.name; + return { + path, + packageName: typeof packageName === "string" ? packageName : null, + domain: entry.domain, + migrationDecision: entry.migrationDecision, + rationale: entry.rationale, + byteSize: statSync(path).size, + }; +} + +const inventory = trackedRustHelpers.map(inventoryEntry); + +if (json) { + console.log(JSON.stringify({ count: inventory.length, entries: inventory }, null, 2)); +} else if (list) { console.log(`Rust helper crate inventory verified (${trackedRustHelpers.length} tracked crates):`); - for (const path of trackedRustHelpers) { - const entry = allowlistedEntries.find((candidate) => candidate.path === path); - if (entry === undefined) { - fail(`internal error: ${path} missing from parsed allowlist`); - } - console.log(` ${path} domain=${entry.domain} decision=${entry.migrationDecision}`); + for (const entry of inventory) { + console.log(` ${entry.path} package=${entry.packageName ?? ""} domain=${entry.domain} decision=${entry.migrationDecision}`); } } else { console.log(`Rust helper crate inventory verified (${trackedRustHelpers.length} tracked crates).`); diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 6597b98d..b850c113 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -277,6 +277,7 @@ grep -Fq 'install_cargo_tool ripgrep rg "$RIPGREP_VERSION"' tools/dev/bootstrap- bun tools/policy/check-python-entrypoints.mjs bun tools/policy/check-rust-helper-crates.mjs +bun tools/policy/check-rust-helper-crates.mjs --json >/dev/null bun tools/policy/check-sdk-manifest.mjs bun tools/policy/list-helper-reference-candidates.mjs --max-refs 0 --active-only grep -Fq 'function helperLooksLikeEntrypoint(' tools/policy/list-helper-reference-candidates.mjs || From 37f7da7b73f3e2dfca8d9255bbc98d01a8281db0 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 04:53:01 +0000 Subject: [PATCH 264/308] docs: clarify bun release command surface --- docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 6 ++++++ docs/maintainers/repo-structure.md | 12 +++++++----- docs/maintainers/tooling.md | 12 ++++++++---- tools/policy/check-docs.sh | 7 +++++++ 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 5d25fbe8..4ce986e4 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,12 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-28: Updated current maintainer tooling docs so protected publishing + guidance names the Bun `tools/release/release-publish.mjs` command surface and + treats `tools/release/release.py` only as a temporary protected implementation + detail while publish dispatch is being ported. Docs policy now rejects the + old active maintainer wording that Cargo publishing runs directly through + `release.py`. - 2026-06-28: Harmonized the Rust helper crate inventory checker with the Python entrypoint inventory by adding `--json` output to `tools/policy/check-rust-helper-crates.mjs`. The JSON includes package name, diff --git a/docs/maintainers/repo-structure.md b/docs/maintainers/repo-structure.md index e30b07c2..0cb9917f 100644 --- a/docs/maintainers/repo-structure.md +++ b/docs/maintainers/repo-structure.md @@ -221,11 +221,13 @@ gap must be represented as an explicit unsupported error and justified in - `package.json` owns JavaScript workspace metadata only. Do not add root workflow aliases; run product and repo work through Moon targets directly. - Release-please owns product versions, changelogs, release PRs, and - product-scoped tags. `tools/release/release.py` owns protected publish steps, - registry checks, and GitHub release assets. -- Cargo publishing runs through `tools/release/release.py` and `cargo publish` - from the protected Release workflow. Do not add a Rust-only release - orchestrator beside release-please. + product-scoped tags. Bun release entrypoints under `tools/release/*.mjs` own + the public check, dry-run, and publish command surface; `release.py` remains a + protected implementation detail only while publish dispatch is being ported. +- Cargo publishing runs through `tools/dev/bun.sh + tools/release/release-publish.mjs publish` and `cargo publish` from the + protected Release workflow. Do not add a Rust-only release orchestrator beside + release-please. - `tools/xtask` owns Rust-heavy automation and release asset orchestration. - `tools/policy`, `tools/dev`, `tools/perf`, and `tools/release` own shell/Python/Node entrypoints by responsibility. CI is thin workflow diff --git a/docs/maintainers/tooling.md b/docs/maintainers/tooling.md index 65f8e5d6..bf9df2bd 100644 --- a/docs/maintainers/tooling.md +++ b/docs/maintainers/tooling.md @@ -15,9 +15,11 @@ predictable without hiding ecosystem-native behavior. - Product-local `targets/*.toml` files own platform artifact metadata. - Product-native build tools own product behavior: Cargo, SwiftPM/Xcode, Gradle, npm/JSR, Expo, React Native Codegen, and PostgreSQL build scripts. -- `tools/release/release.py` currently owns the protected implementation behind - Bun release check, verify, and publish entrypoints: registry checks, - checksums, attestations, and GitHub release asset verification. +- Bun release entrypoints under `tools/release/*.mjs` own the public release + check, dry-run, and publish command surface. `tools/release/release.py` + remains the protected implementation detail behind publish dispatch while + registry publishing, checksums, attestations, and GitHub release asset + verification are being ported. Do not add a second source graph, release graph, or root alias layer over Moon. Do not add a repo-wide tool because it is popular in one language ecosystem. @@ -172,7 +174,9 @@ What release-please does not own: - package-native publish commands; - verifying already-published GitHub release assets. -Those stay in `tools/release/release.py` and product-native release tasks. +Those stay behind the Bun release entrypoints and product-native release tasks. +Until publish dispatch is fully ported, the Bun publish entrypoint may delegate +protected implementation work to `tools/release/release.py`. Do not reintroduce release-plz, git-cliff product changelog ownership, a central release graph, or broad clean-registry reinstall gates as routine CI policy. diff --git a/tools/policy/check-docs.sh b/tools/policy/check-docs.sh index 63d5f487..bf346999 100755 --- a/tools/policy/check-docs.sh +++ b/tools/policy/check-docs.sh @@ -150,6 +150,13 @@ if git grep -n -F "${retired_tool_docs_args[@]}" -- docs/architecture docs/maint fi rm -f /tmp/docs-retired-tool-grep.$$ +if grep -Fq 'Cargo publishing runs through `tools/release/release.py`' docs/maintainers/repo-structure.md; then + fail "repo-structure maintainer docs must route Cargo publish guidance through the Bun release-publish entrypoint" +fi +if grep -Fq 'Those stay in `tools/release/release.py`' docs/maintainers/tooling.md; then + fail "tooling maintainer docs must treat release.py as a protected implementation detail, not the public release command surface" +fi + if git grep -n \ -e 'f0rr0/oliphaunt-oxide' \ -e 'github.com/f0rr0/oliphaunt-oxide' \ From bfda0905c76e97e7783b2683724033d4fa74f654 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 05:03:07 +0000 Subject: [PATCH 265/308] chore: keep no-product dry runs in bun --- tools/policy/check-tooling-stack.sh | 2 ++ tools/release/check_release_metadata.py | 3 ++- tools/release/release-publish.mjs | 17 ++++++++++++++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index b850c113..c04c27c8 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -423,6 +423,8 @@ grep -Fq 'function isNoProductPublishDryRun(' tools/release/release-publish.mjs fail "release publish dry-run wrapper must own the no-product dry-run path in Bun" grep -Fq 'run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check.mjs"]);' tools/release/release-publish.mjs || fail "release publish dry-run wrapper must run release-check directly for no-product dry-runs" +grep -Fq 'run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check-registries.mjs", ...passthrough]);' tools/release/release-publish.mjs || + fail "release publish dry-run wrapper must keep no-product passthrough registry checks in Bun" grep -Fq 'tools/dev/bun.sh tools/release/release-publish.mjs publish-dry-run' .github/workflows/release.yml || fail "release workflow publish dry-runs must use the Bun release-publish entrypoint" grep -Fq 'tools/dev/bun.sh tools/release/release-publish.mjs publish ' .github/workflows/release.yml || diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 7b2e2ebd..13d7eb53 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -999,9 +999,10 @@ def validate_publish_target_coverage() -> None: or 'const COMMANDS = new Set(["publish", "publish-dry-run"]);' not in release_publish or 'function isNoProductPublishDryRun(' not in release_publish or 'run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check.mjs"]);' not in release_publish + or 'run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check-registries.mjs", ...passthrough]);' not in release_publish or 'spawnSync("tools/release/release.py", argv' not in release_publish ): - fail("Release workflow publish commands must use the Bun release-publish entrypoint, and no-product publish dry-runs must run release-check without launching release.py") + fail("Release workflow publish commands must use the Bun release-publish entrypoint, and no-product publish dry-runs must run release-check and passthrough registry checks without launching release.py") 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 diff --git a/tools/release/release-publish.mjs b/tools/release/release-publish.mjs index 6874bbbc..0d587994 100755 --- a/tools/release/release-publish.mjs +++ b/tools/release/release-publish.mjs @@ -35,11 +35,26 @@ if (!COMMANDS.has(command)) { } function isNoProductPublishDryRun(command, args) { - return command === "publish-dry-run" && args.every((arg) => arg === "--allow-dirty"); + return command === "publish-dry-run" && noProductPublishDryRunPassthrough(args) !== null; +} + +function selectsProducts(args) { + return args.some((arg) => arg === "--products-json" || arg.startsWith("--products-json=")); +} + +function noProductPublishDryRunPassthrough(args) { + if (args.includes("--wasm") || selectsProducts(args)) { + return null; + } + return args.filter((arg) => arg !== "--allow-dirty"); } if (isNoProductPublishDryRun(command, argv.slice(1))) { + const passthrough = noProductPublishDryRunPassthrough(argv.slice(1)); run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check.mjs"]); + if (passthrough.length > 0) { + run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check-registries.mjs", ...passthrough]); + } process.exit(0); } From 23ac21008e3436ee5f4487253017d82dd00f4db3 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 05:05:17 +0000 Subject: [PATCH 266/308] docs: record release dry-run bun evidence --- docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 4ce986e4..444058b3 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,17 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-28: Expanded the Bun `tools/release/release-publish.mjs` + no-product `publish-dry-run` path so passthrough-only invocations such as + `--head-ref HEAD` run `release-check.mjs` and then + `release-check-registries.mjs` directly without launching the protected + `release.py` implementation. Product-selected dry-runs, WASIX dry-runs, and + protected publish dispatch still delegate to `release.py` until those package + validation paths are ported. Fresh evidence: + `tools/dev/bun.sh tools/release/release-publish.mjs publish-dry-run --head-ref HEAD`, + `tools/dev/bun.sh tools/release/check-release-metadata.mjs`, + `bash tools/policy/check-tooling-stack.sh`, `bash tools/policy/check-docs.sh`, + and `git diff --check`. - 2026-06-28: Updated current maintainer tooling docs so protected publishing guidance names the Bun `tools/release/release-publish.mjs` command surface and treats `tools/release/release.py` only as a temporary protected implementation From 6ce3c87a31267701c65ba7f3c153087111b7f600 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 05:21:26 +0000 Subject: [PATCH 267/308] chore: move sdk dry runs into bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 16 ++ tools/policy/check-tooling-stack.sh | 8 + tools/release/check_release_metadata.py | 7 +- tools/release/release-cli-utils.mjs | 4 +- tools/release/release-publish.mjs | 91 +++++++++++- tools/release/release-sdk-product-dry-run.mjs | 140 ++++++++++++++++++ 6 files changed, 260 insertions(+), 6 deletions(-) create mode 100644 tools/release/release-sdk-product-dry-run.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 444058b3..267a1e8a 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,22 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-28: Added the Bun SDK product dry-run helper + `tools/release/release-sdk-product-dry-run.mjs` and routed + `release-publish.mjs publish-dry-run --products-json ...` through it when the + selected products are entirely in the low-risk SDK set currently owned in Bun: + `oliphaunt-js` and `oliphaunt-react-native`. The release wrapper still runs + the standard release and registry dependency gates first, so product-selected + dry-runs keep the existing dependency-tag semantics. Fresh evidence: + `tools/dev/bun.sh tools/release/release-sdk-product-dry-run.mjs --product oliphaunt-js --allow-dirty` + passed against staged npm and JSR artifacts; the same helper for + `oliphaunt-react-native` failed at the expected missing staged SDK artifact in + this checkout. `release-publish.mjs publish-dry-run --products-json + '["oliphaunt-js"]' --head-ref HEAD` still stops at the existing registry + dependency gate because `liboliphaunt-native-v0.1.0` is not tagged and + `liboliphaunt-native` is not selected. Guards passed through + `tools/dev/bun.sh tools/release/check-release-metadata.mjs`, + `bash tools/policy/check-tooling-stack.sh`, and `git diff --check`. - 2026-06-28: Expanded the Bun `tools/release/release-publish.mjs` no-product `publish-dry-run` path so passthrough-only invocations such as `--head-ref HEAD` run `release-check.mjs` and then diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index c04c27c8..fbe369e6 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -425,6 +425,14 @@ grep -Fq 'run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check.mjs"]);' t fail "release publish dry-run wrapper must run release-check directly for no-product dry-runs" grep -Fq 'run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check-registries.mjs", ...passthrough]);' tools/release/release-publish.mjs || fail "release publish dry-run wrapper must keep no-product passthrough registry checks in Bun" +grep -Fq 'SUPPORTED_SDK_PRODUCT_DRY_RUNS' tools/release/release-publish.mjs || + fail "release publish dry-run wrapper must import the Bun SDK product dry-run support set" +grep -Fq 'runSdkProductDryRun(product, { allowDirty: productDryRunPlan.allowDirty });' tools/release/release-publish.mjs || + fail "release publish dry-run wrapper must execute supported SDK product dry-runs in Bun" +grep -Fq 'export const SUPPORTED_SDK_PRODUCT_DRY_RUNS = new Set(["oliphaunt-js", "oliphaunt-react-native"]);' tools/release/release-sdk-product-dry-run.mjs || + fail "release SDK product dry-run helper must declare Bun-owned low-risk SDK product dry-runs" +grep -Fq 'tools/release/check-staged-artifacts.mjs", "--require-sdk-product", product' tools/release/release-sdk-product-dry-run.mjs || + fail "Bun product dry-runs must validate staged SDK artifacts through the Bun checker" grep -Fq 'tools/dev/bun.sh tools/release/release-publish.mjs publish-dry-run' .github/workflows/release.yml || fail "release workflow publish dry-runs must use the Bun release-publish entrypoint" grep -Fq 'tools/dev/bun.sh tools/release/release-publish.mjs publish ' .github/workflows/release.yml || diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 13d7eb53..fafeb9e5 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -987,6 +987,7 @@ def validate_publish_target_coverage() -> None: workflow = read_text(".github/workflows/release.yml") release_source = read_text("tools/release/release.py") release_publish = read_text("tools/release/release-publish.mjs") + release_sdk_product_dry_run = read_text("tools/release/release-sdk-product-dry-run.mjs") 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: @@ -1000,9 +1001,13 @@ def validate_publish_target_coverage() -> None: or 'function isNoProductPublishDryRun(' not in release_publish or 'run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check.mjs"]);' not in release_publish or 'run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check-registries.mjs", ...passthrough]);' not in release_publish + or "SUPPORTED_SDK_PRODUCT_DRY_RUNS" not in release_publish + or 'runSdkProductDryRun(product, { allowDirty: productDryRunPlan.allowDirty });' not in release_publish + or 'export const SUPPORTED_SDK_PRODUCT_DRY_RUNS = new Set(["oliphaunt-js", "oliphaunt-react-native"]);' not in release_sdk_product_dry_run + or 'tools/release/check-staged-artifacts.mjs", "--require-sdk-product", product' not in release_sdk_product_dry_run or 'spawnSync("tools/release/release.py", argv' not in release_publish ): - fail("Release workflow publish commands must use the Bun release-publish entrypoint, and no-product publish dry-runs must run release-check and passthrough registry checks without launching release.py") + fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product publish dry-runs must run release-check and passthrough registry checks without launching release.py, and low-risk SDK product dry-runs must stay in Bun") 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 diff --git a/tools/release/release-cli-utils.mjs b/tools/release/release-cli-utils.mjs index eab9a223..3f92ae6b 100644 --- a/tools/release/release-cli-utils.mjs +++ b/tools/release/release-cli-utils.mjs @@ -8,10 +8,10 @@ export function fail(tool, message, exitCode = 1) { process.exit(exitCode); } -export function run(tool, args, { failExitCode = 1 } = {}) { +export function run(tool, args, { failExitCode = 1, cwd = ROOT } = {}) { console.log(`\n==> ${args.join(" ")}`); const result = spawnSync(args[0], args.slice(1), { - cwd: ROOT, + cwd, stdio: "inherit", }); if (result.error) { diff --git a/tools/release/release-publish.mjs b/tools/release/release-publish.mjs index 0d587994..2f13fe7d 100755 --- a/tools/release/release-publish.mjs +++ b/tools/release/release-publish.mjs @@ -2,6 +2,10 @@ import { spawnSync } from "node:child_process"; import { ROOT, run } from "./release-cli-utils.mjs"; +import { + SUPPORTED_SDK_PRODUCT_DRY_RUNS, + runSdkProductDryRun, +} from "./release-sdk-product-dry-run.mjs"; const TOOL = "release-publish.mjs"; const COMMANDS = new Set(["publish", "publish-dry-run"]); @@ -10,9 +14,10 @@ function usage() { console.log(`usage: tools/release/release-publish.mjs [publish args] Runs protected release publish and publish dry-run operations through the Bun -release command surface. The public no-product publish dry-run is handled in -Bun; product, WASIX, and publish dispatch still delegate to release.py while the -protected implementation is ported. +release command surface. The public no-product publish dry-run and selected +low-risk SDK product dry-runs are handled in Bun; other product dry-runs, WASIX, +and protected publish dispatch still delegate to release.py while the protected +implementation is ported. `); } @@ -42,6 +47,22 @@ function selectsProducts(args) { return args.some((arg) => arg === "--products-json" || arg.startsWith("--products-json=")); } +function flagValue(args, flag) { + for (let index = 0; index < args.length; index += 1) { + const value = args[index]; + if (value === flag) { + if (index + 1 >= args.length) { + fail(`${flag} requires a value`); + } + return args[index + 1]; + } + if (value.startsWith(`${flag}=`)) { + return value.slice(flag.length + 1); + } + } + return null; +} + function noProductPublishDryRunPassthrough(args) { if (args.includes("--wasm") || selectsProducts(args)) { return null; @@ -49,6 +70,60 @@ function noProductPublishDryRunPassthrough(args) { return args.filter((arg) => arg !== "--allow-dirty"); } +function jsonOutput(args) { + const result = spawnSync("tools/dev/bun.sh", args, { + cwd: ROOT, + encoding: "utf8", + }); + if (result.status !== 0 || result.error !== undefined) { + return null; + } + try { + return JSON.parse(result.stdout); + } catch { + return null; + } +} + +function productPublishDryRunPlan(args) { + if (args.includes("--wasm")) { + return null; + } + const productsJson = flagValue(args, "--products-json"); + if (productsJson === null) { + return null; + } + let requested; + try { + requested = JSON.parse(productsJson); + } catch { + return null; + } + if (!Array.isArray(requested) || requested.length === 0 || !requested.every((item) => typeof item === "string")) { + return null; + } + if (!requested.every((product) => SUPPORTED_SDK_PRODUCT_DRY_RUNS.has(product))) { + return null; + } + const ordered = jsonOutput([ + "tools/release/release_graph_query.mjs", + "release-order", + "--products-json", + JSON.stringify(requested), + ]); + if (!Array.isArray(ordered) || ordered.length === 0 || !ordered.every((item) => typeof item === "string")) { + return null; + } + if (!ordered.every((product) => SUPPORTED_SDK_PRODUCT_DRY_RUNS.has(product))) { + return null; + } + return { + allowDirty: args.includes("--allow-dirty"), + passthrough: args.filter((arg) => arg !== "--allow-dirty"), + products: ordered, + }; +} + if (isNoProductPublishDryRun(command, argv.slice(1))) { const passthrough = noProductPublishDryRunPassthrough(argv.slice(1)); run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check.mjs"]); @@ -58,6 +133,16 @@ if (isNoProductPublishDryRun(command, argv.slice(1))) { process.exit(0); } +const productDryRunPlan = command === "publish-dry-run" ? productPublishDryRunPlan(argv.slice(1)) : null; +if (productDryRunPlan !== null) { + run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check.mjs"]); + run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check-registries.mjs", ...productDryRunPlan.passthrough]); + for (const product of productDryRunPlan.products) { + runSdkProductDryRun(product, { allowDirty: productDryRunPlan.allowDirty }); + } + process.exit(0); +} + const result = spawnSync("tools/release/release.py", argv, { cwd: ROOT, stdio: "inherit", diff --git a/tools/release/release-sdk-product-dry-run.mjs b/tools/release/release-sdk-product-dry-run.mjs new file mode 100644 index 00000000..5c2f3940 --- /dev/null +++ b/tools/release/release-sdk-product-dry-run.mjs @@ -0,0 +1,140 @@ +#!/usr/bin/env bun +import { readdirSync, statSync } from "node:fs"; +import path from "node:path"; + +import { ROOT, run } from "./release-cli-utils.mjs"; + +const TOOL = "release-sdk-product-dry-run.mjs"; + +export const SUPPORTED_SDK_PRODUCT_DRY_RUNS = new Set(["oliphaunt-js", "oliphaunt-react-native"]); + +function fail(message, exitCode = 1) { + console.error(`${TOOL}: ${message}`); + process.exit(exitCode); +} + +function usage() { + console.log(`usage: tools/release/release-sdk-product-dry-run.mjs --product PRODUCT [--allow-dirty] + +Runs Bun-owned low-risk SDK product publish dry-run checks. Release-wide checks +and registry dependency checks are owned by release-publish.mjs before this +helper is invoked from the public publish dry-run command surface. +`); +} + +function isDirectory(file) { + try { + return statSync(file).isDirectory(); + } catch { + return false; + } +} + +function requireFile(file, message) { + try { + if (statSync(file).isFile()) { + return; + } + } catch { + // handled below + } + fail(message); +} + +function requireDirectory(file, message) { + if (!isDirectory(file)) { + fail(message); + } +} + +function sdkArtifactDir(product) { + return path.join(ROOT, "target", "sdk-artifacts", product); +} + +function requireStagedSdkArtifact(product, description, suffixes) { + const directory = sdkArtifactDir(product); + requireDirectory( + directory, + `${product} requires staged ${description} artifact(s) under target/sdk-artifacts/${product}; download the CI workflow SDK package artifacts before release validation or publishing`, + ); + const matches = readdirSync(directory) + .filter((name) => name !== "artifacts.txt" && suffixes.some((suffix) => name.endsWith(suffix))) + .sort(); + if (matches.length === 0) { + fail( + `${product} requires staged ${description} artifact(s) under target/sdk-artifacts/${product}; download the CI workflow SDK package artifacts before release validation or publishing`, + ); + } + return matches; +} + +function stagedJsrSourceDir(product) { + const directory = path.join(sdkArtifactDir(product), "jsr-source"); + requireDirectory( + directory, + `${product} requires staged JSR source under target/sdk-artifacts/${product}/jsr-source; download the CI workflow SDK package artifacts before release validation or publishing`, + ); + for (const name of ["jsr.json", "package.json", "src"]) { + const candidate = path.join(directory, name); + if (name === "src") { + requireDirectory(candidate, `${product} staged JSR source is missing: ${name}`); + } else { + requireFile(candidate, `${product} staged JSR source is missing: ${name}`); + } + } + return directory; +} + +export function runSdkProductDryRun(product, { allowDirty = false } = {}) { + if (!SUPPORTED_SDK_PRODUCT_DRY_RUNS.has(product)) { + fail(`no Bun publish dry-run handler for ${product}`, 2); + } + run(TOOL, ["tools/dev/bun.sh", "tools/release/check-staged-artifacts.mjs", "--require-sdk-product", product]); + if (product === "oliphaunt-react-native") { + requireStagedSdkArtifact(product, "npm package", [".tgz"]); + return; + } + if (product === "oliphaunt-js") { + requireStagedSdkArtifact(product, "npm package", [".tgz"]); + const command = ["pnpm", "exec", "jsr", "publish", "--dry-run"]; + if (allowDirty) { + command.push("--allow-dirty"); + } + run(TOOL, command, { cwd: stagedJsrSourceDir(product) }); + } +} + +function parseArgs(argv) { + const args = { + allowDirty: false, + product: null, + }; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === "--product") { + if (index + 1 >= argv.length) { + fail("--product requires a value", 2); + } + args.product = argv[index + 1]; + index += 1; + } else if (arg.startsWith("--product=")) { + args.product = arg.slice("--product=".length); + } else if (arg === "--allow-dirty") { + args.allowDirty = true; + } else if (arg === "--help" || arg === "-h") { + usage(); + process.exit(0); + } else { + fail(`unknown argument ${arg}`, 2); + } + } + if (!args.product) { + fail("--product is required", 2); + } + return args; +} + +if (import.meta.main) { + const args = parseArgs(Bun.argv.slice(2)); + runSdkProductDryRun(args.product, { allowDirty: args.allowDirty }); +} From 82ddb5dd4baa8572ca9c20c7c9ef5ee7ebe03955 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 05:26:28 +0000 Subject: [PATCH 268/308] chore: extend sdk dry runs to swift kotlin --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 19 +-- tools/policy/check-tooling-stack.sh | 12 +- tools/release/check_release_metadata.py | 7 +- tools/release/release-sdk-product-dry-run.mjs | 119 ++++++++++++++++-- 4 files changed, 138 insertions(+), 19 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 267a1e8a..a45d0b20 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -82,16 +82,19 @@ until the current-state gates here are checked with fresh local evidence. `tools/release/release-sdk-product-dry-run.mjs` and routed `release-publish.mjs publish-dry-run --products-json ...` through it when the selected products are entirely in the low-risk SDK set currently owned in Bun: - `oliphaunt-js` and `oliphaunt-react-native`. The release wrapper still runs - the standard release and registry dependency gates first, so product-selected - dry-runs keep the existing dependency-tag semantics. Fresh evidence: + `oliphaunt-js`, `oliphaunt-kotlin`, `oliphaunt-react-native`, and + `oliphaunt-swift`. The release wrapper still runs the standard release and + registry dependency gates first, so product-selected dry-runs keep the + existing dependency-tag semantics. Fresh evidence: `tools/dev/bun.sh tools/release/release-sdk-product-dry-run.mjs --product oliphaunt-js --allow-dirty` passed against staged npm and JSR artifacts; the same helper for - `oliphaunt-react-native` failed at the expected missing staged SDK artifact in - this checkout. `release-publish.mjs publish-dry-run --products-json - '["oliphaunt-js"]' --head-ref HEAD` still stops at the existing registry - dependency gate because `liboliphaunt-native-v0.1.0` is not tagged and - `liboliphaunt-native` is not selected. Guards passed through + `oliphaunt-swift`, `oliphaunt-kotlin`, and `oliphaunt-react-native` failed at + the expected missing staged SDK artifact in this checkout. Swift and Kotlin + success paths now preserve the Python dry-run's staged SwiftPM release-tree + and Kotlin Maven repository checks. `release-publish.mjs publish-dry-run + --products-json '["oliphaunt-js"]' --head-ref HEAD` still stops at the + existing registry dependency gate because `liboliphaunt-native-v0.1.0` is not + tagged and `liboliphaunt-native` is not selected. Guards passed through `tools/dev/bun.sh tools/release/check-release-metadata.mjs`, `bash tools/policy/check-tooling-stack.sh`, and `git diff --check`. - 2026-06-28: Expanded the Bun `tools/release/release-publish.mjs` diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index fbe369e6..1f949898 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -429,10 +429,20 @@ grep -Fq 'SUPPORTED_SDK_PRODUCT_DRY_RUNS' tools/release/release-publish.mjs || fail "release publish dry-run wrapper must import the Bun SDK product dry-run support set" grep -Fq 'runSdkProductDryRun(product, { allowDirty: productDryRunPlan.allowDirty });' tools/release/release-publish.mjs || fail "release publish dry-run wrapper must execute supported SDK product dry-runs in Bun" -grep -Fq 'export const SUPPORTED_SDK_PRODUCT_DRY_RUNS = new Set(["oliphaunt-js", "oliphaunt-react-native"]);' tools/release/release-sdk-product-dry-run.mjs || +grep -Fq '"oliphaunt-swift",' tools/release/release-sdk-product-dry-run.mjs || + fail "release SDK product dry-run helper must include Swift in Bun-owned low-risk SDK product dry-runs" +grep -Fq '"oliphaunt-kotlin",' tools/release/release-sdk-product-dry-run.mjs || + fail "release SDK product dry-run helper must include Kotlin in Bun-owned low-risk SDK product dry-runs" +grep -Fq '"oliphaunt-react-native",' tools/release/release-sdk-product-dry-run.mjs || + fail "release SDK product dry-run helper must include React Native in Bun-owned low-risk SDK product dry-runs" +grep -Fq '"oliphaunt-js",' tools/release/release-sdk-product-dry-run.mjs || fail "release SDK product dry-run helper must declare Bun-owned low-risk SDK product dry-runs" grep -Fq 'tools/release/check-staged-artifacts.mjs", "--require-sdk-product", product' tools/release/release-sdk-product-dry-run.mjs || fail "Bun product dry-runs must validate staged SDK artifacts through the Bun checker" +grep -Fq 'prepareStagedSwiftReleaseManifest' tools/release/release-sdk-product-dry-run.mjs || + fail "Bun SDK product dry-runs must preserve Swift staged release manifest validation" +grep -Fq 'stagedKotlinMavenRepo' tools/release/release-sdk-product-dry-run.mjs || + fail "Bun SDK product dry-runs must preserve Kotlin staged Maven repository validation" grep -Fq 'tools/dev/bun.sh tools/release/release-publish.mjs publish-dry-run' .github/workflows/release.yml || fail "release workflow publish dry-runs must use the Bun release-publish entrypoint" grep -Fq 'tools/dev/bun.sh tools/release/release-publish.mjs publish ' .github/workflows/release.yml || diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index fafeb9e5..86d4b3fb 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1003,8 +1003,13 @@ def validate_publish_target_coverage() -> None: or 'run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check-registries.mjs", ...passthrough]);' not in release_publish or "SUPPORTED_SDK_PRODUCT_DRY_RUNS" not in release_publish or 'runSdkProductDryRun(product, { allowDirty: productDryRunPlan.allowDirty });' not in release_publish - or 'export const SUPPORTED_SDK_PRODUCT_DRY_RUNS = new Set(["oliphaunt-js", "oliphaunt-react-native"]);' not in release_sdk_product_dry_run + or '"oliphaunt-js",' not in release_sdk_product_dry_run + or '"oliphaunt-kotlin",' not in release_sdk_product_dry_run + or '"oliphaunt-react-native",' not in release_sdk_product_dry_run + or '"oliphaunt-swift",' not in release_sdk_product_dry_run or 'tools/release/check-staged-artifacts.mjs", "--require-sdk-product", product' not in release_sdk_product_dry_run + or "prepareStagedSwiftReleaseManifest" not in release_sdk_product_dry_run + or "stagedKotlinMavenRepo" not in release_sdk_product_dry_run or 'spawnSync("tools/release/release.py", argv' not in release_publish ): fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product publish dry-runs must run release-check and passthrough registry checks without launching release.py, and low-risk SDK product dry-runs must stay in Bun") diff --git a/tools/release/release-sdk-product-dry-run.mjs b/tools/release/release-sdk-product-dry-run.mjs index 5c2f3940..8e9577f9 100644 --- a/tools/release/release-sdk-product-dry-run.mjs +++ b/tools/release/release-sdk-product-dry-run.mjs @@ -1,12 +1,18 @@ #!/usr/bin/env bun -import { readdirSync, statSync } from "node:fs"; +import { cpSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs"; import path from "node:path"; import { ROOT, run } from "./release-cli-utils.mjs"; +import { currentProductVersionSync } from "./release-artifact-targets.mjs"; const TOOL = "release-sdk-product-dry-run.mjs"; -export const SUPPORTED_SDK_PRODUCT_DRY_RUNS = new Set(["oliphaunt-js", "oliphaunt-react-native"]); +export const SUPPORTED_SDK_PRODUCT_DRY_RUNS = new Set([ + "oliphaunt-js", + "oliphaunt-kotlin", + "oliphaunt-react-native", + "oliphaunt-swift", +]); function fail(message, exitCode = 1) { console.error(`${TOOL}: ${message}`); @@ -30,15 +36,18 @@ function isDirectory(file) { } } -function requireFile(file, message) { +function isFile(file) { try { - if (statSync(file).isFile()) { - return; - } + return statSync(file).isFile(); } catch { - // handled below + return false; + } +} + +function requireFile(file, message) { + if (!isFile(file)) { + fail(message); } - fail(message); } function requireDirectory(file, message) { @@ -51,6 +60,10 @@ function sdkArtifactDir(product) { return path.join(ROOT, "target", "sdk-artifacts", product); } +function rel(file) { + return path.relative(ROOT, file).split(path.sep).join("/"); +} + function requireStagedSdkArtifact(product, description, suffixes) { const directory = sdkArtifactDir(product); requireDirectory( @@ -58,7 +71,8 @@ function requireStagedSdkArtifact(product, description, suffixes) { `${product} requires staged ${description} artifact(s) under target/sdk-artifacts/${product}; download the CI workflow SDK package artifacts before release validation or publishing`, ); const matches = readdirSync(directory) - .filter((name) => name !== "artifacts.txt" && suffixes.some((suffix) => name.endsWith(suffix))) + .map((name) => path.join(directory, name)) + .filter((file) => isFile(file) && path.basename(file) !== "artifacts.txt" && suffixes.some((suffix) => file.endsWith(suffix))) .sort(); if (matches.length === 0) { fail( @@ -85,11 +99,98 @@ function stagedJsrSourceDir(product) { return directory; } +function stagedSwiftReleaseArtifacts() { + const matches = requireStagedSdkArtifact("oliphaunt-swift", "Swift package", [".zip", ".release"]); + const sourceArchives = matches.filter((file) => path.basename(file) === "Oliphaunt-source.zip"); + const manifests = matches.filter((file) => path.basename(file) === "Package.swift.release"); + const releaseTree = path.join(sdkArtifactDir("oliphaunt-swift"), "release-tree"); + if (sourceArchives.length !== 1 || manifests.length !== 1) { + fail( + "oliphaunt-swift release requires exactly one staged Oliphaunt-source.zip and one staged Package.swift.release under target/sdk-artifacts/oliphaunt-swift", + ); + } + requireFile( + path.join(releaseTree, "generated", "swiftpm", "OliphauntICU", "OliphauntICU.swift"), + "oliphaunt-swift release requires staged SwiftPM release-tree files, including generated/swiftpm/OliphauntICU/OliphauntICU.swift", + ); + const manifestText = readFileSync(manifests[0], "utf8"); + for (const fragment of ["binaryTarget(", "liboliphaunt-native-v", "liboliphaunt-", "apple-spm-xcframework.zip", "checksum:"]) { + if (!manifestText.includes(fragment)) { + fail(`oliphaunt-swift staged Package.swift.release is missing ${JSON.stringify(fragment)}`); + } + } + return { manifest: manifests[0], releaseTree }; +} + +function prepareStagedSwiftReleaseManifest() { + const { manifest, releaseTree: stagedReleaseTree } = stagedSwiftReleaseArtifacts(); + const outputDir = path.join(ROOT, "target", "oliphaunt-swift"); + const releaseTree = path.join(outputDir, "release-tree"); + rmSync(releaseTree, { force: true, recursive: true }); + mkdirSync(outputDir, { recursive: true }); + cpSync(stagedReleaseTree, releaseTree, { recursive: true }); + cpSync(manifest, path.join(outputDir, "Package.swift.release")); +} + +function walkFiles(root) { + const files = []; + for (const entry of readdirSync(root, { withFileTypes: true })) { + const child = path.join(root, entry.name); + if (entry.isDirectory()) { + files.push(...walkFiles(child)); + } else if (entry.isFile()) { + files.push(child); + } + } + return files; +} + +function stagedKotlinMavenRepo() { + const root = path.join(sdkArtifactDir("oliphaunt-kotlin"), "maven"); + requireDirectory( + root, + "oliphaunt-kotlin requires staged Maven repository artifacts under target/sdk-artifacts/oliphaunt-kotlin/maven; download the CI workflow Kotlin SDK package artifacts before release validation or publishing", + ); + const version = currentProductVersionSync("oliphaunt-kotlin", TOOL); + const required = [ + `dev/oliphaunt/oliphaunt-android/${version}/oliphaunt-android-${version}.aar`, + `dev/oliphaunt/oliphaunt-android/${version}/oliphaunt-android-${version}.pom`, + `dev/oliphaunt/oliphaunt-android/${version}/oliphaunt-android-${version}.module`, + `dev/oliphaunt/oliphaunt-android-gradle-plugin/${version}/oliphaunt-android-gradle-plugin-${version}.jar`, + `dev/oliphaunt/oliphaunt-android-gradle-plugin/${version}/oliphaunt-android-gradle-plugin-${version}.pom`, + `dev/oliphaunt/oliphaunt-android-gradle-plugin/${version}/oliphaunt-android-gradle-plugin-${version}.module`, + `dev/oliphaunt/android/dev.oliphaunt.android.gradle.plugin/${version}/dev.oliphaunt.android.gradle.plugin-${version}.pom`, + ]; + const missing = required.filter((file) => !isFile(path.join(root, file))); + if (missing.length > 0) { + fail(`oliphaunt-kotlin staged Maven repository is missing: ${missing.map((file) => `target/sdk-artifacts/oliphaunt-kotlin/maven/${file}`).join(", ")}`); + } + for (const file of walkFiles(root)) { + const relative = path.relative(root, file).split(path.sep); + if (relative[0] !== "dev" || relative[1] !== "oliphaunt") { + fail(`oliphaunt-kotlin staged Maven repository contains unexpected path ${rel(file)}`); + } + const suffix = path.extname(file); + if (suffix === ".lastUpdated" || suffix === ".lock") { + fail(`oliphaunt-kotlin staged Maven repository contains local resolver state ${rel(file)}`); + } + } + console.log(`validated staged Kotlin Maven repository: ${rel(root)}`); +} + export function runSdkProductDryRun(product, { allowDirty = false } = {}) { if (!SUPPORTED_SDK_PRODUCT_DRY_RUNS.has(product)) { fail(`no Bun publish dry-run handler for ${product}`, 2); } run(TOOL, ["tools/dev/bun.sh", "tools/release/check-staged-artifacts.mjs", "--require-sdk-product", product]); + if (product === "oliphaunt-swift") { + prepareStagedSwiftReleaseManifest(); + return; + } + if (product === "oliphaunt-kotlin") { + stagedKotlinMavenRepo(); + return; + } if (product === "oliphaunt-react-native") { requireStagedSdkArtifact(product, "npm package", [".tgz"]); return; From 678f3f4ec5de51f8cf432770477ca9a404a7e5a4 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 05:30:06 +0000 Subject: [PATCH 269/308] chore: move rust sdk dry run into bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 12 +++-- tools/policy/check-tooling-stack.sh | 6 +++ tools/release/check_release_metadata.py | 3 ++ tools/release/release-sdk-product-dry-run.mjs | 51 ++++++++++++++++++- 4 files changed, 67 insertions(+), 5 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index a45d0b20..2c48fd4a 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -82,10 +82,14 @@ until the current-state gates here are checked with fresh local evidence. `tools/release/release-sdk-product-dry-run.mjs` and routed `release-publish.mjs publish-dry-run --products-json ...` through it when the selected products are entirely in the low-risk SDK set currently owned in Bun: - `oliphaunt-js`, `oliphaunt-kotlin`, `oliphaunt-react-native`, and - `oliphaunt-swift`. The release wrapper still runs the standard release and - registry dependency gates first, so product-selected dry-runs keep the - existing dependency-tag semantics. Fresh evidence: + `oliphaunt-js`, `oliphaunt-kotlin`, `oliphaunt-react-native`, + `oliphaunt-rust`, and `oliphaunt-swift`. The release wrapper still runs the + standard release and registry dependency gates first, so product-selected + dry-runs keep the existing dependency-tag semantics. Fresh evidence: + `tools/dev/bun.sh tools/release/release-sdk-product-dry-run.mjs --product oliphaunt-rust --allow-dirty` + passed against staged `oliphaunt` and `oliphaunt-build` Cargo package + artifacts and rendered `target/release/cargo-package-sources/oliphaunt/Cargo.toml` + through `tools/release/prepare-rust-release-source.mjs`; `tools/dev/bun.sh tools/release/release-sdk-product-dry-run.mjs --product oliphaunt-js --allow-dirty` passed against staged npm and JSR artifacts; the same helper for `oliphaunt-swift`, `oliphaunt-kotlin`, and `oliphaunt-react-native` failed at diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 1f949898..132325e7 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -435,6 +435,8 @@ grep -Fq '"oliphaunt-kotlin",' tools/release/release-sdk-product-dry-run.mjs || fail "release SDK product dry-run helper must include Kotlin in Bun-owned low-risk SDK product dry-runs" grep -Fq '"oliphaunt-react-native",' tools/release/release-sdk-product-dry-run.mjs || fail "release SDK product dry-run helper must include React Native in Bun-owned low-risk SDK product dry-runs" +grep -Fq '"oliphaunt-rust",' tools/release/release-sdk-product-dry-run.mjs || + fail "release SDK product dry-run helper must include Rust in Bun-owned low-risk SDK product dry-runs" grep -Fq '"oliphaunt-js",' tools/release/release-sdk-product-dry-run.mjs || fail "release SDK product dry-run helper must declare Bun-owned low-risk SDK product dry-runs" grep -Fq 'tools/release/check-staged-artifacts.mjs", "--require-sdk-product", product' tools/release/release-sdk-product-dry-run.mjs || @@ -443,6 +445,10 @@ grep -Fq 'prepareStagedSwiftReleaseManifest' tools/release/release-sdk-product-d fail "Bun SDK product dry-runs must preserve Swift staged release manifest validation" grep -Fq 'stagedKotlinMavenRepo' tools/release/release-sdk-product-dry-run.mjs || fail "Bun SDK product dry-runs must preserve Kotlin staged Maven repository validation" +grep -Fq 'verifyStagedCargoProductCrates("oliphaunt-rust")' tools/release/release-sdk-product-dry-run.mjs || + fail "Bun SDK product dry-runs must preserve Rust staged Cargo crate validation" +grep -Fq 'tools/release/prepare-rust-release-source.mjs' tools/release/release-sdk-product-dry-run.mjs || + fail "Bun SDK product dry-runs must render the Rust publish source through the Bun helper" grep -Fq 'tools/dev/bun.sh tools/release/release-publish.mjs publish-dry-run' .github/workflows/release.yml || fail "release workflow publish dry-runs must use the Bun release-publish entrypoint" grep -Fq 'tools/dev/bun.sh tools/release/release-publish.mjs publish ' .github/workflows/release.yml || diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 86d4b3fb..c8e4428d 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1006,10 +1006,13 @@ def validate_publish_target_coverage() -> None: or '"oliphaunt-js",' not in release_sdk_product_dry_run or '"oliphaunt-kotlin",' not in release_sdk_product_dry_run or '"oliphaunt-react-native",' not in release_sdk_product_dry_run + or '"oliphaunt-rust",' not in release_sdk_product_dry_run or '"oliphaunt-swift",' not in release_sdk_product_dry_run or 'tools/release/check-staged-artifacts.mjs", "--require-sdk-product", product' not in release_sdk_product_dry_run or "prepareStagedSwiftReleaseManifest" not in release_sdk_product_dry_run or "stagedKotlinMavenRepo" not in release_sdk_product_dry_run + or 'verifyStagedCargoProductCrates("oliphaunt-rust")' not in release_sdk_product_dry_run + or "tools/release/prepare-rust-release-source.mjs" not in release_sdk_product_dry_run or 'spawnSync("tools/release/release.py", argv' not in release_publish ): fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product publish dry-runs must run release-check and passthrough registry checks without launching release.py, and low-risk SDK product dry-runs must stay in Bun") diff --git a/tools/release/release-sdk-product-dry-run.mjs b/tools/release/release-sdk-product-dry-run.mjs index 8e9577f9..6de7716e 100644 --- a/tools/release/release-sdk-product-dry-run.mjs +++ b/tools/release/release-sdk-product-dry-run.mjs @@ -3,7 +3,7 @@ import { cpSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from " import path from "node:path"; import { ROOT, run } from "./release-cli-utils.mjs"; -import { currentProductVersionSync } from "./release-artifact-targets.mjs"; +import { currentProductVersionSync, registryPackageRows } from "./release-artifact-targets.mjs"; const TOOL = "release-sdk-product-dry-run.mjs"; @@ -11,6 +11,7 @@ export const SUPPORTED_SDK_PRODUCT_DRY_RUNS = new Set([ "oliphaunt-js", "oliphaunt-kotlin", "oliphaunt-react-native", + "oliphaunt-rust", "oliphaunt-swift", ]); @@ -178,6 +179,50 @@ function stagedKotlinMavenRepo() { console.log(`validated staged Kotlin Maven repository: ${rel(root)}`); } +function stagedCargoCrates(product) { + const matches = requireStagedSdkArtifact(product, "Cargo package", [".crate"]); + const names = matches.map((file) => path.basename(file)); + if (names.length !== new Set(names).size) { + fail(`${product} staged Cargo artifacts contain duplicate crate filenames: ${names.join(", ")}`); + } + return matches; +} + +function cratesioProductCrates(product) { + const crates = registryPackageRows({ product, packageKind: "crates" }, TOOL) + .map((row) => row.packageName) + .sort(); + if (crates.length === 0) { + fail(`${product} declares no crates.io packages`); + } + return crates; +} + +function verifyStagedCargoProductCrates(product) { + const version = currentProductVersionSync(product, TOOL); + const stagedNames = stagedCargoCrates(product).map((file) => path.basename(file)).sort(); + const expectedNames = cratesioProductCrates(product) + .map((crate) => `${crate}-${version}.crate`) + .sort(); + for (const expectedName of expectedNames) { + if (!stagedNames.includes(expectedName)) { + fail( + `${product} staged Cargo artifacts must contain exactly one ${expectedName}; staged=${JSON.stringify(stagedNames)}`, + ); + } + console.log(`validated staged Cargo crate identity: ${product} -> target/sdk-artifacts/${product}/${expectedName}`); + } + if (JSON.stringify(stagedNames) !== JSON.stringify(expectedNames)) { + fail(`${product} staged Cargo artifacts mismatch: expected=${JSON.stringify(expectedNames)}, staged=${JSON.stringify(stagedNames)}`); + } +} + +function runRustSdkDryRun() { + verifyStagedCargoProductCrates("oliphaunt-rust"); + run(TOOL, ["tools/dev/bun.sh", "tools/release/prepare-rust-release-source.mjs"]); + console.log("validated staged Rust SDK crates; skipping source cargo publish dry-run."); +} + export function runSdkProductDryRun(product, { allowDirty = false } = {}) { if (!SUPPORTED_SDK_PRODUCT_DRY_RUNS.has(product)) { fail(`no Bun publish dry-run handler for ${product}`, 2); @@ -191,6 +236,10 @@ export function runSdkProductDryRun(product, { allowDirty = false } = {}) { stagedKotlinMavenRepo(); return; } + if (product === "oliphaunt-rust") { + runRustSdkDryRun(); + return; + } if (product === "oliphaunt-react-native") { requireStagedSdkArtifact(product, "npm package", [".tgz"]); return; From 2b3957453358fd3a91bd5e54196d2091a3e6f476 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 05:34:32 +0000 Subject: [PATCH 270/308] chore: move wasix rust dry run into bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 11 ++++++--- tools/policy/check-tooling-stack.sh | 6 ++++- tools/release/check_release_metadata.py | 4 +++- tools/release/release-publish.mjs | 8 +++---- tools/release/release-sdk-product-dry-run.mjs | 23 +++++++++++++++++-- 5 files changed, 41 insertions(+), 11 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 2c48fd4a..a0cfde4e 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -83,13 +83,18 @@ until the current-state gates here are checked with fresh local evidence. `release-publish.mjs publish-dry-run --products-json ...` through it when the selected products are entirely in the low-risk SDK set currently owned in Bun: `oliphaunt-js`, `oliphaunt-kotlin`, `oliphaunt-react-native`, - `oliphaunt-rust`, and `oliphaunt-swift`. The release wrapper still runs the - standard release and registry dependency gates first, so product-selected - dry-runs keep the existing dependency-tag semantics. Fresh evidence: + `oliphaunt-rust`, `oliphaunt-wasix-rust`, and `oliphaunt-swift`. The release + wrapper still runs the standard release and registry dependency gates first, + so product-selected dry-runs keep the existing dependency-tag semantics. + Fresh evidence: `tools/dev/bun.sh tools/release/release-sdk-product-dry-run.mjs --product oliphaunt-rust --allow-dirty` passed against staged `oliphaunt` and `oliphaunt-build` Cargo package artifacts and rendered `target/release/cargo-package-sources/oliphaunt/Cargo.toml` through `tools/release/prepare-rust-release-source.mjs`; + `tools/dev/bun.sh tools/release/release-sdk-product-dry-run.mjs --product oliphaunt-wasix-rust --allow-dirty` + passed against the staged `oliphaunt-wasix` Cargo package artifact and + rendered `target/release/cargo-package-sources/oliphaunt-wasix/Cargo.toml` + through the existing Bun WASIX SDK packager exports; `tools/dev/bun.sh tools/release/release-sdk-product-dry-run.mjs --product oliphaunt-js --allow-dirty` passed against staged npm and JSR artifacts; the same helper for `oliphaunt-swift`, `oliphaunt-kotlin`, and `oliphaunt-react-native` failed at diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 132325e7..3f7b1a9f 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -427,7 +427,7 @@ grep -Fq 'run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check-registries fail "release publish dry-run wrapper must keep no-product passthrough registry checks in Bun" grep -Fq 'SUPPORTED_SDK_PRODUCT_DRY_RUNS' tools/release/release-publish.mjs || fail "release publish dry-run wrapper must import the Bun SDK product dry-run support set" -grep -Fq 'runSdkProductDryRun(product, { allowDirty: productDryRunPlan.allowDirty });' tools/release/release-publish.mjs || +grep -Fq 'await runSdkProductDryRun(product, { allowDirty: productDryRunPlan.allowDirty });' tools/release/release-publish.mjs || fail "release publish dry-run wrapper must execute supported SDK product dry-runs in Bun" grep -Fq '"oliphaunt-swift",' tools/release/release-sdk-product-dry-run.mjs || fail "release SDK product dry-run helper must include Swift in Bun-owned low-risk SDK product dry-runs" @@ -437,6 +437,8 @@ grep -Fq '"oliphaunt-react-native",' tools/release/release-sdk-product-dry-run.m fail "release SDK product dry-run helper must include React Native in Bun-owned low-risk SDK product dry-runs" grep -Fq '"oliphaunt-rust",' tools/release/release-sdk-product-dry-run.mjs || fail "release SDK product dry-run helper must include Rust in Bun-owned low-risk SDK product dry-runs" +grep -Fq '"oliphaunt-wasix-rust",' tools/release/release-sdk-product-dry-run.mjs || + fail "release SDK product dry-run helper must include WASIX Rust in Bun-owned low-risk SDK product dry-runs" grep -Fq '"oliphaunt-js",' tools/release/release-sdk-product-dry-run.mjs || fail "release SDK product dry-run helper must declare Bun-owned low-risk SDK product dry-runs" grep -Fq 'tools/release/check-staged-artifacts.mjs", "--require-sdk-product", product' tools/release/release-sdk-product-dry-run.mjs || @@ -449,6 +451,8 @@ grep -Fq 'verifyStagedCargoProductCrates("oliphaunt-rust")' tools/release/releas fail "Bun SDK product dry-runs must preserve Rust staged Cargo crate validation" grep -Fq 'tools/release/prepare-rust-release-source.mjs' tools/release/release-sdk-product-dry-run.mjs || fail "Bun SDK product dry-runs must render the Rust publish source through the Bun helper" +grep -Fq 'prepareOliphauntWasixReleaseSource' tools/release/release-sdk-product-dry-run.mjs || + fail "Bun SDK product dry-runs must render the WASIX Rust publish source through the Bun helper" grep -Fq 'tools/dev/bun.sh tools/release/release-publish.mjs publish-dry-run' .github/workflows/release.yml || fail "release workflow publish dry-runs must use the Bun release-publish entrypoint" grep -Fq 'tools/dev/bun.sh tools/release/release-publish.mjs publish ' .github/workflows/release.yml || diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index c8e4428d..e85cafd7 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1002,17 +1002,19 @@ def validate_publish_target_coverage() -> None: or 'run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check.mjs"]);' not in release_publish or 'run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check-registries.mjs", ...passthrough]);' not in release_publish or "SUPPORTED_SDK_PRODUCT_DRY_RUNS" not in release_publish - or 'runSdkProductDryRun(product, { allowDirty: productDryRunPlan.allowDirty });' not in release_publish + or 'await runSdkProductDryRun(product, { allowDirty: productDryRunPlan.allowDirty });' not in release_publish or '"oliphaunt-js",' not in release_sdk_product_dry_run or '"oliphaunt-kotlin",' not in release_sdk_product_dry_run or '"oliphaunt-react-native",' not in release_sdk_product_dry_run or '"oliphaunt-rust",' not in release_sdk_product_dry_run + or '"oliphaunt-wasix-rust",' not in release_sdk_product_dry_run or '"oliphaunt-swift",' not in release_sdk_product_dry_run or 'tools/release/check-staged-artifacts.mjs", "--require-sdk-product", product' not in release_sdk_product_dry_run or "prepareStagedSwiftReleaseManifest" not in release_sdk_product_dry_run or "stagedKotlinMavenRepo" not in release_sdk_product_dry_run or 'verifyStagedCargoProductCrates("oliphaunt-rust")' not in release_sdk_product_dry_run or "tools/release/prepare-rust-release-source.mjs" not in release_sdk_product_dry_run + or "prepareOliphauntWasixReleaseSource" not in release_sdk_product_dry_run or 'spawnSync("tools/release/release.py", argv' not in release_publish ): fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product publish dry-runs must run release-check and passthrough registry checks without launching release.py, and low-risk SDK product dry-runs must stay in Bun") diff --git a/tools/release/release-publish.mjs b/tools/release/release-publish.mjs index 2f13fe7d..7a1332a6 100755 --- a/tools/release/release-publish.mjs +++ b/tools/release/release-publish.mjs @@ -15,9 +15,9 @@ function usage() { Runs protected release publish and publish dry-run operations through the Bun release command surface. The public no-product publish dry-run and selected -low-risk SDK product dry-runs are handled in Bun; other product dry-runs, WASIX, -and protected publish dispatch still delegate to release.py while the protected -implementation is ported. +low-risk SDK product dry-runs are handled in Bun; other product dry-runs, +legacy --wasm dry-runs, and protected publish dispatch still delegate to +release.py while the protected implementation is ported. `); } @@ -138,7 +138,7 @@ if (productDryRunPlan !== null) { run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check.mjs"]); run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check-registries.mjs", ...productDryRunPlan.passthrough]); for (const product of productDryRunPlan.products) { - runSdkProductDryRun(product, { allowDirty: productDryRunPlan.allowDirty }); + await runSdkProductDryRun(product, { allowDirty: productDryRunPlan.allowDirty }); } process.exit(0); } diff --git a/tools/release/release-sdk-product-dry-run.mjs b/tools/release/release-sdk-product-dry-run.mjs index 6de7716e..8bb51bff 100644 --- a/tools/release/release-sdk-product-dry-run.mjs +++ b/tools/release/release-sdk-product-dry-run.mjs @@ -4,6 +4,10 @@ import path from "node:path"; import { ROOT, run } from "./release-cli-utils.mjs"; import { currentProductVersionSync, registryPackageRows } from "./release-artifact-targets.mjs"; +import { + currentOliphauntWasixSdkVersion, + prepareOliphauntWasixReleaseSource, +} from "./package_oliphaunt_wasix_sdk_crate.mjs"; const TOOL = "release-sdk-product-dry-run.mjs"; @@ -12,6 +16,7 @@ export const SUPPORTED_SDK_PRODUCT_DRY_RUNS = new Set([ "oliphaunt-kotlin", "oliphaunt-react-native", "oliphaunt-rust", + "oliphaunt-wasix-rust", "oliphaunt-swift", ]); @@ -223,7 +228,17 @@ function runRustSdkDryRun() { console.log("validated staged Rust SDK crates; skipping source cargo publish dry-run."); } -export function runSdkProductDryRun(product, { allowDirty = false } = {}) { +async function runWasixRustSdkDryRun() { + verifyStagedCargoProductCrates("oliphaunt-wasix-rust"); + const version = await currentOliphauntWasixSdkVersion(); + const manifest = await prepareOliphauntWasixReleaseSource(version); + console.log(`validated generated WASIX Rust binding release source: ${rel(manifest)}`); + console.log( + "validated staged WASIX Rust binding package shape and generated publish manifest; source publish runs after WASIX artifact crates are published.", + ); +} + +export async function runSdkProductDryRun(product, { allowDirty = false } = {}) { if (!SUPPORTED_SDK_PRODUCT_DRY_RUNS.has(product)) { fail(`no Bun publish dry-run handler for ${product}`, 2); } @@ -240,6 +255,10 @@ export function runSdkProductDryRun(product, { allowDirty = false } = {}) { runRustSdkDryRun(); return; } + if (product === "oliphaunt-wasix-rust") { + await runWasixRustSdkDryRun(); + return; + } if (product === "oliphaunt-react-native") { requireStagedSdkArtifact(product, "npm package", [".tgz"]); return; @@ -286,5 +305,5 @@ function parseArgs(argv) { if (import.meta.main) { const args = parseArgs(Bun.argv.slice(2)); - runSdkProductDryRun(args.product, { allowDirty: args.allowDirty }); + await runSdkProductDryRun(args.product, { allowDirty: args.allowDirty }); } From 2ac04a3084383779bca2dfe77decd15e29cf92a8 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 05:55:34 +0000 Subject: [PATCH 271/308] chore: move node-direct dry run into bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 10 + tools/policy/check-tooling-stack.sh | 16 +- tools/release/check_artifact_targets.mjs | 7 +- tools/release/check_release_metadata.py | 17 +- tools/release/release-product-dry-run.mjs | 415 ++++++++++++++++++ tools/release/release-publish.mjs | 18 +- 6 files changed, 463 insertions(+), 20 deletions(-) create mode 100644 tools/release/release-product-dry-run.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index a0cfde4e..9a611fd0 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,16 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-28: Added a Bun product dry-run bridge + `tools/release/release-product-dry-run.mjs` and moved + `oliphaunt-node-direct` product dry-run dispatch out of `release.py` when + selected through `release-publish.mjs publish-dry-run --products-json ...`. + The Node direct path now runs package-shape checks, validates staged release + assets through `check-node-direct-release-assets.mjs`, rewrites the staged + checksum manifest, and validates the optional prebuilt npm tarballs against + the published target metadata in Bun. Policy guards now require + `SUPPORTED_BUN_PRODUCT_DRY_RUNS`, `runBunProductDryRun`, and the Node direct + release/npm tarball validation path. - 2026-06-28: Added the Bun SDK product dry-run helper `tools/release/release-sdk-product-dry-run.mjs` and routed `release-publish.mjs publish-dry-run --products-json ...` through it when the diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 3f7b1a9f..676be20d 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -425,10 +425,18 @@ grep -Fq 'run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check.mjs"]);' t fail "release publish dry-run wrapper must run release-check directly for no-product dry-runs" grep -Fq 'run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check-registries.mjs", ...passthrough]);' tools/release/release-publish.mjs || fail "release publish dry-run wrapper must keep no-product passthrough registry checks in Bun" -grep -Fq 'SUPPORTED_SDK_PRODUCT_DRY_RUNS' tools/release/release-publish.mjs || - fail "release publish dry-run wrapper must import the Bun SDK product dry-run support set" -grep -Fq 'await runSdkProductDryRun(product, { allowDirty: productDryRunPlan.allowDirty });' tools/release/release-publish.mjs || - fail "release publish dry-run wrapper must execute supported SDK product dry-runs in Bun" +grep -Fq 'SUPPORTED_BUN_PRODUCT_DRY_RUNS' tools/release/release-publish.mjs || + fail "release publish dry-run wrapper must import the Bun product dry-run support set" +grep -Fq 'await runBunProductDryRun(product, { allowDirty: productDryRunPlan.allowDirty });' tools/release/release-publish.mjs || + fail "release publish dry-run wrapper must execute supported product dry-runs in Bun" +grep -Fq 'SUPPORTED_SDK_PRODUCT_DRY_RUNS' tools/release/release-product-dry-run.mjs || + fail "release product dry-run bridge must preserve SDK helper ownership" +grep -Fq 'NODE_DIRECT_PRODUCT,' tools/release/release-product-dry-run.mjs || + fail "release product dry-run bridge must include Node direct in Bun-owned product dry-runs" +grep -Fq 'ensureNodeDirectReleaseAssets' tools/release/release-product-dry-run.mjs || + fail "Bun Node direct product dry-run must validate staged release assets" +grep -Fq 'nodeDirectOptionalNpmTarballs' tools/release/release-product-dry-run.mjs || + fail "Bun Node direct product dry-run must validate optional npm tarball artifacts" grep -Fq '"oliphaunt-swift",' tools/release/release-sdk-product-dry-run.mjs || fail "release SDK product dry-run helper must include Swift in Bun-owned low-risk SDK product dry-runs" grep -Fq '"oliphaunt-kotlin",' tools/release/release-sdk-product-dry-run.mjs || diff --git a/tools/release/check_artifact_targets.mjs b/tools/release/check_artifact_targets.mjs index c202688e..2a107d3d 100644 --- a/tools/release/check_artifact_targets.mjs +++ b/tools/release/check_artifact_targets.mjs @@ -710,7 +710,12 @@ function validateCiReleaseArtifacts() { requireText( "tools/release/release.py", "node_direct_optional_npm_tarballs", - "Node direct release publish must validate staged optional npm tarballs", + "Node direct protected npm publish must validate staged optional npm tarballs", + ); + requireText( + "tools/release/release-product-dry-run.mjs", + "nodeDirectOptionalNpmTarballs", + "Node direct product dry-run must validate staged optional npm tarballs in Bun", ); requireText( "tools/release/release.py", diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index e85cafd7..14c7c4eb 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -987,6 +987,7 @@ def validate_publish_target_coverage() -> None: workflow = read_text(".github/workflows/release.yml") release_source = read_text("tools/release/release.py") release_publish = read_text("tools/release/release-publish.mjs") + release_product_dry_run = read_text("tools/release/release-product-dry-run.mjs") release_sdk_product_dry_run = read_text("tools/release/release-sdk-product-dry-run.mjs") 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") @@ -1001,8 +1002,12 @@ def validate_publish_target_coverage() -> None: or 'function isNoProductPublishDryRun(' not in release_publish or 'run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check.mjs"]);' not in release_publish or 'run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check-registries.mjs", ...passthrough]);' not in release_publish - or "SUPPORTED_SDK_PRODUCT_DRY_RUNS" not in release_publish - or 'await runSdkProductDryRun(product, { allowDirty: productDryRunPlan.allowDirty });' not in release_publish + or "SUPPORTED_BUN_PRODUCT_DRY_RUNS" not in release_publish + or 'await runBunProductDryRun(product, { allowDirty: productDryRunPlan.allowDirty });' not in release_publish + or "SUPPORTED_SDK_PRODUCT_DRY_RUNS" not in release_product_dry_run + or "NODE_DIRECT_PRODUCT," not in release_product_dry_run + or "ensureNodeDirectReleaseAssets" not in release_product_dry_run + or "nodeDirectOptionalNpmTarballs" not in release_product_dry_run or '"oliphaunt-js",' not in release_sdk_product_dry_run or '"oliphaunt-kotlin",' not in release_sdk_product_dry_run or '"oliphaunt-react-native",' not in release_sdk_product_dry_run @@ -1017,7 +1022,7 @@ def validate_publish_target_coverage() -> None: or "prepareOliphauntWasixReleaseSource" not in release_sdk_product_dry_run or 'spawnSync("tools/release/release.py", argv' not in release_publish ): - fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product publish dry-runs must run release-check and passthrough registry checks without launching release.py, and low-risk SDK product dry-runs must stay in Bun") + fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product publish dry-runs must run release-check and passthrough registry checks without launching release.py, and low-risk product dry-runs must stay in Bun") 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 @@ -2037,9 +2042,9 @@ def validate_typescript( "shared MSVC CI setup must force Rust MSVC builds to use the MSVC linker under Git Bash", ) require_text( - "tools/release/release.py", - "node_direct_optional_npm_tarballs", - "Node direct release dry-run must validate staged optional npm tarballs from the builder job", + "tools/release/release-product-dry-run.mjs", + "nodeDirectOptionalNpmTarballs", + "Node direct release dry-run must validate staged optional npm tarballs from the builder job in Bun", ) require_text( "src/sdks/js/src/native/assets-deno.ts", diff --git a/tools/release/release-product-dry-run.mjs b/tools/release/release-product-dry-run.mjs new file mode 100644 index 00000000..bd5cd1d1 --- /dev/null +++ b/tools/release/release-product-dry-run.mjs @@ -0,0 +1,415 @@ +#!/usr/bin/env bun +import { createHash } from "node:crypto"; +import { + copyFileSync, + mkdirSync, + readdirSync, + readFileSync, + statSync, +} from "node:fs"; +import path from "node:path"; +import { gunzipSync } from "node:zlib"; + +import { ROOT, run } from "./release-cli-utils.mjs"; +import { + SUPPORTED_SDK_PRODUCT_DRY_RUNS, + runSdkProductDryRun, +} from "./release-sdk-product-dry-run.mjs"; +import { + artifactTargets, + compareText, + currentProductVersionSync, +} from "./release-artifact-targets.mjs"; + +const TOOL = "release-product-dry-run.mjs"; +const NODE_DIRECT_PRODUCT = "oliphaunt-node-direct"; +const NODE_DIRECT_KIND = "node-direct-addon"; +const NODE_DIRECT_PACKAGE_ROOT = path.join(ROOT, "src/runtimes/node-direct/packages"); + +export const SUPPORTED_BUN_PRODUCT_DRY_RUNS = new Set([ + ...SUPPORTED_SDK_PRODUCT_DRY_RUNS, + NODE_DIRECT_PRODUCT, +]); + +function fail(message, exitCode = 1) { + console.error(`${TOOL}: ${message}`); + process.exit(exitCode); +} + +function rel(file) { + return path.relative(ROOT, file).split(path.sep).join("/"); +} + +function isFile(file) { + try { + return statSync(file).isFile(); + } catch { + return false; + } +} + +function isDirectory(file) { + try { + return statSync(file).isDirectory(); + } catch { + return false; + } +} + +function sha256File(file) { + return createHash("sha256").update(readFileSync(file)).digest("hex"); +} + +function stagedRuntimeInputDirs(envName) { + const raw = process.env[envName] ?? process.env.OLIPHAUNT_RELEASE_ASSET_INPUT_DIRS ?? ""; + return raw + .split(path.delimiter) + .filter(Boolean) + .map((item) => { + const expanded = item === "~" || item.startsWith("~/") + ? path.join(process.env.HOME ?? "", item.slice(1)) + : item; + return path.isAbsolute(expanded) ? expanded : path.join(ROOT, expanded); + }); +} + +function globRegex(pattern) { + return new RegExp(`^${pattern.split("*").map(escapeRegExp).join(".*")}$`, "u"); +} + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); +} + +function copyStagedRuntimeAssets({ + product, + destination, + envName, + patterns, +}) { + const sourceDirs = stagedRuntimeInputDirs(envName); + if (sourceDirs.length === 0) { + fail( + `${product} requires staged runtime artifacts; set ${envName} or OLIPHAUNT_RELEASE_ASSET_INPUT_DIRS to the downloaded CI artifact directory`, + ); + } + mkdirSync(destination, { recursive: true }); + const regexes = patterns.map(globRegex); + let copied = 0; + for (const sourceDir of sourceDirs) { + if (!isDirectory(sourceDir)) { + fail(`${product} release asset input directory does not exist: ${sourceDir}`); + } + for (const name of readdirSync(sourceDir).sort(compareText)) { + if (!regexes.some((regex) => regex.test(name))) { + continue; + } + const source = path.join(sourceDir, name); + if (!isFile(source)) { + continue; + } + const output = path.join(destination, name); + if (isFile(output)) { + if (sha256File(output) !== sha256File(source)) { + fail(`${product} release asset input collision for ${name}: ${rel(output)} and ${rel(source)} have different bytes`); + } + continue; + } + copyFileSync(source, output); + copied += 1; + } + } + if (copied === 0) { + fail(`${product} found no staged runtime artifacts matching ${JSON.stringify(patterns)} under ${JSON.stringify(sourceDirs)}`); + } +} + +function hasNodeDirectReleaseArchive(assetDir) { + if (!isDirectory(assetDir)) { + return false; + } + return readdirSync(assetDir).some((name) => + name.startsWith("oliphaunt-node-direct-") && (name.endsWith(".tar.gz") || name.endsWith(".zip")), + ); +} + +function ensureNodeDirectReleaseAssets() { + const assetDir = path.join(ROOT, "target/oliphaunt-node-direct/release-assets"); + if (!hasNodeDirectReleaseArchive(assetDir)) { + copyStagedRuntimeAssets({ + product: NODE_DIRECT_PRODUCT, + destination: assetDir, + envName: "OLIPHAUNT_NODE_ADDON_ASSET_INPUT_DIRS", + patterns: ["oliphaunt-node-direct-*.tar.gz", "oliphaunt-node-direct-*.zip"], + }); + } + const version = currentProductVersionSync(NODE_DIRECT_PRODUCT, TOOL); + run(TOOL, [ + "tools/dev/bun.sh", + "tools/release/write_checksum_manifest.mjs", + "--asset-dir", + rel(assetDir), + "--output", + `oliphaunt-node-direct-${version}-release-assets.sha256`, + "--pattern", + "oliphaunt-node-direct-*.tar.gz", + "--pattern", + "oliphaunt-node-direct-*.zip", + ]); + run(TOOL, [ + "tools/dev/bun.sh", + "tools/release/check-node-direct-release-assets.mjs", + "--asset-dir", + rel(assetDir), + ]); +} + +function npmPackageDirsUnder(packageRoot) { + const packages = new Map(); + if (!isDirectory(packageRoot)) { + fail(`${rel(packageRoot)} does not contain npm package descriptors`); + } + for (const packageDirName of readdirSync(packageRoot).sort(compareText)) { + const packageDir = path.join(packageRoot, packageDirName); + const packageJsonPath = path.join(packageDir, "package.json"); + if (!isFile(packageJsonPath)) { + continue; + } + let packageJson; + try { + packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")); + } catch (error) { + fail(`${rel(packageJsonPath)} is not valid JSON: ${error.message}`); + } + const packageName = packageJson.name; + if (typeof packageName !== "string" || packageName.length === 0) { + fail(`${rel(packageJsonPath)} must declare name`); + } + if (packages.has(packageName)) { + fail(`duplicate npm package name ${packageName} in ${rel(packages.get(packageName))} and ${rel(packageDir)}`); + } + packages.set(packageName, packageDir); + } + if (packages.size === 0) { + fail(`${rel(packageRoot)} does not contain npm package descriptors`); + } + return packages; +} + +function nodeDirectOptionalPackageTargets(version) { + const packageDirs = npmPackageDirsUnder(NODE_DIRECT_PACKAGE_ROOT); + const packages = []; + for (const target of artifactTargets(NODE_DIRECT_PRODUCT, NODE_DIRECT_KIND, TOOL)) { + const packageName = target.npm_package; + if (typeof packageName !== "string" || packageName.length === 0) { + fail(`${target.id} must declare npm_package for npm optional package publication`); + } + const packageDir = packageDirs.get(packageName); + if (packageDir === undefined) { + fail(`${target.id} declares unknown Node direct npm package ${packageName}`); + } + const packageJson = JSON.parse(readFileSync(path.join(packageDir, "package.json"), "utf8")); + if (packageJson.name !== packageName) { + fail(`${rel(packageDir)}/package.json name must be ${packageName}`); + } + if (packageJson.version !== version) { + fail(`${packageName} package version must match ${NODE_DIRECT_PRODUCT} ${version}`); + } + packages.push([packageName, packageDir, target]); + } + const expected = packages.map(([packageName]) => packageName).sort(compareText); + const actual = [...packageDirs.keys()].sort(compareText); + if (JSON.stringify(actual) !== JSON.stringify(expected)) { + fail("Node direct npm optional package metadata must match published artifact targets exactly"); + } + return packages.sort((left, right) => compareText(left[0], right[0])); +} + +function safeNpmPackageFilenamePrefix(packageName) { + return packageName.replace(/^@/u, "").replace("/", "-"); +} + +function nodeDirectNpmPackageDir() { + return path.join(ROOT, "target/oliphaunt-node-direct/npm-packages"); +} + +function expectedNodeDirectNpmTarball(packageName, version) { + return path.join(nodeDirectNpmPackageDir(), `${safeNpmPackageFilenamePrefix(packageName)}-${version}.tgz`); +} + +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 readTarGzMember(file, expectedName) { + const buffer = gunzipSync(readFileSync(file)); + for (let offset = 0; offset + 512 <= buffer.length;) { + const header = buffer.subarray(offset, offset + 512); + if (header.every((byte) => byte === 0)) { + break; + } + const rawName = parseTarString(header, 0, 100); + const prefix = parseTarString(header, 345, 155); + const name = prefix ? `${prefix}/${rawName}` : rawName; + const size = parseTarOctal(header, 124, 12); + const dataOffset = offset + 512; + if (name === expectedName) { + return buffer.subarray(dataOffset, dataOffset + size); + } + offset = dataOffset + Math.ceil(size / 512) * 512; + } + return null; +} + +function readTarGzEntries(file) { + const buffer = gunzipSync(readFileSync(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 rawName = parseTarString(header, 0, 100); + const prefix = parseTarString(header, 345, 155); + const name = prefix ? `${prefix}/${rawName}` : rawName; + const size = parseTarOctal(header, 124, 12); + const type = header.subarray(156, 157).toString("utf8"); + entries.set(name, { size, isFile: type === "" || type === "0" }); + offset += 512 + Math.ceil(size / 512) * 512; + } + return entries; +} + +async function validateNodeDirectOptionalTarball(packageName, version, tarball) { + if (!isFile(tarball)) { + fail(`missing Node direct optional npm package artifact: ${rel(tarball)}`); + } + let entries; + try { + entries = readTarGzEntries(tarball); + } catch (error) { + fail(`${rel(tarball)} is not a valid Node direct optional npm tarball: ${error.message}`); + } + for (const required of ["package/package.json", "package/prebuilds/oliphaunt_node.node"]) { + if (!entries.has(required)) { + fail(`${rel(tarball)} is missing ${required}`); + } + } + const prebuild = entries.get("package/prebuilds/oliphaunt_node.node"); + if (!prebuild.isFile || prebuild.size <= 0) { + fail(`${rel(tarball)} prebuilt addon must be a non-empty regular file`); + } + let packageJson; + try { + const packageData = readTarGzMember(tarball, "package/package.json"); + if (packageData === null) { + fail(`${rel(tarball)} package/package.json could not be read`); + } + packageJson = JSON.parse(packageData.toString("utf8")); + } catch (error) { + fail(`${rel(tarball)} package/package.json is not valid JSON: ${error.message}`); + } + if (packageJson.name !== packageName) { + fail(`${rel(tarball)} package name must be ${packageName}, got ${JSON.stringify(packageJson.name)}`); + } + if (packageJson.version !== version) { + fail(`${rel(tarball)} package version must be ${version}, got ${JSON.stringify(packageJson.version)}`); + } +} + +async function nodeDirectOptionalNpmTarballs(version) { + const tarballs = []; + for (const [packageName] of nodeDirectOptionalPackageTargets(version)) { + const tarball = expectedNodeDirectNpmTarball(packageName, version); + await validateNodeDirectOptionalTarball(packageName, version, tarball); + tarballs.push([packageName, tarball]); + } + const expected = new Set(tarballs.map(([, tarball]) => path.resolve(tarball))); + const unexpected = isDirectory(nodeDirectNpmPackageDir()) + ? readdirSync(nodeDirectNpmPackageDir()) + .filter((name) => name.endsWith(".tgz")) + .map((name) => path.join(nodeDirectNpmPackageDir(), name)) + .filter((file) => !expected.has(path.resolve(file))) + .map((file) => path.basename(file)) + .sort(compareText) + : []; + if (unexpected.length > 0) { + fail(`unexpected Node direct optional npm package artifact(s): ${unexpected.join(", ")}`); + } + return tarballs; +} + +async function runNodeDirectDryRun() { + run(TOOL, ["src/runtimes/node-direct/tools/check-package.sh", "package-shape"]); + ensureNodeDirectReleaseAssets(); + await nodeDirectOptionalNpmTarballs(currentProductVersionSync(NODE_DIRECT_PRODUCT, TOOL)); +} + +export async function runBunProductDryRun(product, { allowDirty = false } = {}) { + if (SUPPORTED_SDK_PRODUCT_DRY_RUNS.has(product)) { + await runSdkProductDryRun(product, { allowDirty }); + return; + } + if (product === NODE_DIRECT_PRODUCT) { + await runNodeDirectDryRun(); + return; + } + fail(`no Bun publish dry-run handler for ${product}`, 2); +} + +function usage() { + console.log(`usage: tools/release/release-product-dry-run.mjs --product PRODUCT [--allow-dirty] + +Runs Bun-owned product publish dry-run checks. Release-wide checks and registry +dependency checks are owned by release-publish.mjs before this helper is invoked +from the public publish dry-run command surface. +`); +} + +function parseArgs(argv) { + const args = { + allowDirty: false, + product: null, + }; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === "--allow-dirty") { + args.allowDirty = true; + } else if (arg === "--product") { + const value = argv[index + 1]; + if (!value) { + usage(); + fail("--product requires a value", 2); + } + args.product = value; + index += 1; + } else if (arg.startsWith("--product=")) { + args.product = arg.slice("--product=".length); + } else if (arg === "-h" || arg === "--help") { + usage(); + process.exit(0); + } else { + usage(); + fail(`unknown argument ${arg}`, 2); + } + } + if (args.product === null) { + usage(); + fail("--product is required", 2); + } + return args; +} + +if (import.meta.main) { + const args = parseArgs(Bun.argv.slice(2)); + await runBunProductDryRun(args.product, { allowDirty: args.allowDirty }); +} diff --git a/tools/release/release-publish.mjs b/tools/release/release-publish.mjs index 7a1332a6..28fbf170 100755 --- a/tools/release/release-publish.mjs +++ b/tools/release/release-publish.mjs @@ -3,9 +3,9 @@ import { spawnSync } from "node:child_process"; import { ROOT, run } from "./release-cli-utils.mjs"; import { - SUPPORTED_SDK_PRODUCT_DRY_RUNS, - runSdkProductDryRun, -} from "./release-sdk-product-dry-run.mjs"; + SUPPORTED_BUN_PRODUCT_DRY_RUNS, + runBunProductDryRun, +} from "./release-product-dry-run.mjs"; const TOOL = "release-publish.mjs"; const COMMANDS = new Set(["publish", "publish-dry-run"]); @@ -15,9 +15,9 @@ function usage() { Runs protected release publish and publish dry-run operations through the Bun release command surface. The public no-product publish dry-run and selected -low-risk SDK product dry-runs are handled in Bun; other product dry-runs, -legacy --wasm dry-runs, and protected publish dispatch still delegate to -release.py while the protected implementation is ported. +low-risk product dry-runs are handled in Bun; other product dry-runs, legacy +--wasm dry-runs, and protected publish dispatch still delegate to release.py +while the protected implementation is ported. `); } @@ -102,7 +102,7 @@ function productPublishDryRunPlan(args) { if (!Array.isArray(requested) || requested.length === 0 || !requested.every((item) => typeof item === "string")) { return null; } - if (!requested.every((product) => SUPPORTED_SDK_PRODUCT_DRY_RUNS.has(product))) { + if (!requested.every((product) => SUPPORTED_BUN_PRODUCT_DRY_RUNS.has(product))) { return null; } const ordered = jsonOutput([ @@ -114,7 +114,7 @@ function productPublishDryRunPlan(args) { if (!Array.isArray(ordered) || ordered.length === 0 || !ordered.every((item) => typeof item === "string")) { return null; } - if (!ordered.every((product) => SUPPORTED_SDK_PRODUCT_DRY_RUNS.has(product))) { + if (!ordered.every((product) => SUPPORTED_BUN_PRODUCT_DRY_RUNS.has(product))) { return null; } return { @@ -138,7 +138,7 @@ if (productDryRunPlan !== null) { run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check.mjs"]); run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check-registries.mjs", ...productDryRunPlan.passthrough]); for (const product of productDryRunPlan.products) { - await runSdkProductDryRun(product, { allowDirty: productDryRunPlan.allowDirty }); + await runBunProductDryRun(product, { allowDirty: productDryRunPlan.allowDirty }); } process.exit(0); } From 7122d26c1abdd813b71a7387ed32e48acd638a95 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 05:59:53 +0000 Subject: [PATCH 272/308] chore: align node-direct dry run guard --- docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 6 ++++++ src/sdks/js/tools/check-sdk.sh | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 9a611fd0..c0cc4d80 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,12 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-28: Aligned the TypeScript SDK package-shape guard with the Bun + Node-direct dry-run bridge. `src/sdks/js/tools/check-sdk.sh` now requires + `ensureNodeDirectReleaseAssets` and `nodeDirectOptionalNpmTarballs` in + `tools/release/release-product-dry-run.mjs` instead of treating + `tools/release/release.py` as the public dry-run owner. Protected publish + validation remains separately guarded until publish dispatch is ported. - 2026-06-28: Added a Bun product dry-run bridge `tools/release/release-product-dry-run.mjs` and moved `oliphaunt-node-direct` product dry-run dispatch out of `release.py` when diff --git a/src/sdks/js/tools/check-sdk.sh b/src/sdks/js/tools/check-sdk.sh index 59c70423..815ea7a8 100755 --- a/src/sdks/js/tools/check-sdk.sh +++ b/src/sdks/js/tools/check-sdk.sh @@ -388,10 +388,10 @@ require_source_text "$package_dir/src/native/node-addon.ts" "oliphaunt-node-dire "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" \ "Node direct runtime must package the prebuilt Node.js native-direct adapter as a release asset" -require_source_text "$root/tools/release/release.py" "ensure_node_direct_release_assets" \ - "Node direct release dry-run must validate staged Node.js native-direct adapter release assets" -require_source_text "$root/tools/release/release.py" "node_direct_optional_npm_tarballs" \ - "Node direct release dry-run must validate staged optional npm tarballs from builder jobs" +require_source_text "$root/tools/release/release-product-dry-run.mjs" "ensureNodeDirectReleaseAssets" \ + "Node direct release dry-run must validate staged Node.js native-direct adapter release assets in Bun" +require_source_text "$root/tools/release/release-product-dry-run.mjs" "nodeDirectOptionalNpmTarballs" \ + "Node direct release dry-run must validate staged optional npm tarballs from builder jobs in Bun" 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" \ From f4a8039addf2b72890cf98528cd9732f90a5ed43 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 06:02:43 +0000 Subject: [PATCH 273/308] chore: track node-direct dry run inputs --- docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 4 ++++ src/sdks/js/moon.yml | 6 ++---- tools/policy/check-tooling-stack.sh | 5 +++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index c0cc4d80..a90931ad 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -84,6 +84,10 @@ until the current-state gates here are checked with fresh local evidence. `tools/release/release-product-dry-run.mjs` instead of treating `tools/release/release.py` as the public dry-run owner. Protected publish validation remains separately guarded until publish dispatch is ported. + `src/sdks/js/moon.yml` now tracks the Bun product dry-run helper for the + TypeScript SDK tasks that read those guards and drops stale direct + `release.py` inputs where the task no longer reads the protected + implementation. Tooling-stack policy rejects regressing that input surface. - 2026-06-28: Added a Bun product dry-run bridge `tools/release/release-product-dry-run.mjs` and moved `oliphaunt-node-direct` product dry-run dispatch out of `release.py` when diff --git a/src/sdks/js/moon.yml b/src/sdks/js/moon.yml index 7d3e60df..82d89338 100644 --- a/src/sdks/js/moon.yml +++ b/src/sdks/js/moon.yml @@ -45,7 +45,7 @@ tasks: - "/pnpm-workspace.yaml" - "/src/sdks/js/**/*" - "/src/runtimes/node-direct/**/*" - - "/tools/release/release.py" + - "/tools/release/release-product-dry-run.mjs" - "/tools/runtime/**/*" options: cache: true @@ -114,7 +114,6 @@ tasks: - "/src/runtimes/node-direct/**/*" - "/tools/release/build-sdk-ci-artifacts.mjs" - "/tools/release/check-staged-artifacts.mjs" - - "/tools/release/release.py" - "/tools/test/**/*" - "/tools/runtime/**/*" outputs: @@ -137,7 +136,6 @@ tasks: - "/src/runtimes/node-direct/**/*" - "/tools/release/build-sdk-ci-artifacts.mjs" - "/tools/release/check-staged-artifacts.mjs" - - "/tools/release/release.py" - "/tools/test/**/*" - "/tools/runtime/**/*" outputs: @@ -199,7 +197,7 @@ tasks: - "/pnpm-workspace.yaml" - "/src/sdks/js/**/*" - "/src/runtimes/node-direct/**/*" - - "/tools/release/release.py" + - "/tools/release/release-product-dry-run.mjs" - "/tools/runtime/**/*" options: cache: local diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 676be20d..1d9d6f9f 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -437,6 +437,11 @@ grep -Fq 'ensureNodeDirectReleaseAssets' tools/release/release-product-dry-run.m fail "Bun Node direct product dry-run must validate staged release assets" grep -Fq 'nodeDirectOptionalNpmTarballs' tools/release/release-product-dry-run.mjs || fail "Bun Node direct product dry-run must validate optional npm tarball artifacts" +grep -Fq '/tools/release/release-product-dry-run.mjs' src/sdks/js/moon.yml || + fail "TypeScript SDK Moon tasks must track the Bun Node direct product dry-run helper" +if grep -Fq '/tools/release/release.py' src/sdks/js/moon.yml; then + fail "TypeScript SDK Moon tasks must not track release.py after Node direct dry-run guards moved to Bun" +fi grep -Fq '"oliphaunt-swift",' tools/release/release-sdk-product-dry-run.mjs || fail "release SDK product dry-run helper must include Swift in Bun-owned low-risk SDK product dry-runs" grep -Fq '"oliphaunt-kotlin",' tools/release/release-sdk-product-dry-run.mjs || From 7c4acbe6502aaff30700f9080638ffd864da5c95 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 06:13:03 +0000 Subject: [PATCH 274/308] chore: move broker dry run into bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 15 + tools/policy/check-tooling-stack.sh | 8 + tools/release/check_artifact_targets.mjs | 5 + tools/release/check_release_metadata.py | 9 + tools/release/release-product-dry-run.mjs | 314 +++++++++++++++++- 5 files changed, 343 insertions(+), 8 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index a90931ad..26345e0f 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,21 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-28: Extended the Bun product dry-run bridge to + `oliphaunt-broker`. The Broker product path now validates staged release + assets with `check-broker-release-assets.mjs`, rewrites the checksum + manifest, stages and packs the platform npm helper packages from release + archives, validates the packed npm tarballs, and generates/validates Broker + Cargo artifact crates through `package_broker_cargo_artifacts.mjs`. + `tools/dev/bun.sh tools/release/release-product-dry-run.mjs --product oliphaunt-broker --allow-dirty` + passed against the staged Broker release assets in this checkout. Release + graph ordering for `["oliphaunt-broker"]` resolves to Broker only. The public + `release-publish.mjs publish-dry-run --products-json '["oliphaunt-broker"]' + --allow-dirty --head-ref HEAD` route now reaches the Bun release/registry + preflight and stops at the expected missing `liboliphaunt-native-v0.1.0` + dependency tag because `liboliphaunt-native` is not selected. Release + metadata, artifact-target, and tooling-stack guards now require the Broker + Bun dry-run path. - 2026-06-28: Aligned the TypeScript SDK package-shape guard with the Bun Node-direct dry-run bridge. `src/sdks/js/tools/check-sdk.sh` now requires `ensureNodeDirectReleaseAssets` and `nodeDirectOptionalNpmTarballs` in diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 1d9d6f9f..e9a7c590 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -431,6 +431,14 @@ grep -Fq 'await runBunProductDryRun(product, { allowDirty: productDryRunPlan.all fail "release publish dry-run wrapper must execute supported product dry-runs in Bun" grep -Fq 'SUPPORTED_SDK_PRODUCT_DRY_RUNS' tools/release/release-product-dry-run.mjs || fail "release product dry-run bridge must preserve SDK helper ownership" +grep -Fq 'BROKER_PRODUCT,' tools/release/release-product-dry-run.mjs || + fail "release product dry-run bridge must include Broker in Bun-owned product dry-runs" +grep -Fq 'ensureBrokerReleaseAssets' tools/release/release-product-dry-run.mjs || + fail "Bun Broker product dry-run must validate staged release assets" +grep -Fq 'brokerNpmTarballs' tools/release/release-product-dry-run.mjs || + fail "Bun Broker product dry-run must validate broker npm tarball artifacts" +grep -Fq 'tools/release/package_broker_cargo_artifacts.mjs' tools/release/release-product-dry-run.mjs || + fail "Bun Broker product dry-run must generate broker Cargo artifact crates" grep -Fq 'NODE_DIRECT_PRODUCT,' tools/release/release-product-dry-run.mjs || fail "release product dry-run bridge must include Node direct in Bun-owned product dry-runs" grep -Fq 'ensureNodeDirectReleaseAssets' tools/release/release-product-dry-run.mjs || diff --git a/tools/release/check_artifact_targets.mjs b/tools/release/check_artifact_targets.mjs index 2a107d3d..f1499fdf 100644 --- a/tools/release/check_artifact_targets.mjs +++ b/tools/release/check_artifact_targets.mjs @@ -1152,6 +1152,11 @@ function validateCiReleaseArtifacts() { "npm-package-sources", "npm artifact packages must be assembled from staged package sources instead of mutating checked-in package directories", ); + requireText( + "tools/release/release-product-dry-run.mjs", + "brokerNpmTarballs", + "Broker product dry-run must validate staged broker npm tarballs in Bun", + ); requireText( "tools/release/release.py", "package-liboliphaunt-cargo-artifacts.mjs", diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 14c7c4eb..075d910d 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1005,6 +1005,10 @@ def validate_publish_target_coverage() -> None: or "SUPPORTED_BUN_PRODUCT_DRY_RUNS" not in release_publish or 'await runBunProductDryRun(product, { allowDirty: productDryRunPlan.allowDirty });' not in release_publish or "SUPPORTED_SDK_PRODUCT_DRY_RUNS" not in release_product_dry_run + or "BROKER_PRODUCT," not in release_product_dry_run + or "ensureBrokerReleaseAssets" not in release_product_dry_run + or "brokerNpmTarballs" not in release_product_dry_run + or "tools/release/package_broker_cargo_artifacts.mjs" not in release_product_dry_run or "NODE_DIRECT_PRODUCT," not in release_product_dry_run or "ensureNodeDirectReleaseAssets" not in release_product_dry_run or "nodeDirectOptionalNpmTarballs" not in release_product_dry_run @@ -2046,6 +2050,11 @@ def validate_typescript( "nodeDirectOptionalNpmTarballs", "Node direct release dry-run must validate staged optional npm tarballs from the builder job in Bun", ) + require_text( + "tools/release/release-product-dry-run.mjs", + "brokerNpmTarballs", + "Broker release dry-run must validate staged broker npm tarballs from release assets in Bun", + ) require_text( "src/sdks/js/src/native/assets-deno.ts", "runtimeRelativePath", diff --git a/tools/release/release-product-dry-run.mjs b/tools/release/release-product-dry-run.mjs index bd5cd1d1..01b78ebe 100644 --- a/tools/release/release-product-dry-run.mjs +++ b/tools/release/release-product-dry-run.mjs @@ -1,11 +1,15 @@ #!/usr/bin/env bun +import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; import { + chmodSync, copyFileSync, mkdirSync, readdirSync, readFileSync, + rmSync, statSync, + writeFileSync, } from "node:fs"; import path from "node:path"; import { gunzipSync } from "node:zlib"; @@ -22,12 +26,16 @@ import { } from "./release-artifact-targets.mjs"; const TOOL = "release-product-dry-run.mjs"; +const BROKER_PRODUCT = "oliphaunt-broker"; +const BROKER_KIND = "broker-helper"; +const BROKER_PACKAGE_ROOT = path.join(ROOT, "src/runtimes/broker/packages"); const NODE_DIRECT_PRODUCT = "oliphaunt-node-direct"; const NODE_DIRECT_KIND = "node-direct-addon"; const NODE_DIRECT_PACKAGE_ROOT = path.join(ROOT, "src/runtimes/node-direct/packages"); export const SUPPORTED_BUN_PRODUCT_DRY_RUNS = new Set([ ...SUPPORTED_SDK_PRODUCT_DRY_RUNS, + BROKER_PRODUCT, NODE_DIRECT_PRODUCT, ]); @@ -60,6 +68,23 @@ function sha256File(file) { return createHash("sha256").update(readFileSync(file)).digest("hex"); } +function commandOutput(command, args, { cwd = ROOT, encoding = "utf8" } = {}) { + const result = spawnSync(command, args, { + cwd, + encoding, + maxBuffer: 100 * 1024 * 1024, + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.error !== undefined) { + fail(`${command} failed to start: ${result.error.message}`); + } + if (result.status !== 0) { + const stderr = Buffer.isBuffer(result.stderr) ? result.stderr.toString("utf8") : result.stderr; + fail(`${command} ${args.join(" ")} failed${stderr ? `: ${stderr.trim()}` : ""}`); + } + return result.stdout; +} + function stagedRuntimeInputDirs(envName) { const raw = process.env[envName] ?? process.env.OLIPHAUNT_RELEASE_ASSET_INPUT_DIRS ?? ""; return raw @@ -133,6 +158,46 @@ function hasNodeDirectReleaseArchive(assetDir) { ); } +function hasBrokerReleaseArchive(assetDir) { + if (!isDirectory(assetDir)) { + return false; + } + return readdirSync(assetDir).some((name) => + name.startsWith("oliphaunt-broker-") && (name.endsWith(".tar.gz") || name.endsWith(".zip")), + ); +} + +function ensureBrokerReleaseAssets() { + const assetDir = path.join(ROOT, "target/oliphaunt-broker/release-assets"); + if (!hasBrokerReleaseArchive(assetDir)) { + copyStagedRuntimeAssets({ + product: BROKER_PRODUCT, + destination: assetDir, + envName: "OLIPHAUNT_BROKER_RELEASE_ASSET_INPUT_DIRS", + patterns: ["oliphaunt-broker-*.tar.gz", "oliphaunt-broker-*.zip"], + }); + } + const version = currentProductVersionSync(BROKER_PRODUCT, TOOL); + run(TOOL, [ + "tools/dev/bun.sh", + "tools/release/write_checksum_manifest.mjs", + "--asset-dir", + rel(assetDir), + "--output", + `oliphaunt-broker-${version}-release-assets.sha256`, + "--pattern", + "oliphaunt-broker-*.tar.gz", + "--pattern", + "oliphaunt-broker-*.zip", + ]); + run(TOOL, [ + "tools/dev/bun.sh", + "tools/release/check-broker-release-assets.mjs", + "--asset-dir", + rel(assetDir), + ]); +} + function ensureNodeDirectReleaseAssets() { const assetDir = path.join(ROOT, "target/oliphaunt-node-direct/release-assets"); if (!hasNodeDirectReleaseArchive(assetDir)) { @@ -196,35 +261,61 @@ function npmPackageDirsUnder(packageRoot) { return packages; } -function nodeDirectOptionalPackageTargets(version) { - const packageDirs = npmPackageDirsUnder(NODE_DIRECT_PACKAGE_ROOT); +function artifactNpmPackageTargets({ + product, + kind, + surface, + packageRoot, + version, +}) { + const packageDirs = npmPackageDirsUnder(packageRoot); const packages = []; - for (const target of artifactTargets(NODE_DIRECT_PRODUCT, NODE_DIRECT_KIND, TOOL)) { + for (const target of artifactTargets(product, kind, TOOL).filter((candidate) => candidate.surfaces.includes(surface))) { const packageName = target.npm_package; if (typeof packageName !== "string" || packageName.length === 0) { - fail(`${target.id} must declare npm_package for npm optional package publication`); + fail(`${target.id} must declare npm_package for npm artifact package publication`); } const packageDir = packageDirs.get(packageName); if (packageDir === undefined) { - fail(`${target.id} declares unknown Node direct npm package ${packageName}`); + fail(`${target.id} declares unknown npm package ${packageName}`); } const packageJson = JSON.parse(readFileSync(path.join(packageDir, "package.json"), "utf8")); if (packageJson.name !== packageName) { fail(`${rel(packageDir)}/package.json name must be ${packageName}`); } if (packageJson.version !== version) { - fail(`${packageName} package version must match ${NODE_DIRECT_PRODUCT} ${version}`); + fail(`${packageName} package version must match ${product} ${version}`); } packages.push([packageName, packageDir, target]); } const expected = packages.map(([packageName]) => packageName).sort(compareText); const actual = [...packageDirs.keys()].sort(compareText); if (JSON.stringify(actual) !== JSON.stringify(expected)) { - fail("Node direct npm optional package metadata must match published artifact targets exactly"); + fail(`${rel(packageRoot)} package descriptors must match published ${product} npm artifact targets for ${surface}`); } return packages.sort((left, right) => compareText(left[0], right[0])); } +function nodeDirectOptionalPackageTargets(version) { + return artifactNpmPackageTargets({ + product: NODE_DIRECT_PRODUCT, + kind: NODE_DIRECT_KIND, + surface: "npm-optional", + packageRoot: NODE_DIRECT_PACKAGE_ROOT, + version, + }); +} + +function brokerNpmPackageTargets(version) { + return artifactNpmPackageTargets({ + product: BROKER_PRODUCT, + kind: BROKER_KIND, + surface: "typescript-broker", + packageRoot: BROKER_PACKAGE_ROOT, + version, + }); +} + function safeNpmPackageFilenamePrefix(packageName) { return packageName.replace(/^@/u, "").replace("/", "-"); } @@ -281,14 +372,203 @@ function readTarGzEntries(file) { const rawName = parseTarString(header, 0, 100); const prefix = parseTarString(header, 345, 155); const name = prefix ? `${prefix}/${rawName}` : rawName; + const mode = parseTarOctal(header, 100, 8); const size = parseTarOctal(header, 124, 12); const type = header.subarray(156, 157).toString("utf8"); - entries.set(name, { size, isFile: type === "" || type === "0" }); + entries.set(name, { mode, size, isFile: type === "" || type === "0" }); offset += 512 + Math.ceil(size / 512) * 512; } return entries; } +function validateNoConsumerInstallScripts(packageJson, context) { + const scripts = packageJson.scripts; + if (scripts === undefined) { + return; + } + if (scripts === null || typeof scripts !== "object" || Array.isArray(scripts)) { + fail(`${context} scripts must be an object when present`); + } + for (const scriptName of ["preinstall", "install", "postinstall", "prepare"]) { + if (Object.hasOwn(scripts, scriptName)) { + fail(`${context} must not declare consumer install lifecycle script ${scriptName}`); + } + } +} + +function npmPackageSourceStageDir(packageName) { + return path.join(ROOT, "target/release/npm-package-sources", safeNpmPackageFilenamePrefix(packageName)); +} + +function stageNpmPackageDescriptor(packageName, sourceDir, version, { target = null } = {}) { + const stageDir = npmPackageSourceStageDir(packageName); + rmSync(stageDir, { recursive: true, force: true }); + mkdirSync(stageDir, { recursive: true }); + for (const descriptor of ["package.json", "README.md"]) { + const source = path.join(sourceDir, descriptor); + if (!isFile(source)) { + fail(`${rel(sourceDir)} is missing ${descriptor}`); + } + copyFileSync(source, path.join(stageDir, descriptor)); + } + const packageJsonPath = path.join(stageDir, "package.json"); + const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")); + if (packageJson.name !== packageName) { + fail(`${rel(packageJsonPath)} name must be ${packageName}`); + } + if (packageJson.version !== version) { + fail(`${packageName} package version must match ${version}`); + } + if (target !== null && packageJson.oliphaunt?.target !== target) { + fail(`${packageName} package oliphaunt.target must be ${target}`); + } + validateNoConsumerInstallScripts(packageJson, `${packageName} npm package`); + return stageDir; +} + +function readReleaseArchiveMember(archive, memberName) { + if (archive.endsWith(".tar.gz")) { + for (const candidate of [memberName, `./${memberName}`]) { + const data = readTarGzMember(archive, candidate); + if (data !== null) { + return data; + } + } + fail(`${rel(archive)} is missing ${memberName}`); + } + if (path.extname(archive) === ".zip") { + for (const candidate of [memberName, `./${memberName}`]) { + const result = spawnSync("unzip", ["-p", archive, candidate], { + cwd: ROOT, + encoding: "buffer", + maxBuffer: 100 * 1024 * 1024, + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.error !== undefined) { + fail(`unzip failed to start: ${result.error.message}`); + } + if (result.status === 0) { + return result.stdout; + } + } + fail(`${rel(archive)} is missing ${memberName}`); + } + fail(`${rel(archive)} has unsupported release archive extension`); +} + +function extractReleaseArchiveFile(archive, memberName, destination, { mode = null } = {}) { + const data = readReleaseArchiveMember(archive, memberName); + mkdirSync(path.dirname(destination), { recursive: true }); + writeFileSync(destination, data); + if (mode !== null) { + chmodSync(destination, mode); + } +} + +function pnpmPackForNpmPublish(packageDir) { + const packageJson = JSON.parse(readFileSync(path.join(packageDir, "package.json"), "utf8")); + const packageName = packageJson.name; + if (typeof packageName !== "string" || packageName.length === 0) { + fail(`${rel(packageDir)}/package.json must declare a package name`); + } + const packDir = path.join(ROOT, "target/release/npm-packages", safeNpmPackageFilenamePrefix(packageName)); + rmSync(packDir, { recursive: true, force: true }); + mkdirSync(packDir, { recursive: true }); + const rendered = commandOutput("pnpm", ["pack", "--pack-destination", packDir, "--json"], { cwd: packageDir }); + let manifest; + try { + manifest = JSON.parse(rendered); + } catch (error) { + fail(`pnpm pack for ${packageName} did not emit JSON: ${error.message}`); + } + const filename = Array.isArray(manifest) ? manifest[0]?.filename : manifest?.filename; + if (typeof filename !== "string" || !filename.endsWith(".tgz")) { + fail(`pnpm pack for ${packageName} did not report a .tgz filename`); + } + const tarball = path.isAbsolute(filename) ? filename : path.join(packDir, filename); + if (!isFile(tarball)) { + fail(`pnpm pack for ${packageName} did not create ${rel(tarball)}`); + } + return tarball; +} + +function validatePackedNpmPackage({ + packageName, + version, + tarball, + requiredMembers, + executableMembers = [], +}) { + let entries; + try { + entries = readTarGzEntries(tarball); + } catch (error) { + fail(`${rel(tarball)} is not a valid npm tarball: ${error.message}`); + } + if (!entries.has("package/package.json")) { + fail(`${rel(tarball)} is missing package/package.json`); + } + let packageJson; + try { + const packageData = readTarGzMember(tarball, "package/package.json"); + if (packageData === null) { + fail(`${rel(tarball)} package/package.json could not be read`); + } + packageJson = JSON.parse(packageData.toString("utf8")); + } catch (error) { + fail(`${rel(tarball)} package/package.json is not valid JSON: ${error.message}`); + } + if (packageJson.name !== packageName) { + fail(`${rel(tarball)} package name must be ${packageName}, got ${JSON.stringify(packageJson.name)}`); + } + if (packageJson.version !== version) { + fail(`${rel(tarball)} package version must be ${version}, got ${JSON.stringify(packageJson.version)}`); + } + for (const member of requiredMembers) { + const entry = entries.get(member); + if (entry === undefined) { + fail(`${rel(tarball)} is missing ${member}`); + } + if (!entry.isFile || entry.size <= 0) { + fail(`${rel(tarball)} ${member} must be a non-empty regular file`); + } + } + for (const member of executableMembers) { + const entry = entries.get(member); + if (entry === undefined) { + fail(`${rel(tarball)} is missing executable ${member}`); + } + if (!entry.isFile || entry.size <= 0 || (entry.mode & 0o111) === 0) { + fail(`${rel(tarball)} ${member} must be a non-empty executable file`); + } + } +} + +function brokerNpmTarballs(version) { + const tarballs = []; + const assetDir = path.join(ROOT, "target/oliphaunt-broker/release-assets"); + for (const [packageName, packageDir, target] of brokerNpmPackageTargets(version)) { + const executableRelativePath = target.executable_relative_path; + if (typeof executableRelativePath !== "string" || executableRelativePath.length === 0) { + fail(`${target.id} must declare executable_relative_path for npm artifact package publication`); + } + const stageDir = stageNpmPackageDescriptor(packageName, packageDir, version, { target: target.target }); + const archive = path.join(assetDir, target.asset.replaceAll("{version}", version)); + extractReleaseArchiveFile(archive, executableRelativePath, path.join(stageDir, executableRelativePath), { mode: 0o755 }); + const tarball = pnpmPackForNpmPublish(stageDir); + const requiredMembers = [`package/${executableRelativePath}`]; + validatePackedNpmPackage({ + packageName, + version, + tarball, + requiredMembers, + executableMembers: requiredMembers, + }); + tarballs.push([packageName, tarball]); + } + return tarballs; +} + async function validateNodeDirectOptionalTarball(packageName, version, tarball) { if (!isFile(tarball)) { fail(`missing Node direct optional npm package artifact: ${rel(tarball)}`); @@ -354,11 +634,29 @@ async function runNodeDirectDryRun() { await nodeDirectOptionalNpmTarballs(currentProductVersionSync(NODE_DIRECT_PRODUCT, TOOL)); } +function runBrokerDryRun() { + const version = currentProductVersionSync(BROKER_PRODUCT, TOOL); + ensureBrokerReleaseAssets(); + brokerNpmTarballs(version); + run(TOOL, [ + "tools/dev/bun.sh", + "tools/release/package_broker_cargo_artifacts.mjs", + "--version", + version, + "--output-dir", + "target/oliphaunt-broker/cargo-artifacts", + ]); +} + export async function runBunProductDryRun(product, { allowDirty = false } = {}) { if (SUPPORTED_SDK_PRODUCT_DRY_RUNS.has(product)) { await runSdkProductDryRun(product, { allowDirty }); return; } + if (product === BROKER_PRODUCT) { + runBrokerDryRun(); + return; + } if (product === NODE_DIRECT_PRODUCT) { await runNodeDirectDryRun(); return; From c0e3d70fe9e8bce94f83085e35fc959c56cb70f3 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 06:27:20 +0000 Subject: [PATCH 275/308] chore: move extension dry runs into bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 28 ++++-- tools/policy/check-tooling-stack.sh | 8 ++ tools/release/check_artifact_targets.mjs | 20 +++++ tools/release/check_release_metadata.py | 15 ++++ tools/release/release-product-dry-run.mjs | 87 +++++++++++++++++++ 5 files changed, 151 insertions(+), 7 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 26345e0f..01ceca90 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -78,6 +78,22 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-28: Extended the Bun product dry-run bridge to exact-extension + products. `SUPPORTED_BUN_PRODUCT_DRY_RUNS` now includes + `exactExtensionProducts(TOOL)`, so selected extension products route through + `release-product-dry-run.mjs` instead of the Python `publish-dry-run` + product handler when the product set is otherwise Bun-supported. The Bun path + reuses `check-staged-artifacts.mjs --require-extension-product + --require-full-extension-targets`, prints the staged release asset + paths, builds the extension Maven artifact TSV with + `build_maven_artifact_manifest.mjs`, and runs + `:oliphaunt-maven-artifacts:publishToMavenLocal` against Maven Local. + Direct validation on the partial local + `oliphaunt-extension-unaccent` staging failed closed before Gradle with the + expected missing published targets. The Bun support-set probe confirmed + `oliphaunt-extension-unaccent` is registered. Release metadata, + artifact-target, and tooling-stack checks passed with guards requiring the + Bun exact-extension dry-run path. - 2026-06-28: Extended the Bun product dry-run bridge to `oliphaunt-broker`. The Broker product path now validates staged release assets with `check-broker-release-assets.mjs`, rewrites the checksum @@ -2477,18 +2493,16 @@ until the current-state gates here are checked with fresh local evidence. - 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`. - The current inventory contains 5 tracked Python files: release orchestration, - release/package validators, local registry publishing, and the extension - model generator. New Python files must either be intentionally allowlisted or + The current inventory contains 4 tracked Python files: release orchestration, + release/package validators, and the extension model generator. New Python + files must either be intentionally allowlisted or ported to Bun. The current migration order is: 1. port the remaining release checkers in the release-graph cluster (`check_release_metadata.py`, `check_consumer_shape.py`) behind parity smokes and then remove their Python compatibility imports; - 2. port `local_registry_publish.py` after artifact package generation and - release metadata are Bun-native, preserving the local registry e2e path; - 3. port `release.py` last, when the underlying validators and registry helpers + 2. port `release.py` last, when the underlying validators and registry helpers have Bun entrypoints; - 4. port `src/extensions/tools/check-extension-model.py` as a separate + 3. port `src/extensions/tools/check-extension-model.py` as a separate generator migration, because it is the canonical multi-language extension model and needs generated-output parity across SDKs. - The local-registry metadata needed by release metadata checks now has a Bun diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index e9a7c590..8f0fe95e 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -445,6 +445,14 @@ grep -Fq 'ensureNodeDirectReleaseAssets' tools/release/release-product-dry-run.m fail "Bun Node direct product dry-run must validate staged release assets" grep -Fq 'nodeDirectOptionalNpmTarballs' tools/release/release-product-dry-run.mjs || fail "Bun Node direct product dry-run must validate optional npm tarball artifacts" +grep -Fq 'exactExtensionProducts(TOOL)' tools/release/release-product-dry-run.mjs || + fail "release product dry-run bridge must include exact-extension products in Bun-owned product dry-runs" +grep -Fq 'runExtensionDryRun' tools/release/release-product-dry-run.mjs || + fail "Bun exact-extension product dry-run must validate staged release assets and Maven artifacts" +grep -Fq '"--require-full-extension-targets"' tools/release/release-product-dry-run.mjs || + fail "Bun exact-extension product dry-run must reject partial staged exact-extension packages" +grep -Fq ':oliphaunt-maven-artifacts:publishToMavenLocal' tools/release/release-product-dry-run.mjs || + fail "Bun exact-extension product dry-run must publish extension Maven artifacts to Maven Local" grep -Fq '/tools/release/release-product-dry-run.mjs' src/sdks/js/moon.yml || fail "TypeScript SDK Moon tasks must track the Bun Node direct product dry-run helper" if grep -Fq '/tools/release/release.py' src/sdks/js/moon.yml; then diff --git a/tools/release/check_artifact_targets.mjs b/tools/release/check_artifact_targets.mjs index f1499fdf..6b805ef6 100644 --- a/tools/release/check_artifact_targets.mjs +++ b/tools/release/check_artifact_targets.mjs @@ -717,6 +717,21 @@ function validateCiReleaseArtifacts() { "nodeDirectOptionalNpmTarballs", "Node direct product dry-run must validate staged optional npm tarballs in Bun", ); + requireText( + "tools/release/release-product-dry-run.mjs", + "exactExtensionProducts(TOOL)", + "Exact-extension product dry-runs must be selected through the Bun dry-run support set", + ); + requireText( + "tools/release/release-product-dry-run.mjs", + "--require-full-extension-targets", + "Exact-extension product dry-runs must reject partial staged package artifacts in Bun", + ); + requireText( + "tools/release/release-product-dry-run.mjs", + ":oliphaunt-maven-artifacts:publishToMavenLocal", + "Exact-extension product dry-runs must run Maven Local publication in Bun", + ); requireText( "tools/release/release.py", 'run(["npm", "publish", str(tarball), "--access", "public", "--provenance"])', @@ -1157,6 +1172,11 @@ function validateCiReleaseArtifacts() { "brokerNpmTarballs", "Broker product dry-run must validate staged broker npm tarballs in Bun", ); + requireText( + "tools/release/release-product-dry-run.mjs", + "runExtensionDryRun", + "Exact-extension product dry-run must validate staged release assets and Maven artifacts in Bun", + ); requireText( "tools/release/release.py", "package-liboliphaunt-cargo-artifacts.mjs", diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 075d910d..a372423e 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -2055,6 +2055,21 @@ def validate_typescript( "brokerNpmTarballs", "Broker release dry-run must validate staged broker npm tarballs from release assets in Bun", ) + require_text( + "tools/release/release-product-dry-run.mjs", + "exactExtensionProducts(TOOL)", + "Exact-extension release dry-runs must run through the Bun product dry-run support set", + ) + require_text( + "tools/release/release-product-dry-run.mjs", + "--require-full-extension-targets", + "Exact-extension release dry-runs must reject partial staged extension packages in Bun", + ) + require_text( + "tools/release/release-product-dry-run.mjs", + ":oliphaunt-maven-artifacts:publishToMavenLocal", + "Exact-extension release dry-runs must publish extension Maven artifacts to Maven Local in Bun", + ) require_text( "src/sdks/js/src/native/assets-deno.ts", "runtimeRelativePath", diff --git a/tools/release/release-product-dry-run.mjs b/tools/release/release-product-dry-run.mjs index 01b78ebe..c21e53ef 100644 --- a/tools/release/release-product-dry-run.mjs +++ b/tools/release/release-product-dry-run.mjs @@ -23,6 +23,7 @@ import { artifactTargets, compareText, currentProductVersionSync, + exactExtensionProducts, } from "./release-artifact-targets.mjs"; const TOOL = "release-product-dry-run.mjs"; @@ -35,6 +36,7 @@ const NODE_DIRECT_PACKAGE_ROOT = path.join(ROOT, "src/runtimes/node-direct/packa export const SUPPORTED_BUN_PRODUCT_DRY_RUNS = new Set([ ...SUPPORTED_SDK_PRODUCT_DRY_RUNS, + ...exactExtensionProducts(TOOL), BROKER_PRODUCT, NODE_DIRECT_PRODUCT, ]); @@ -648,6 +650,87 @@ function runBrokerDryRun() { ]); } +function extensionPackageDir(product) { + return path.join(ROOT, "target/extension-artifacts", product); +} + +function extensionAssetPaths(product) { + run(TOOL, [ + "tools/dev/bun.sh", + "tools/release/check-staged-artifacts.mjs", + "--require-extension-product", + product, + "--require-full-extension-targets", + ]); + const assetDir = path.join(extensionPackageDir(product), "release-assets"); + if (!isDirectory(assetDir)) { + fail(`${product} extension package did not create ${rel(assetDir)}`); + } + const assets = readdirSync(assetDir) + .sort(compareText) + .map((name) => path.join(assetDir, name)) + .filter(isFile); + if (assets.length === 0) { + fail(`${product} extension package produced no release assets`); + } + return assets.map(rel); +} + +function buildMavenArtifactManifest(name, { runtime = false, extensions = false, extensionProducts = [] } = {}) { + const outputPath = path.join(ROOT, "target/release/maven-artifacts", `${name}.tsv`); + const command = [ + "tools/dev/bun.sh", + "tools/release/build_maven_artifact_manifest.mjs", + "--output", + rel(outputPath), + ]; + if (runtime) { + command.push("--runtime"); + } + if (extensions) { + command.push("--extensions"); + } + for (const extensionProduct of extensionProducts) { + command.push("--extension-product", extensionProduct); + } + run(TOOL, command); + return outputPath; +} + +function runMavenArtifactPublisher(manifest, task, cacheSlug) { + run(TOOL, [ + "src/sdks/kotlin/gradlew", + "-p", + "src/sdks/kotlin", + task, + `-PoliphauntMavenArtifactsManifest=${manifest}`, + `-PoliphauntBuildRoot=${path.join(ROOT, "target/liboliphaunt-sdk-check/gradle", cacheSlug)}`, + "--project-cache-dir", + path.join(ROOT, "target/liboliphaunt-sdk-check/gradle-cache", cacheSlug), + "--configure-on-demand", + "--no-configuration-cache", + ]); +} + +function runExtensionMavenArtifactDryRun(product) { + const manifest = buildMavenArtifactManifest(product, { + extensions: true, + extensionProducts: [product], + }); + runMavenArtifactPublisher( + manifest, + ":oliphaunt-maven-artifacts:publishToMavenLocal", + `${product}-maven-dry-run`, + ); +} + +function runExtensionDryRun(product) { + for (const asset of extensionAssetPaths(product)) { + console.log(`${product} release asset: ${asset}`); + } + runExtensionMavenArtifactDryRun(product); +} + export async function runBunProductDryRun(product, { allowDirty = false } = {}) { if (SUPPORTED_SDK_PRODUCT_DRY_RUNS.has(product)) { await runSdkProductDryRun(product, { allowDirty }); @@ -661,6 +744,10 @@ export async function runBunProductDryRun(product, { allowDirty = false } = {}) await runNodeDirectDryRun(); return; } + if (exactExtensionProducts(TOOL).includes(product)) { + runExtensionDryRun(product); + return; + } fail(`no Bun publish dry-run handler for ${product}`, 2); } From 45863ac81e842b75b62a7f55f5687af171540df9 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 06:55:50 +0000 Subject: [PATCH 276/308] chore: move wasix runtime dry run into bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 11 + tools/policy/check-tooling-stack.sh | 11 + ...heck-liboliphaunt-wasix-release-assets.mjs | 435 ++++++++++++++++++ tools/release/check_artifact_targets.mjs | 25 + tools/release/check_release_metadata.py | 5 + tools/release/release-product-dry-run.mjs | 151 ++++++ tools/xtask/src/release_workspace.rs | 2 +- 7 files changed, 639 insertions(+), 1 deletion(-) create mode 100644 tools/release/check-liboliphaunt-wasix-release-assets.mjs diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 01ceca90..8005ccee 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -2883,3 +2883,14 @@ until the current-state gates here are checked with fresh local evidence. workflow and `tools/release/moon.yml`. `check_release_metadata.py` and `check-tooling-stack.sh` now reject reintroducing the Python compatibility entrypoint on the active root Moon surface. +- On 2026-06-28, the `liboliphaunt-wasix` product dry-run moved onto Bun. The + new WASIX release asset checker validates the graph-derived public asset set, + checksum manifest coverage, extension-free portable runtime assets, required + split `pg_dump`/`psql` payloads for tools crates, and the intentional absence + of WASIX `pg_ctl`. Fresh local evidence passed for + `cargo run -p xtask -- release package-assets`, + `tools/dev/bun.sh tools/release/check-liboliphaunt-wasix-release-assets.mjs`, + `tools/dev/bun.sh tools/release/release-product-dry-run.mjs --product liboliphaunt-wasix --allow-dirty`, + `tools/dev/bun.sh tools/release/check-release-metadata.mjs`, + `tools/dev/bun.sh tools/release/check_artifact_targets.mjs`, and + `bash tools/policy/check-tooling-stack.sh`. diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 8f0fe95e..44fe8827 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -59,6 +59,7 @@ 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/release/check-liboliphaunt-wasix-release-assets.mjs require_file tools/dev/bun.sh require_file tools/dev/deno.sh require_file tools/dev/install-actionlint.sh @@ -439,6 +440,16 @@ grep -Fq 'brokerNpmTarballs' tools/release/release-product-dry-run.mjs || fail "Bun Broker product dry-run must validate broker npm tarball artifacts" grep -Fq 'tools/release/package_broker_cargo_artifacts.mjs' tools/release/release-product-dry-run.mjs || fail "Bun Broker product dry-run must generate broker Cargo artifact crates" +grep -Fq 'WASIX_PRODUCT,' tools/release/release-product-dry-run.mjs || + fail "release product dry-run bridge must include liboliphaunt-wasix in Bun-owned product dry-runs" +grep -Fq 'ensureWasixReleaseAssets' tools/release/release-product-dry-run.mjs || + fail "Bun WASIX runtime product dry-run must validate staged WASIX release assets" +grep -Fq 'tools/release/check-liboliphaunt-wasix-release-assets.mjs' tools/release/release-product-dry-run.mjs || + fail "Bun WASIX runtime product dry-run must use the WASIX release asset checker" +grep -Fq 'tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs' tools/release/release-product-dry-run.mjs || + fail "Bun WASIX runtime product dry-run must generate WASIX Cargo artifact crates" +grep -Fq 'validateWasixCargoArtifacts' tools/release/release-product-dry-run.mjs || + fail "Bun WASIX runtime product dry-run must validate generated Cargo artifact manifest rows" grep -Fq 'NODE_DIRECT_PRODUCT,' tools/release/release-product-dry-run.mjs || fail "release product dry-run bridge must include Node direct in Bun-owned product dry-runs" grep -Fq 'ensureNodeDirectReleaseAssets' tools/release/release-product-dry-run.mjs || diff --git a/tools/release/check-liboliphaunt-wasix-release-assets.mjs b/tools/release/check-liboliphaunt-wasix-release-assets.mjs new file mode 100644 index 00000000..9bc92291 --- /dev/null +++ b/tools/release/check-liboliphaunt-wasix-release-assets.mjs @@ -0,0 +1,435 @@ +#!/usr/bin/env bun +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { + existsSync, + readdirSync, + readFileSync, + statSync, +} from "node:fs"; +import path from "node:path"; + +import { + ROOT, + compareText, + currentProductVersionSync, + expectedAssetRows, +} from "./release-artifact-targets.mjs"; + +const TOOL = "check-liboliphaunt-wasix-release-assets.mjs"; +const PRODUCT = "liboliphaunt-wasix"; +const DEFAULT_ASSET_DIR = "target/oliphaunt-wasix/release-assets"; +const PORTABLE_RUNTIME_ARCHIVE_MEMBER = "target/oliphaunt-wasix/assets/oliphaunt.wasix.tar.zst"; +const PORTABLE_MANIFEST_MEMBER = "target/oliphaunt-wasix/assets/manifest.json"; +const SPLIT_TOOL_PAYLOAD_MEMBERS = new Set([ + "target/oliphaunt-wasix/assets/bin/pg_dump.wasix.wasm", + "target/oliphaunt-wasix/assets/bin/psql.wasix.wasm", +]); +const FORBIDDEN_PORTABLE_ASSET_MEMBERS = new Set([ + "target/oliphaunt-wasix/assets/bin/pg_ctl.wasix.wasm", +]); +const CORE_RUNTIME_MEMBERS = new Set([ + "oliphaunt/bin/initdb", + "oliphaunt/bin/postgres", +]); +const FORBIDDEN_RUNTIME_MEMBERS = new Set([ + "oliphaunt/bin/pg_ctl", + "oliphaunt/bin/pg_dump", + "oliphaunt/bin/psql", +]); + +function fail(message) { + console.error(`${TOOL}: ${message}`); + process.exit(1); +} + +function rel(file) { + const relative = path.relative(ROOT, String(file)); + if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) { + return String(file).split(path.sep).join("/"); + } + return relative.split(path.sep).join("/"); +} + +function isFile(file) { + try { + return statSync(file).isFile(); + } catch { + return false; + } +} + +function isDirectory(file) { + try { + return statSync(file).isDirectory(); + } catch { + return false; + } +} + +function sha256File(file) { + return createHash("sha256").update(readFileSync(file)).digest("hex"); +} + +function runCapture(command, args, { input = undefined, label = `${command} ${args.join(" ")}` } = {}) { + const result = spawnSync(command, args, { + cwd: ROOT, + input, + encoding: "buffer", + maxBuffer: 200 * 1024 * 1024, + stdio: ["pipe", "pipe", "pipe"], + }); + if (result.error !== undefined) { + fail(`${label} failed to start: ${result.error.message}`); + } + if (result.status !== 0) { + const stderr = Buffer.isBuffer(result.stderr) ? result.stderr.toString("utf8") : result.stderr; + fail(`${label} failed${stderr ? `: ${stderr.trim()}` : ""}`); + } + return result.stdout; +} + +function normalizeTarMember(member, context) { + const normalized = String(member).replaceAll("\\", "/").replace(/\/+$/u, ""); + const parts = normalized.split("/").filter((part) => part && part !== "."); + if (parts.length === 0 || normalized.startsWith("/") || parts.includes("..")) { + fail(`${context} contains unsafe archive member ${JSON.stringify(member)}`); + } + return parts.join("/"); +} + +function tarZstdMembers(archive) { + const output = runCapture("tar", ["--zstd", "-tf", archive], { + label: `list ${rel(archive)}`, + }).toString("utf8"); + return output + .split(/\r?\n/u) + .map((line) => line.trim()) + .filter(Boolean) + .map((member) => normalizeTarMember(member, rel(archive))); +} + +function tarZstdBufferMembers(data, context) { + const output = runCapture("tar", ["--zstd", "-tf", "-"], { + input: data, + label: `list nested zstd tar for ${context}`, + }).toString("utf8"); + return output + .split(/\r?\n/u) + .map((line) => line.trim()) + .filter(Boolean) + .map((member) => normalizeTarMember(member, context)); +} + +function findTarZstdMember(archive, expected) { + for (const member of tarZstdMembers(archive)) { + if (member === expected) { + return member; + } + } + return null; +} + +function readTarZstdMember(archive, expected) { + const member = findTarZstdMember(archive, expected); + if (member === null) { + fail(`${rel(archive)} is missing ${expected}`); + } + return runCapture("tar", ["--zstd", "-xOf", archive, member], { + label: `read ${expected} from ${rel(archive)}`, + }); +} + +function readTarZstdJsonMember(archive, expected) { + const data = readTarZstdMember(archive, expected).toString("utf8"); + try { + return JSON.parse(data); + } catch (error) { + fail(`${rel(archive)} ${expected} is not valid JSON: ${error.message}`); + } +} + +function simpleRelativePath(value, context) { + if (typeof value !== "string" || value.length === 0) { + fail(`${context} must be a non-empty string`); + } + const normalized = value.replaceAll("\\", "/").replace(/\/+$/u, ""); + const parts = normalized.split("/"); + if (normalized.startsWith("/") || parts.some((part) => !part || part === "." || part === "..")) { + fail(`${context} path must be a simple relative path, got ${JSON.stringify(value)}`); + } + return normalized; +} + +function expectedParentDirs(paths) { + const parents = new Set(); + for (const item of paths) { + const parts = item.split("/"); + for (let index = 1; index < parts.length; index += 1) { + parents.add(parts.slice(0, index).join("/")); + } + } + return parents; +} + +function parseChecksumManifest(file) { + const checksums = new Map(); + for (const [index, rawLine] of readFileSync(file, "utf8").split(/\r?\n/u).entries()) { + const line = rawLine.trim(); + if (!line) { + continue; + } + const match = line.match(/^([0-9a-f]{64}) \.\/([^/]+)$/u); + if (match === null) { + fail(`${rel(file)}:${index + 1} must use ' ./' entries`); + } + const [, sha256, assetName] = match; + if (checksums.has(assetName)) { + fail(`${rel(file)}:${index + 1} declares duplicate checksum for ${assetName}`); + } + checksums.set(assetName, sha256); + } + return checksums; +} + +function expectedAssetNames(version) { + return expectedAssetRows({ product: PRODUCT, version }, TOOL) + .map((row) => row.assetName) + .sort(compareText); +} + +function validateAssetSet(assetDir, version) { + const expected = new Set(expectedAssetNames(version)); + const actual = new Set( + readdirSync(assetDir) + .map((name) => path.join(assetDir, name)) + .filter(isFile) + .map((file) => path.basename(file)) + .sort(compareText), + ); + if (JSON.stringify([...actual].sort(compareText)) !== JSON.stringify([...expected].sort(compareText))) { + fail( + `${PRODUCT} staged release assets must match release metadata exactly: ` + + `expected=${JSON.stringify([...expected].sort(compareText))}, actual=${JSON.stringify([...actual].sort(compareText))}`, + ); + } + + const checksumName = `${PRODUCT}-${version}-release-assets.sha256`; + const checksumPath = path.join(assetDir, checksumName); + if (!isFile(checksumPath)) { + fail(`${PRODUCT} staged release assets are missing checksum manifest ${checksumName}`); + } + const checksums = parseChecksumManifest(checksumPath); + const expectedChecksumAssets = new Set([...expected].filter((name) => name !== checksumName)); + const actualChecksumAssets = new Set(checksums.keys()); + if ( + JSON.stringify([...actualChecksumAssets].sort(compareText)) !== + JSON.stringify([...expectedChecksumAssets].sort(compareText)) + ) { + fail( + `${PRODUCT} checksum manifest must cover release assets exactly: ` + + `expected=${JSON.stringify([...expectedChecksumAssets].sort(compareText))}, ` + + `actual=${JSON.stringify([...actualChecksumAssets].sort(compareText))}`, + ); + } + for (const [assetName, expectedSha] of checksums) { + const actualSha = sha256File(path.join(assetDir, assetName)); + if (actualSha !== expectedSha) { + fail(`${PRODUCT} release asset ${assetName} checksum mismatch`); + } + } +} + +function validatePortableReleaseAsset(archive) { + const members = new Set(tarZstdMembers(archive)); + const extensionMembers = [...members] + .filter((member) => member.startsWith("target/oliphaunt-wasix/assets/extensions/")) + .sort(compareText); + if (extensionMembers.length > 0) { + fail(`${rel(archive)} must not contain extension payloads: ${extensionMembers.slice(0, 5).join(", ")}`); + } + const missingToolPayloads = [...SPLIT_TOOL_PAYLOAD_MEMBERS] + .filter((member) => !members.has(member)) + .sort(compareText); + if (missingToolPayloads.length > 0) { + fail(`${rel(archive)} must include split WASIX tool payloads for registry tools crates: ${missingToolPayloads.join(", ")}`); + } + const forbiddenPortableMembers = [...members] + .filter((member) => FORBIDDEN_PORTABLE_ASSET_MEMBERS.has(member)) + .sort(compareText); + if (forbiddenPortableMembers.length > 0) { + fail(`${rel(archive)} must not contain WASIX pg_ctl payloads: ${forbiddenPortableMembers.join(", ")}`); + } + + const manifest = readTarZstdJsonMember(archive, PORTABLE_MANIFEST_MEMBER); + if (JSON.stringify(manifest.extensions) !== "[]") { + fail(`${rel(archive)} asset manifest must contain an empty extensions array`); + } + for (const key of ["pg-dump", "psql"]) { + if (Object.hasOwn(manifest, key)) { + fail(`${rel(archive)} asset manifest must not contain split WASIX tool entry ${key}`); + } + } + + const icuSidecarMembers = [...members] + .filter((member) => member === "target/oliphaunt-wasix/icu" || member.startsWith("target/oliphaunt-wasix/icu/")) + .sort(compareText); + if (icuSidecarMembers.length > 0) { + fail(`${rel(archive)} must not contain ICU data sidecar files: ${icuSidecarMembers.slice(0, 5).join(", ")}`); + } + + const runtimeArchive = readTarZstdMember(archive, PORTABLE_RUNTIME_ARCHIVE_MEMBER); + const runtimeMembers = new Set(tarZstdBufferMembers(runtimeArchive, "WASIX runtime archive")); + const missing = [...CORE_RUNTIME_MEMBERS] + .filter((member) => !runtimeMembers.has(member)) + .sort(compareText); + if (missing.length > 0) { + fail(`${rel(archive)} must bundle core WASIX runtime binaries inside ${PORTABLE_RUNTIME_ARCHIVE_MEMBER}: ${missing.join(", ")}`); + } + const bundledIcu = [...runtimeMembers] + .filter((member) => member === "oliphaunt/share/icu" || member.startsWith("oliphaunt/share/icu/")) + .sort(compareText); + if (bundledIcu.length > 0) { + fail(`${rel(archive)} must not bundle ICU data inside ${PORTABLE_RUNTIME_ARCHIVE_MEMBER}: ${bundledIcu.slice(0, 5).join(", ")}`); + } + const bundledTools = [...runtimeMembers] + .filter((member) => FORBIDDEN_RUNTIME_MEMBERS.has(member)) + .sort(compareText); + if (bundledTools.length > 0) { + fail(`${rel(archive)} must not bundle standalone tools inside ${PORTABLE_RUNTIME_ARCHIVE_MEMBER}: ${bundledTools.join(", ")}`); + } +} + +function validateIcuReleaseAsset(archive) { + const members = new Set(tarZstdMembers(archive)); + const icuRoot = "target/oliphaunt-wasix/icu/share/icu"; + const icuEntries = [...members] + .filter((member) => { + if (!member.startsWith(`${icuRoot}/`)) { + return false; + } + const relative = member.slice(`${icuRoot}/`.length).split("/").filter(Boolean); + return relative.length > 0 && relative[0].startsWith("icudt"); + }) + .sort(compareText); + if (icuEntries.length === 0) { + fail(`${rel(archive)} must contain ICU data files under ${icuRoot}`); + } + const parentDirs = expectedParentDirs(new Set(icuEntries)); + const unexpected = [...members] + .filter((member) => !parentDirs.has(member) && !member.startsWith(`${icuRoot}/`)) + .sort(compareText); + if (unexpected.length > 0) { + fail(`${rel(archive)} contains unexpected non-ICU files: ${unexpected.slice(0, 5).join(", ")}`); + } +} + +function validateAotReleaseAsset(archive) { + const members = new Set(tarZstdMembers(archive)); + const manifestMembers = [...members] + .filter((member) => member.startsWith("target/oliphaunt-wasix/aot/") && member.endsWith("/manifest.json")) + .sort(compareText); + if (manifestMembers.length !== 1) { + fail(`${rel(archive)} must contain exactly one AOT manifest, got ${JSON.stringify(manifestMembers)}`); + } + const manifestPath = manifestMembers[0]; + const aotRoot = manifestPath.slice(0, -"/manifest.json".length); + const manifest = readTarZstdJsonMember(archive, manifestPath); + if (!Array.isArray(manifest.artifacts) || manifest.artifacts.length === 0) { + fail(`${rel(archive)} AOT manifest must contain artifacts`); + } + + const expectedFiles = new Set([manifestPath]); + for (const artifact of manifest.artifacts) { + if (artifact === null || Array.isArray(artifact) || typeof artifact !== "object") { + fail(`${rel(archive)} AOT manifest contains a non-object artifact`); + } + const name = artifact.name; + if (typeof name !== "string" || name.length === 0) { + fail(`${rel(archive)} AOT manifest contains an artifact without a name`); + } + if (name.startsWith("extension:")) { + fail(`${rel(archive)} must not contain extension AOT artifact ${name}`); + } + expectedFiles.add(`${aotRoot}/${simpleRelativePath(artifact.path, `${rel(archive)} AOT artifact ${name}`)}`); + } + + const parentDirs = expectedParentDirs(expectedFiles); + const actualFiles = new Set([...members].filter((member) => !parentDirs.has(member))); + if ( + JSON.stringify([...actualFiles].sort(compareText)) !== + JSON.stringify([...expectedFiles].sort(compareText)) + ) { + fail( + `${rel(archive)} AOT file set mismatch: ` + + `expected ${JSON.stringify([...expectedFiles].sort(compareText))}, got ${JSON.stringify([...actualFiles].sort(compareText))}`, + ); + } +} + +function validateAssetContents(assetDir, version) { + validatePortableReleaseAsset(path.join(assetDir, `${PRODUCT}-${version}-runtime-portable.tar.zst`)); + validateIcuReleaseAsset(path.join(assetDir, `${PRODUCT}-${version}-icu-data.tar.zst`)); + const aotArchives = readdirSync(assetDir) + .filter((name) => name.startsWith(`${PRODUCT}-${version}-runtime-aot-`) && name.endsWith(".tar.zst")) + .map((name) => path.join(assetDir, name)) + .sort(compareText); + if (aotArchives.length === 0) { + fail(`${PRODUCT} release assets are missing target AOT archives`); + } + for (const archive of aotArchives) { + validateAotReleaseAsset(archive); + } +} + +function usage() { + console.log(`usage: tools/release/check-liboliphaunt-wasix-release-assets.mjs [--asset-dir DIR] [--version VERSION] + +Validates staged liboliphaunt-wasix GitHub release assets, their checksum +manifest, and runtime/ICU/AOT archive boundaries. +`); +} + +function optionValue(argv, index) { + const value = argv[index + 1]; + if (value === undefined || value.startsWith("--")) { + usage(); + fail(`${argv[index]} requires a value`); + } + return value; +} + +function parseArgs(argv) { + const args = { + assetDir: DEFAULT_ASSET_DIR, + version: null, + }; + for (let index = 0; index < argv.length;) { + const arg = argv[index]; + if (arg === "--asset-dir") { + args.assetDir = optionValue(argv, index); + index += 2; + } else if (arg === "--version") { + args.version = optionValue(argv, index); + index += 2; + } else if (arg === "-h" || arg === "--help") { + usage(); + process.exit(0); + } else { + usage(); + fail(`unknown argument ${arg}`); + } + } + return { + assetDir: path.isAbsolute(args.assetDir) ? args.assetDir : path.join(ROOT, args.assetDir), + version: args.version ?? currentProductVersionSync(PRODUCT, TOOL), + }; +} + +const args = parseArgs(Bun.argv.slice(2)); +if (!existsSync(args.assetDir) || !isDirectory(args.assetDir)) { + fail(`${PRODUCT} release asset directory does not exist: ${rel(args.assetDir)}`); +} +validateAssetSet(args.assetDir, args.version); +validateAssetContents(args.assetDir, args.version); +console.log(`validated ${PRODUCT} staged release assets under ${rel(args.assetDir)}`); diff --git a/tools/release/check_artifact_targets.mjs b/tools/release/check_artifact_targets.mjs index 6b805ef6..a9ffde90 100644 --- a/tools/release/check_artifact_targets.mjs +++ b/tools/release/check_artifact_targets.mjs @@ -1172,6 +1172,31 @@ function validateCiReleaseArtifacts() { "brokerNpmTarballs", "Broker product dry-run must validate staged broker npm tarballs in Bun", ); + requireText( + "tools/release/release-product-dry-run.mjs", + "runWasixRuntimeDryRun", + "liboliphaunt-wasix product dry-run must validate staged WASIX release assets and Cargo artifacts in Bun", + ); + requireText( + "tools/release/release-product-dry-run.mjs", + "tools/release/check-liboliphaunt-wasix-release-assets.mjs", + "liboliphaunt-wasix product dry-run must use the Bun WASIX release asset checker", + ); + requireText( + "tools/release/check-liboliphaunt-wasix-release-assets.mjs", + "expectedAssetRows({ product: PRODUCT, version }", + "WASIX release asset checker must derive expected assets from release metadata", + ); + requireText( + "tools/release/check-liboliphaunt-wasix-release-assets.mjs", + "SPLIT_TOOL_PAYLOAD_MEMBERS", + "WASIX release asset checker must require pg_dump/psql payloads for split tools crates", + ); + requireText( + "tools/release/check-liboliphaunt-wasix-release-assets.mjs", + "FORBIDDEN_PORTABLE_ASSET_MEMBERS", + "WASIX release asset checker must reject pg_ctl payloads from portable assets", + ); requireText( "tools/release/release-product-dry-run.mjs", "runExtensionDryRun", diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index a372423e..e8543f22 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1009,6 +1009,11 @@ def validate_publish_target_coverage() -> None: or "ensureBrokerReleaseAssets" not in release_product_dry_run or "brokerNpmTarballs" not in release_product_dry_run or "tools/release/package_broker_cargo_artifacts.mjs" not in release_product_dry_run + or "WASIX_PRODUCT," not in release_product_dry_run + or "ensureWasixReleaseAssets" not in release_product_dry_run + or "tools/release/check-liboliphaunt-wasix-release-assets.mjs" not in release_product_dry_run + or "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs" not in release_product_dry_run + or "validateWasixCargoArtifacts" not in release_product_dry_run or "NODE_DIRECT_PRODUCT," not in release_product_dry_run or "ensureNodeDirectReleaseAssets" not in release_product_dry_run or "nodeDirectOptionalNpmTarballs" not in release_product_dry_run diff --git a/tools/release/release-product-dry-run.mjs b/tools/release/release-product-dry-run.mjs index c21e53ef..a84ebf6c 100644 --- a/tools/release/release-product-dry-run.mjs +++ b/tools/release/release-product-dry-run.mjs @@ -25,11 +25,16 @@ import { currentProductVersionSync, exactExtensionProducts, } from "./release-artifact-targets.mjs"; +import { + WASIX_CARGO_ARTIFACT_SCHEMA, + publicCargoPackageNames as wasixPublicCargoPackageNames, +} from "./wasix-cargo-artifact-contract.mjs"; const TOOL = "release-product-dry-run.mjs"; const BROKER_PRODUCT = "oliphaunt-broker"; const BROKER_KIND = "broker-helper"; const BROKER_PACKAGE_ROOT = path.join(ROOT, "src/runtimes/broker/packages"); +const WASIX_PRODUCT = "liboliphaunt-wasix"; const NODE_DIRECT_PRODUCT = "oliphaunt-node-direct"; const NODE_DIRECT_KIND = "node-direct-addon"; const NODE_DIRECT_PACKAGE_ROOT = path.join(ROOT, "src/runtimes/node-direct/packages"); @@ -38,6 +43,7 @@ export const SUPPORTED_BUN_PRODUCT_DRY_RUNS = new Set([ ...SUPPORTED_SDK_PRODUCT_DRY_RUNS, ...exactExtensionProducts(TOOL), BROKER_PRODUCT, + WASIX_PRODUCT, NODE_DIRECT_PRODUCT, ]); @@ -169,6 +175,15 @@ function hasBrokerReleaseArchive(assetDir) { ); } +function hasWasixReleaseArchive(assetDir) { + if (!isDirectory(assetDir)) { + return false; + } + return readdirSync(assetDir).some((name) => + name.startsWith("liboliphaunt-wasix-") && name.endsWith(".tar.zst"), + ); +} + function ensureBrokerReleaseAssets() { const assetDir = path.join(ROOT, "target/oliphaunt-broker/release-assets"); if (!hasBrokerReleaseArchive(assetDir)) { @@ -200,6 +215,37 @@ function ensureBrokerReleaseAssets() { ]); } +function ensureWasixReleaseAssets() { + const assetDir = path.join(ROOT, "target/oliphaunt-wasix/release-assets"); + if (!hasWasixReleaseArchive(assetDir)) { + copyStagedRuntimeAssets({ + product: WASIX_PRODUCT, + destination: assetDir, + envName: "OLIPHAUNT_WASIX_RELEASE_ASSET_INPUT_DIRS", + patterns: ["liboliphaunt-wasix-*.tar.zst"], + }); + } + const version = currentProductVersionSync(WASIX_PRODUCT, TOOL); + run(TOOL, [ + "tools/dev/bun.sh", + "tools/release/write_checksum_manifest.mjs", + "--asset-dir", + rel(assetDir), + "--output", + `liboliphaunt-wasix-${version}-release-assets.sha256`, + "--pattern", + "liboliphaunt-wasix-*.tar.zst", + ]); + run(TOOL, [ + "tools/dev/bun.sh", + "tools/release/check-liboliphaunt-wasix-release-assets.mjs", + "--asset-dir", + rel(assetDir), + "--version", + version, + ]); +} + function ensureNodeDirectReleaseAssets() { const assetDir = path.join(ROOT, "target/oliphaunt-node-direct/release-assets"); if (!hasNodeDirectReleaseArchive(assetDir)) { @@ -650,6 +696,107 @@ function runBrokerDryRun() { ]); } +function isExpectedWasixExtensionPackage(name, kind) { + if (kind === "wasix-extension") { + return exactExtensionProducts(TOOL).some((product) => name === `${product}-wasix`); + } + if (kind === "wasix-extension-aot") { + return exactExtensionProducts(TOOL).some((product) => name.startsWith(`${product}-wasix-aot-`)); + } + return false; +} + +function validateWasixCargoArtifacts(outputDir) { + const manifestPath = path.join(outputDir, "packages.json"); + if (!isFile(manifestPath)) { + fail(`missing generated ${WASIX_PRODUCT} Cargo artifact manifest: ${rel(manifestPath)}`); + } + let data; + try { + data = JSON.parse(readFileSync(manifestPath, "utf8")); + } catch (error) { + fail(`${rel(manifestPath)} is not valid JSON: ${error.message}`); + } + if (data?.schema !== WASIX_CARGO_ARTIFACT_SCHEMA || !Array.isArray(data.packages)) { + fail(`${rel(manifestPath)} has an invalid WASIX Cargo artifact schema`); + } + + const expectedBaseCrates = new Set(wasixPublicCargoPackageNames()); + const generatedCrates = new Set(); + const expectedCratePaths = new Set(); + const allowedKinds = new Set([ + "wasix-runtime", + "wasix-tools", + "wasix-aot", + "wasix-tools-aot", + "icu-data", + "wasix-extension", + "wasix-extension-aot", + ]); + for (const item of data.packages) { + if (item === null || Array.isArray(item) || typeof item !== "object") { + fail(`${rel(manifestPath)} package entries must be objects`); + } + const { name, role, kind, manifestPath: rawManifest, cratePath: rawCrate } = item; + if (![name, role, kind, rawManifest].every((value) => typeof value === "string" && value.length > 0)) { + fail(`${rel(manifestPath)} has an invalid package row: ${JSON.stringify(item)}`); + } + if (role !== "artifact") { + fail(`${rel(manifestPath)} must contain direct WASIX artifact packages, got role ${JSON.stringify(role)}`); + } + if (!allowedKinds.has(kind)) { + fail(`${rel(manifestPath)} has unsupported WASIX Cargo artifact kind ${JSON.stringify(kind)}`); + } + if (!expectedBaseCrates.has(name) && !isExpectedWasixExtensionPackage(name, kind)) { + fail(`unexpected ${WASIX_PRODUCT} Cargo artifact crate ${name}`); + } + const sourceManifest = path.join(ROOT, rawManifest); + if (!isFile(sourceManifest)) { + fail(`missing generated ${WASIX_PRODUCT} Cargo source manifest: ${rawManifest}`); + } + if (typeof rawCrate !== "string" || rawCrate.length === 0) { + fail(`generated ${WASIX_PRODUCT} Cargo artifact ${name} must have a cratePath`); + } + const cratePath = path.join(ROOT, rawCrate); + if (!isFile(cratePath)) { + fail(`missing generated ${WASIX_PRODUCT} Cargo artifact crate for ${name}: ${rawCrate}`); + } + generatedCrates.add(name); + expectedCratePaths.add(path.resolve(cratePath)); + } + + const missingBaseCrates = [...expectedBaseCrates] + .filter((name) => !generatedCrates.has(name)) + .sort(compareText); + if (missingBaseCrates.length > 0) { + fail(`generated ${WASIX_PRODUCT} Cargo artifacts are missing configured runtime crates: ${missingBaseCrates.join(", ")}`); + } + const unexpected = readdirSync(outputDir) + .filter((name) => name.endsWith(".crate")) + .map((name) => path.join(outputDir, name)) + .filter((file) => !expectedCratePaths.has(path.resolve(file))) + .map((file) => path.basename(file)) + .sort(compareText); + if (unexpected.length > 0) { + fail(`unexpected ${WASIX_PRODUCT} Cargo artifact crate(s): ${unexpected.join(", ")}`); + } +} + +function runWasixRuntimeDryRun() { + const version = currentProductVersionSync(WASIX_PRODUCT, TOOL); + const outputDir = path.join(ROOT, "target/oliphaunt-wasix/cargo-artifacts"); + ensureWasixReleaseAssets(); + run(TOOL, [ + "tools/dev/bun.sh", + "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs", + "--version", + version, + "--output-dir", + rel(outputDir), + ]); + validateWasixCargoArtifacts(outputDir); +} + function extensionPackageDir(product) { return path.join(ROOT, "target/extension-artifacts", product); } @@ -740,6 +887,10 @@ export async function runBunProductDryRun(product, { allowDirty = false } = {}) runBrokerDryRun(); return; } + if (product === WASIX_PRODUCT) { + runWasixRuntimeDryRun(); + return; + } if (product === NODE_DIRECT_PRODUCT) { await runNodeDirectDryRun(); return; diff --git a/tools/xtask/src/release_workspace.rs b/tools/xtask/src/release_workspace.rs index c5114f8d..98235297 100644 --- a/tools/xtask/src/release_workspace.rs +++ b/tools/xtask/src/release_workspace.rs @@ -378,7 +378,7 @@ pub(super) fn package_release_assets() -> Result<()> { bundle.display() ) })?; - checksum_lines.push(format!("{} {name}", sha256_file(bundle)?)); + checksum_lines.push(format!("{} ./{name}", sha256_file(bundle)?)); } checksum_lines.sort(); let checksum_path = output_dir.join(format!( From b7bc7c902e2898ca1576c336bd7b0d7ecfd8342c Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 07:17:16 +0000 Subject: [PATCH 277/308] chore: route wasm publish dry run through bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 19 ++++- docs/maintainers/development.md | 5 +- ...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 +- tools/release/release-publish.mjs | 34 ++++++-- 8 files changed, 101 insertions(+), 56 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 8005ccee..ddf116e0 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -161,13 +161,26 @@ until the current-state gates here are checked with fresh local evidence. no-product `publish-dry-run` path so passthrough-only invocations such as `--head-ref HEAD` run `release-check.mjs` and then `release-check-registries.mjs` directly without launching the protected - `release.py` implementation. Product-selected dry-runs, WASIX dry-runs, and - protected publish dispatch still delegate to `release.py` until those package - validation paths are ported. Fresh evidence: + `release.py` implementation. At this checkpoint, product-selected dry-runs, + the legacy WASIX shortcut, and protected publish dispatch still delegated to + `release.py` until those package validation paths were ported. Fresh evidence: `tools/dev/bun.sh tools/release/release-publish.mjs publish-dry-run --head-ref HEAD`, `tools/dev/bun.sh tools/release/check-release-metadata.mjs`, `bash tools/policy/check-tooling-stack.sh`, `bash tools/policy/check-docs.sh`, and `git diff --check`. +- 2026-06-28: Moved the legacy + `tools/release/release-publish.mjs publish-dry-run --wasm` shortcut onto the + Bun release-publish path. The route now runs `release-check.mjs`, any + passthrough registry checks, and the existing `oliphaunt-wasix-rust` SDK + product dry-run without launching `release.py`; product-selected dry-runs + still take precedence over the legacy shortcut, matching the Python parser's + behavior. Fresh evidence: + `tools/dev/bun.sh tools/release/release-publish.mjs publish-dry-run --wasm --allow-dirty`, + `tools/dev/bun.sh tools/release/check-release-metadata.mjs`, + `bash tools/policy/check-tooling-stack.sh`, `bash tools/policy/check-docs.sh`, + and `git diff --check`. The first run also caught stale release PR derived + digest/evidence files, which were refreshed through + `tools/dev/bun.sh tools/release/sync-release-pr.mjs`. - 2026-06-28: Updated current maintainer tooling docs so protected publishing guidance names the Bun `tools/release/release-publish.mjs` command surface and treats `tools/release/release.py` only as a temporary protected implementation diff --git a/docs/maintainers/development.md b/docs/maintainers/development.md index 3647d5e3..3a99824e 100644 --- a/docs/maintainers/development.md +++ b/docs/maintainers/development.md @@ -160,9 +160,8 @@ The validation entrypoint is split by maintainer workflow: - `moon run :check && moon run :test && moon run :smoke`: fast contributor lane for repo, lint, source tests, and examples; - `moon run :regression`: broader SQL, protocol, extension, and runtime regression suites; -- `tools/dev/bun.sh tools/release/release-publish.mjs publish-dry-run --wasm`: release-workspace package checks plus publish - dry-runs for internal crates after CI-generated AOT artifacts have been - downloaded. +- `tools/dev/bun.sh tools/release/release-publish.mjs publish-dry-run --wasm`: Bun-owned WASIX Rust SDK publish + dry-run after CI-generated WASIX/AOT and SDK artifacts have been downloaded. Moon caches deterministic task results when their declared source inputs and task dependencies have not changed. Local `:smoke` targets use `cache: local`, 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 4efa115e..372af6c6 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:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4", + "sourceDigest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2", "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 c696c0cb..d04afb40 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:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -56,7 +56,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -92,7 +92,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -128,7 +128,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -164,7 +164,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -200,7 +200,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -236,7 +236,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -272,7 +272,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -308,7 +308,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -344,7 +344,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -380,7 +380,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -416,7 +416,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -452,7 +452,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -488,7 +488,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -524,7 +524,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -560,7 +560,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -596,7 +596,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -632,7 +632,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -668,7 +668,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -704,7 +704,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -740,7 +740,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -776,7 +776,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -812,7 +812,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -848,7 +848,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -884,7 +884,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -920,7 +920,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -956,7 +956,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -992,7 +992,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -1028,7 +1028,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -1064,7 +1064,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -1100,7 +1100,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -1136,7 +1136,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -1172,7 +1172,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -1208,7 +1208,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -1244,7 +1244,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -1280,7 +1280,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -1316,7 +1316,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -1352,7 +1352,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -1388,7 +1388,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4" + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" } ], "platform-targets": [ @@ -1420,7 +1420,7 @@ "path": "src/extensions/evidence/runs" } ], - "source-digest": "sha256:00b3f5a8122d09c441c0c8b70253f32401cb7b82b95afa5fc43eff8ab142c8d4", + "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2", "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 aa809b8d..720513b7 100644 --- a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 +++ b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 @@ -1 +1 @@ -9cbe9b35eaa955c2f205314933cf7c5aeaa6ce0638089378a261326e15851f22 +8dc087fc0c529f19e1151a544d2257e13328df10fc9e7b9a1d3bc2a2c7cc41a9 diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 44fe8827..81541102 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -430,6 +430,15 @@ grep -Fq 'SUPPORTED_BUN_PRODUCT_DRY_RUNS' tools/release/release-publish.mjs || fail "release publish dry-run wrapper must import the Bun product dry-run support set" grep -Fq 'await runBunProductDryRun(product, { allowDirty: productDryRunPlan.allowDirty });' tools/release/release-publish.mjs || fail "release publish dry-run wrapper must execute supported product dry-runs in Bun" +grep -Fq 'function legacyWasmPublishDryRunPlan(' tools/release/release-publish.mjs || + fail "release publish dry-run wrapper must own the legacy --wasm dry-run path in Bun" +grep -Fq 'LEGACY_WASM_DRY_RUN_PRODUCT = "oliphaunt-wasix-rust"' tools/release/release-publish.mjs || + fail "legacy --wasm publish dry-run must map to the WASIX Rust SDK product" +grep -Fq 'await runBunProductDryRun(legacyWasmDryRunPlan.product, { allowDirty: legacyWasmDryRunPlan.allowDirty });' tools/release/release-publish.mjs || + fail "legacy --wasm publish dry-run must execute the WASIX Rust SDK dry-run in Bun" +if grep -Fq -- '--wasm dry-runs, and protected publish dispatch still delegate to release.py' tools/release/release-publish.mjs; then + fail "release-publish must not describe legacy --wasm dry-runs as delegated to release.py" +fi grep -Fq 'SUPPORTED_SDK_PRODUCT_DRY_RUNS' tools/release/release-product-dry-run.mjs || fail "release product dry-run bridge must preserve SDK helper ownership" grep -Fq 'BROKER_PRODUCT,' tools/release/release-product-dry-run.mjs || diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index e8543f22..5156175d 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1004,6 +1004,10 @@ def validate_publish_target_coverage() -> None: or 'run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check-registries.mjs", ...passthrough]);' not in release_publish or "SUPPORTED_BUN_PRODUCT_DRY_RUNS" not in release_publish or 'await runBunProductDryRun(product, { allowDirty: productDryRunPlan.allowDirty });' not in release_publish + or "function legacyWasmPublishDryRunPlan(" not in release_publish + or 'LEGACY_WASM_DRY_RUN_PRODUCT = "oliphaunt-wasix-rust"' not in release_publish + or 'await runBunProductDryRun(legacyWasmDryRunPlan.product, { allowDirty: legacyWasmDryRunPlan.allowDirty });' not in release_publish + or "--wasm dry-runs, and protected publish dispatch still delegate to release.py" in release_publish or "SUPPORTED_SDK_PRODUCT_DRY_RUNS" not in release_product_dry_run or "BROKER_PRODUCT," not in release_product_dry_run or "ensureBrokerReleaseAssets" not in release_product_dry_run @@ -1031,7 +1035,7 @@ def validate_publish_target_coverage() -> None: or "prepareOliphauntWasixReleaseSource" not in release_sdk_product_dry_run or 'spawnSync("tools/release/release.py", argv' not in release_publish ): - fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product publish dry-runs must run release-check and passthrough registry checks without launching release.py, and low-risk product dry-runs must stay in Bun") + fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product and legacy --wasm publish dry-runs must run through Bun without launching release.py, and low-risk product dry-runs must stay in Bun") 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 diff --git a/tools/release/release-publish.mjs b/tools/release/release-publish.mjs index 28fbf170..4659320b 100755 --- a/tools/release/release-publish.mjs +++ b/tools/release/release-publish.mjs @@ -15,9 +15,10 @@ function usage() { Runs protected release publish and publish dry-run operations through the Bun release command surface. The public no-product publish dry-run and selected -low-risk product dry-runs are handled in Bun; other product dry-runs, legacy ---wasm dry-runs, and protected publish dispatch still delegate to release.py -while the protected implementation is ported. +low-risk product dry-runs are handled in Bun, including the legacy --wasm +shortcut for the WASIX Rust SDK dry-run. Other product dry-runs and protected +publish dispatch still delegate to release.py while the protected +implementation is ported. `); } @@ -28,6 +29,7 @@ function fail(message, exitCode = 2) { const argv = Bun.argv.slice(2); const command = argv[0]; +const LEGACY_WASM_DRY_RUN_PRODUCT = "oliphaunt-wasix-rust"; if (command === "-h" || command === "--help") { usage(); @@ -70,6 +72,17 @@ function noProductPublishDryRunPassthrough(args) { return args.filter((arg) => arg !== "--allow-dirty"); } +function legacyWasmPublishDryRunPlan(args) { + if (!args.includes("--wasm") || selectsProducts(args)) { + return null; + } + return { + allowDirty: args.includes("--allow-dirty"), + passthrough: args.filter((arg) => arg !== "--allow-dirty" && arg !== "--wasm"), + product: LEGACY_WASM_DRY_RUN_PRODUCT, + }; +} + function jsonOutput(args) { const result = spawnSync("tools/dev/bun.sh", args, { cwd: ROOT, @@ -86,9 +99,6 @@ function jsonOutput(args) { } function productPublishDryRunPlan(args) { - if (args.includes("--wasm")) { - return null; - } const productsJson = flagValue(args, "--products-json"); if (productsJson === null) { return null; @@ -119,7 +129,7 @@ function productPublishDryRunPlan(args) { } return { allowDirty: args.includes("--allow-dirty"), - passthrough: args.filter((arg) => arg !== "--allow-dirty"), + passthrough: args.filter((arg) => arg !== "--allow-dirty" && arg !== "--wasm"), products: ordered, }; } @@ -143,6 +153,16 @@ if (productDryRunPlan !== null) { process.exit(0); } +const legacyWasmDryRunPlan = command === "publish-dry-run" ? legacyWasmPublishDryRunPlan(argv.slice(1)) : null; +if (legacyWasmDryRunPlan !== null) { + run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check.mjs"]); + if (legacyWasmDryRunPlan.passthrough.length > 0) { + run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check-registries.mjs", ...legacyWasmDryRunPlan.passthrough]); + } + await runBunProductDryRun(legacyWasmDryRunPlan.product, { allowDirty: legacyWasmDryRunPlan.allowDirty }); + process.exit(0); +} + const result = spawnSync("tools/release/release.py", argv, { cwd: ROOT, stdio: "inherit", From d5f99a8339875d43ffbc06600cc26dd6eefcb5e6 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 07:31:51 +0000 Subject: [PATCH 278/308] fix: keep wasix tools out of root artifact staging --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 6 + .../rust/crates/oliphaunt-build/README.md | 7 + .../rust/crates/oliphaunt-build/src/lib.rs | 256 ++++++++++++++++-- tools/release/check_consumer_shape.py | 14 + tools/release/check_release_metadata.py | 8 + 5 files changed, 271 insertions(+), 20 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index ddf116e0..70a47439 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -2907,3 +2907,9 @@ until the current-state gates here are checked with fresh local evidence. `tools/dev/bun.sh tools/release/check-release-metadata.mjs`, `tools/dev/bun.sh tools/release/check_artifact_targets.mjs`, and `bash tools/policy/check-tooling-stack.sh`. +- On 2026-06-28, `oliphaunt-build` stopped treating WASIX `pg_dump`/`psql` + tools as unconditional runtime artifacts. Root WASIX staging now requires + only `liboliphaunt-wasix` runtime plus AOT manifests; apps that enable the + `oliphaunt-wasix` `tools` feature, or set `[package.metadata.oliphaunt] + tools = true`, stage `oliphaunt-wasix-tools` and tools-AOT separately. + The build-helper tests now cover both root-only and split-tools WASIX staging. diff --git a/src/sdks/rust/crates/oliphaunt-build/README.md b/src/sdks/rust/crates/oliphaunt-build/README.md index f32b6c11..ac571fbd 100644 --- a/src/sdks/rust/crates/oliphaunt-build/README.md +++ b/src/sdks/rust/crates/oliphaunt-build/README.md @@ -17,5 +17,12 @@ validates the selected application metadata, copies the already-resolved artifacts into `OUT_DIR/oliphaunt/resources`, and writes `OUT_DIR/oliphaunt/oliphaunt-assets.lock`. +For `runtime = "liboliphaunt-wasix"`, root runtime staging includes only the +portable runtime and matching AOT runtime artifacts. If the application enables +the `oliphaunt-wasix` `tools` feature, `oliphaunt-build` also stages the split +`oliphaunt-wasix-tools` and tools-AOT artifacts that provide `pg_dump` and +`psql`. Applications that enable tools indirectly can set +`[package.metadata.oliphaunt] tools = true` to make that intent explicit. + It performs no network I/O, does not mutate `Cargo.toml`, and writes no generated files outside `OUT_DIR`. diff --git a/src/sdks/rust/crates/oliphaunt-build/src/lib.rs b/src/sdks/rust/crates/oliphaunt-build/src/lib.rs index be092c8f..f8408b9a 100644 --- a/src/sdks/rust/crates/oliphaunt-build/src/lib.rs +++ b/src/sdks/rust/crates/oliphaunt-build/src/lib.rs @@ -84,9 +84,9 @@ impl BuildContext { fn configure(&self) -> Result { let cargo_toml = self.manifest_dir.join("Cargo.toml"); let app = read_application_manifest(&cargo_toml)?; - let metadata = app.package.metadata.oliphaunt; + let metadata = &app.package.metadata.oliphaunt; let artifacts = self.read_artifact_manifests()?; - let selected = select_artifacts(&metadata, &artifacts, &self.target)?; + let selected = select_artifacts(&app, &artifacts, &self.target)?; let root = self.out_dir.join("oliphaunt"); let resources_dir = root.join("resources"); @@ -113,7 +113,7 @@ impl BuildContext { .map_err(|source| Error::io("create Oliphaunt OUT_DIR", &root, source))?; let staged = stage_artifacts(&selected, &resources_dir)?; - write_lock_file(&lock_file, &metadata, &self.target, &staged)?; + write_lock_file(&lock_file, metadata, &self.target, &staged)?; write_generated_rust(&generated_rust, &resources_dir, &lock_file)?; let mut cargo_instructions = vec![ @@ -193,10 +193,11 @@ fn read_application_manifest(path: &Path) -> Result { } fn select_artifacts( - metadata: &OliphauntMetadata, + app: &ApplicationManifest, artifacts: &[ArtifactManifest], target: &str, ) -> Result> { + let metadata = &app.package.metadata.oliphaunt; let selected_extensions: BTreeSet<&str> = metadata.extensions.iter().map(String::as_str).collect(); for artifact in artifacts { @@ -253,14 +254,6 @@ 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", @@ -269,14 +262,24 @@ 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", - )?); + if app.oliphaunt_wasix_tools_enabled() { + selected.push(require_artifact( + artifacts, + "oliphaunt-wasix-tools", + Some(&metadata.runtime_version), + ArtifactKind::WasixTools, + "portable", + "selected WASIX tools", + )?); + 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!( @@ -496,9 +499,63 @@ fn sha256_hex(bytes: &[u8]) -> String { out } +fn dependencies_enable_feature( + dependencies: &BTreeMap, + package: &str, + feature: &str, +) -> bool { + dependencies + .iter() + .any(|(name, spec)| dependency_enables_feature(name, spec, package, feature)) +} + +fn dependency_enables_feature( + name: &str, + spec: &toml::Value, + package: &str, + feature: &str, +) -> bool { + let toml::Value::Table(table) = spec else { + return false; + }; + let dependency_name = table + .get("package") + .and_then(toml::Value::as_str) + .unwrap_or(name); + if dependency_name != package { + return false; + } + let Some(toml::Value::Array(features)) = table.get("features") else { + return false; + }; + features + .iter() + .any(|candidate| candidate.as_str() == Some(feature)) +} + #[derive(Debug, Deserialize)] struct ApplicationManifest { package: ApplicationPackage, + #[serde(default)] + dependencies: BTreeMap, + #[serde(default)] + target: BTreeMap, +} + +impl ApplicationManifest { + fn oliphaunt_wasix_tools_enabled(&self) -> bool { + self.package.metadata.oliphaunt.tools + || dependencies_enable_feature(&self.dependencies, "oliphaunt-wasix", "tools") + || self.target.values().any(|target| { + dependencies_enable_feature(&target.dependencies, "oliphaunt-wasix", "tools") + }) + } +} + +#[derive(Debug, Default, Deserialize)] +struct ApplicationTargetTable { + #[serde(default)] + dependencies: BTreeMap, } #[derive(Debug, Deserialize)] @@ -521,6 +578,8 @@ struct OliphauntMetadata { extensions: Vec, #[serde(default)] icu: bool, + #[serde(default)] + tools: bool, } impl OliphauntMetadata { @@ -1334,6 +1393,163 @@ runtime-version = "0.1.0" ); } + #[test] + fn wasix_runtime_without_tools_stages_root_runtime_only() { + let temp = app_with_metadata( + r#" +[dependencies] +oliphaunt-wasix = "0.1.0" + +[package.metadata.oliphaunt] +runtime = "liboliphaunt-wasix" +runtime-version = "0.1.0" +"#, + ); + let runtime_manifest = write_artifact_manifest( + &temp, + "wasix-runtime.toml", + "liboliphaunt-wasix", + "0.1.0", + "wasix-runtime", + "portable", + None, + "oliphaunt.wasix.tar.zst", + ); + let aot_manifest = write_artifact_manifest( + &temp, + "wasix-aot.toml", + "liboliphaunt-wasix", + "0.1.0", + "wasix-aot", + "x86_64-unknown-linux-gnu", + None, + "oliphaunt-llvm-opta.bin.zst", + ); + 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, aot_manifest], + }; + + let output = context + .configure() + .expect("root WASIX runtime should not require split tools"); + + let lock = fs::read_to_string(output.lock_file).unwrap(); + assert!(lock.contains("product = \"liboliphaunt-wasix\"")); + assert!(!lock.contains("product = \"oliphaunt-wasix-tools\"")); + assert!( + output + .resources_dir + .join("wasix-runtime/liboliphaunt-wasix/bin/initdb.wasix.wasm") + .is_file() + ); + assert!( + output + .resources_dir + .join("wasix-aot/liboliphaunt-wasix/manifest.json") + .is_file() + ); + assert!( + !output + .resources_dir + .join("wasix-tools/oliphaunt-wasix-tools") + .exists() + ); + } + + #[test] + fn wasix_runtime_with_tools_feature_stages_split_tools() { + let temp = app_with_metadata( + r#" +[dependencies] +oliphaunt-wasix = { version = "0.1.0", features = ["tools"] } + +[package.metadata.oliphaunt] +runtime = "liboliphaunt-wasix" +runtime-version = "0.1.0" +"#, + ); + let runtime_manifest = write_artifact_manifest( + &temp, + "wasix-runtime.toml", + "liboliphaunt-wasix", + "0.1.0", + "wasix-runtime", + "portable", + None, + "oliphaunt.wasix.tar.zst", + ); + let tools_manifest = write_artifact_manifest( + &temp, + "wasix-tools.toml", + "oliphaunt-wasix-tools", + "0.1.0", + "wasix-tools", + "portable", + None, + "bin/pg_dump.wasix.wasm", + ); + let aot_manifest = write_artifact_manifest( + &temp, + "wasix-aot.toml", + "liboliphaunt-wasix", + "0.1.0", + "wasix-aot", + "x86_64-unknown-linux-gnu", + None, + "oliphaunt-llvm-opta.bin.zst", + ); + let tools_aot_manifest = write_artifact_manifest( + &temp, + "wasix-tools-aot.toml", + "oliphaunt-wasix-tools", + "0.1.0", + "wasix-tools-aot", + "x86_64-unknown-linux-gnu", + None, + "pg_dump-llvm-opta.bin.zst", + ); + 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, + tools_manifest, + aot_manifest, + tools_aot_manifest, + ], + }; + + let output = context + .configure() + .expect("WASIX tools feature should stage split tools artifacts"); + + let lock = fs::read_to_string(output.lock_file).unwrap(); + assert!(lock.contains("product = \"oliphaunt-wasix-tools\"")); + assert!(lock.contains("kind = \"wasix-tools-aot\"")); + assert!( + output + .resources_dir + .join("wasix-tools/oliphaunt-wasix-tools/bin/pg_dump.wasix.wasm") + .is_file() + ); + assert!( + output + .resources_dir + .join("wasix-tools/oliphaunt-wasix-tools/bin/psql.wasix.wasm") + .is_file() + ); + assert!( + output + .resources_dir + .join("wasix-tools-aot/oliphaunt-wasix-tools/pg_dump-llvm-opta.bin.zst") + .is_file() + ); + } + #[test] fn artifact_manifest_rejects_incomplete_native_tools_payload() { let temp = app_with_metadata(""); diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index 4ed037a3..f95dc6bc 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -2045,6 +2045,20 @@ def check_wasm(findings: list[Finding]) -> None: ], severity="P0", ) + oliphaunt_build_source = read_text("src/sdks/rust/crates/oliphaunt-build/src/lib.rs") + require( + findings, + product, + "wasm-build-tools-opt-in", + "fn oliphaunt_wasix_tools_enabled(&self) -> bool" in oliphaunt_build_source + and 'dependencies_enable_feature(&self.dependencies, "oliphaunt-wasix", "tools")' + in oliphaunt_build_source + and "wasix_runtime_without_tools_stages_root_runtime_only" in oliphaunt_build_source + and "wasix_runtime_with_tools_feature_stages_split_tools" in oliphaunt_build_source, + "oliphaunt-build must keep WASIX pg_dump/psql staging behind the explicit tools opt-in instead of treating tools as root runtime assets.", + "src/sdks/rust/crates/oliphaunt-build/src/lib.rs", + 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( diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 5156175d..6b3838dd 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -2315,6 +2315,7 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None 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") + oliphaunt_build_source = read_text("src/sdks/rust/crates/oliphaunt-build/src/lib.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 @@ -2323,6 +2324,13 @@ 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") + if ( + "fn oliphaunt_wasix_tools_enabled(&self) -> bool" not in oliphaunt_build_source + or 'dependencies_enable_feature(&self.dependencies, "oliphaunt-wasix", "tools")' not in oliphaunt_build_source + or "wasix_runtime_without_tools_stages_root_runtime_only" not in oliphaunt_build_source + or "wasix_runtime_with_tools_feature_stages_split_tools" not in oliphaunt_build_source + ): + fail("oliphaunt-build must stage WASIX pg_dump/psql tools artifacts only when the app opts into the oliphaunt-wasix tools feature") 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 ( From c10b0f4e3ed01b5fbd27809e307ecff4f6210000 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 07:42:18 +0000 Subject: [PATCH 279/308] fix: validate split native tools by target --- .../rust/crates/oliphaunt-build/src/lib.rs | 179 ++++++++++++++---- tools/release/check_consumer_shape.py | 9 + tools/release/check_release_metadata.py | 7 +- 3 files changed, 162 insertions(+), 33 deletions(-) diff --git a/src/sdks/rust/crates/oliphaunt-build/src/lib.rs b/src/sdks/rust/crates/oliphaunt-build/src/lib.rs index f8408b9a..229e6703 100644 --- a/src/sdks/rust/crates/oliphaunt-build/src/lib.rs +++ b/src/sdks/rust/crates/oliphaunt-build/src/lib.rs @@ -702,34 +702,18 @@ impl ArtifactManifest { 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", - ], + &native_tool_paths(&self.target, &["postgres", "initdb", "pg_ctl"]), )?; + self.reject_files(&relatives, &native_tool_path_variants(&["pg_dump", "psql"]))?; } ArtifactKind::NativeTools => { - self.require_files(&relatives, &["runtime/bin/pg_dump", "runtime/bin/psql"])?; + self.require_files( + &relatives, + &native_tool_paths(&self.target, &["pg_dump", "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", - ], + &native_tool_path_variants(&["postgres", "initdb", "pg_ctl"]), )?; } ArtifactKind::WasixRuntime => { @@ -790,9 +774,14 @@ impl ArtifactManifest { Ok(()) } - fn require_files(&self, relatives: &BTreeSet<&str>, required: &[&str]) -> Result<()> { + fn require_files>( + &self, + relatives: &BTreeSet<&str>, + required: &[S], + ) -> Result<()> { for relative in required { - if !relatives.contains(relative) && !windows_tool_variant_present(relatives, relative) { + let relative = relative.as_ref(); + if !relatives.contains(relative) { return Err(Error::new(format!( "{} {} artifact is missing required payload {relative:?}", self.label(), @@ -803,8 +792,13 @@ impl ArtifactManifest { Ok(()) } - fn reject_files(&self, relatives: &BTreeSet<&str>, rejected: &[&str]) -> Result<()> { + fn reject_files>( + &self, + relatives: &BTreeSet<&str>, + rejected: &[S], + ) -> Result<()> { for relative in rejected { + let relative = relative.as_ref(); if relatives.contains(relative) { return Err(Error::new(format!( "{} {} artifact must not contain payload {relative:?}", @@ -817,12 +811,32 @@ impl ArtifactManifest { } } -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()) +fn native_tool_paths(target: &str, stems: &[&str]) -> Vec { + let suffix = if is_windows_target(target) { + ".exe" + } else { + "" + }; + stems + .iter() + .map(|stem| format!("runtime/bin/{stem}{suffix}")) + .collect() +} + +fn native_tool_path_variants(stems: &[&str]) -> Vec { + stems + .iter() + .flat_map(|stem| { + [ + format!("runtime/bin/{stem}"), + format!("runtime/bin/{stem}.exe"), + ] + }) + .collect() +} + +fn is_windows_target(target: &str) -> bool { + target.contains("windows") } #[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)] @@ -1613,6 +1627,107 @@ runtime-version = "0.1.0" } } + #[test] + fn artifact_manifest_accepts_windows_native_split_payloads() { + 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-pc-windows-msvc", + None, + &[ + "runtime/bin/postgres.exe", + "runtime/bin/initdb.exe", + "runtime/bin/pg_ctl.exe", + ], + ); + let tools_manifest = write_artifact_manifest_with_relatives( + &temp, + "tools.toml", + "oliphaunt-tools", + "0.1.0", + "native-tools", + "x86_64-pc-windows-msvc", + None, + &["runtime/bin/pg_dump.exe", "runtime/bin/psql.exe"], + ); + let context = BuildContext { + manifest_dir: temp.path().to_path_buf(), + out_dir: temp.path().join("out"), + target: "x86_64-pc-windows-msvc".to_owned(), + artifact_manifest_paths: vec![runtime_manifest, tools_manifest], + }; + + let manifests = context + .read_artifact_manifests() + .expect("Windows native runtime/tools split should validate"); + + assert_eq!(manifests.len(), 2); + } + + #[test] + fn artifact_manifest_rejects_linux_native_runtime_with_windows_tool_names() { + 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.exe", + "runtime/bin/initdb.exe", + "runtime/bin/pg_ctl.exe", + ], + ); + 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("Linux native runtime must use Unix tool names"); + + assert!(error.to_string().contains("missing required payload")); + assert!(error.to_string().contains("runtime/bin/postgres")); + } + + #[test] + fn artifact_manifest_rejects_windows_native_tools_with_unix_tool_names() { + 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-pc-windows-msvc", + None, + &["runtime/bin/pg_dump", "runtime/bin/psql"], + ); + let context = BuildContext { + manifest_dir: temp.path().to_path_buf(), + out_dir: temp.path().join("out"), + target: "x86_64-pc-windows-msvc".to_owned(), + artifact_manifest_paths: vec![tools_manifest], + }; + + let error = context + .read_artifact_manifests() + .expect_err("Windows native tools must use .exe tool names"); + + assert!(error.to_string().contains("missing required payload")); + assert!(error.to_string().contains("runtime/bin/pg_dump.exe")); + } + #[test] fn artifact_manifest_rejects_wasix_runtime_client_tool_payloads() { for tool in ["bin/pg_dump.wasix.wasm", "bin/psql.wasix.wasm"] { diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index f95dc6bc..acc8822d 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -879,6 +879,7 @@ def check_liboliphaunt(findings: list[Finding]) -> None: native_windows_packager = read_text("tools/release/package-liboliphaunt-windows-assets.ps1") release_cli = read_text("tools/release/release.py") local_registry_publisher = read_text("tools/release/local-registry-publish.mjs") + oliphaunt_build_source = read_text("src/sdks/rust/crates/oliphaunt-build/src/lib.rs") native_runtime_package_split_failures = native_npm_tool_split_failures( "src/runtimes/liboliphaunt/native/packages", tool_set="runtime", @@ -923,6 +924,14 @@ def check_liboliphaunt(findings: list[Finding]) -> None: and "NON_PUBLISHABLE_LOCAL_CARGO_CRATE_PREFIXES" in local_registry_publisher and "isDefaultCargoTmpCrateArtifact" in local_registry_publisher and "ignored malformed Cargo scratch artifact" in local_registry_publisher + and 'native_tool_paths(&self.target, &["postgres", "initdb", "pg_ctl"])' + in oliphaunt_build_source + and 'native_tool_paths(&self.target, &["pg_dump", "psql"])' in oliphaunt_build_source + and "artifact_manifest_accepts_windows_native_split_payloads" in oliphaunt_build_source + and "artifact_manifest_rejects_linux_native_runtime_with_windows_tool_names" + in oliphaunt_build_source + and "artifact_manifest_rejects_windows_native_tools_with_unix_tool_names" + in oliphaunt_build_source and "NATIVE_RUNTIME_TOOL_STEMS" in native_optimizer and "NATIVE_TOOLS_TOOL_STEMS" in native_optimizer and not native_runtime_package_split_failures diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 6b3838dd..42e62bb4 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -2293,6 +2293,7 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None native_linux_packager_source = read_text("tools/release/package-liboliphaunt-linux-assets.sh") native_macos_packager_source = read_text("tools/release/package-liboliphaunt-macos-assets.sh") native_windows_packager_source = read_text("tools/release/package-liboliphaunt-windows-assets.ps1") + native_build_source = read_text("src/sdks/rust/crates/oliphaunt-build/src/lib.rs") if ( NATIVE_RUNTIME_TOOL_STEMS != ("initdb", "pg_ctl", "postgres") or NATIVE_TOOLS_TOOL_STEMS != ("pg_dump", "psql") @@ -2310,12 +2311,16 @@ def validate_wasm(wasix_runtime_version: str, wasm_binding_version: str) -> None or 'toolSet: "tools"' not in native_packager_source or "packageBase: TOOLS_PRODUCT" not in native_packager_source or "artifactProduct: TOOLS_PRODUCT" not in native_packager_source + or 'native_tool_paths(&self.target, &["postgres", "initdb", "pg_ctl"])' + not in native_build_source + or 'native_tool_paths(&self.target, &["pg_dump", "psql"])' not in native_build_source + or "artifact_manifest_accepts_windows_native_split_payloads" not in native_build_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") - oliphaunt_build_source = read_text("src/sdks/rust/crates/oliphaunt-build/src/lib.rs") + oliphaunt_build_source = native_build_source 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 From d406cd748a7230faa9107dcabe1fdb4bcdf2b6b9 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 07:54:41 +0000 Subject: [PATCH 280/308] fix: run native publish dry run in bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 15 + tools/policy/check-tooling-stack.sh | 14 + tools/release/check_release_metadata.py | 7 + tools/release/release-product-dry-run.mjs | 450 +++++++++++++++++- 4 files changed, 484 insertions(+), 2 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 70a47439..a9a221e3 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -58,6 +58,10 @@ 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. +- [x] Port `liboliphaunt-native` product publish dry-run off the protected + Python release implementation into the Bun product dry-run helper, including + native runtime/tools/ICU npm packages, split Cargo artifact crates, Maven + runtime artifact publishing, and fixture-backed validation. - [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`. @@ -78,6 +82,17 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-28: Ported `liboliphaunt-native` product publish dry-run into + `tools/release/release-product-dry-run.mjs`. The Bun path now stages or + copies native release assets, rewrites the checksum manifest, validates the + root native release asset set, generates and validates split native Cargo + artifact crates plus the source-only `oliphaunt-tools` facade, packs and + validates native runtime npm packages, split `@oliphaunt/tools-*` packages, + and `@oliphaunt/icu`, and publishes the runtime Maven artifact manifest to + Maven Local for dry-run validation. Fresh fixture-backed validation passed: + `OLIPHAUNT_LIBOLIPHAUNT_RELEASE_ASSET_INPUT_DIRS=target/liboliphaunt/native-dry-run-fixture-assets tools/dev/bun.sh tools/release/release-product-dry-run.mjs --product liboliphaunt-native --allow-dirty`. + Release metadata and tooling-stack guards now require this native Bun dry-run + path. - 2026-06-28: Extended the Bun product dry-run bridge to exact-extension products. `SUPPORTED_BUN_PRODUCT_DRY_RUNS` now includes `exactExtensionProducts(TOOL)`, so selected extension products route through diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 81541102..237040a7 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -441,6 +441,20 @@ if grep -Fq -- '--wasm dry-runs, and protected publish dispatch still delegate t fi grep -Fq 'SUPPORTED_SDK_PRODUCT_DRY_RUNS' tools/release/release-product-dry-run.mjs || fail "release product dry-run bridge must preserve SDK helper ownership" +grep -Fq 'LIBOLIPHAUNT_NATIVE_PRODUCT,' tools/release/release-product-dry-run.mjs || + fail "release product dry-run bridge must include liboliphaunt-native in Bun-owned product dry-runs" +grep -Fq 'ensureLiboliphauntReleaseAssets' tools/release/release-product-dry-run.mjs || + fail "Bun liboliphaunt-native product dry-run must validate staged release assets" +grep -Fq 'tools/release/check-liboliphaunt-release-assets.mjs' tools/release/release-product-dry-run.mjs || + fail "Bun liboliphaunt-native product dry-run must use the native release asset checker" +grep -Fq 'tools/release/package-liboliphaunt-cargo-artifacts.mjs' tools/release/release-product-dry-run.mjs || + fail "Bun liboliphaunt-native product dry-run must generate native Cargo artifact crates" +grep -Fq 'validateNativeCargoArtifacts' tools/release/release-product-dry-run.mjs || + fail "Bun liboliphaunt-native product dry-run must validate generated native Cargo artifact manifest rows" +grep -Fq 'liboliphauntNpmTarballs' tools/release/release-product-dry-run.mjs || + fail "Bun liboliphaunt-native product dry-run must validate native runtime/tools/ICU npm tarballs" +grep -Fq 'liboliphaunt-native-maven-dry-run' tools/release/release-product-dry-run.mjs || + fail "Bun liboliphaunt-native product dry-run must publish runtime Maven artifacts to Maven Local" grep -Fq 'BROKER_PRODUCT,' tools/release/release-product-dry-run.mjs || fail "release product dry-run bridge must include Broker in Bun-owned product dry-runs" grep -Fq 'ensureBrokerReleaseAssets' tools/release/release-product-dry-run.mjs || diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 42e62bb4..ca3471bb 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1009,6 +1009,13 @@ def validate_publish_target_coverage() -> None: or 'await runBunProductDryRun(legacyWasmDryRunPlan.product, { allowDirty: legacyWasmDryRunPlan.allowDirty });' not in release_publish or "--wasm dry-runs, and protected publish dispatch still delegate to release.py" in release_publish or "SUPPORTED_SDK_PRODUCT_DRY_RUNS" not in release_product_dry_run + or "LIBOLIPHAUNT_NATIVE_PRODUCT," not in release_product_dry_run + or "ensureLiboliphauntReleaseAssets" not in release_product_dry_run + or "tools/release/check-liboliphaunt-release-assets.mjs" not in release_product_dry_run + or "tools/release/package-liboliphaunt-cargo-artifacts.mjs" not in release_product_dry_run + or "validateNativeCargoArtifacts" not in release_product_dry_run + or "liboliphauntNpmTarballs" not in release_product_dry_run + or "liboliphaunt-native-maven-dry-run" not in release_product_dry_run or "BROKER_PRODUCT," not in release_product_dry_run or "ensureBrokerReleaseAssets" not in release_product_dry_run or "brokerNpmTarballs" not in release_product_dry_run diff --git a/tools/release/release-product-dry-run.mjs b/tools/release/release-product-dry-run.mjs index a84ebf6c..fa4ca0c7 100644 --- a/tools/release/release-product-dry-run.mjs +++ b/tools/release/release-product-dry-run.mjs @@ -4,7 +4,10 @@ import { createHash } from "node:crypto"; import { chmodSync, copyFileSync, + cpSync, + existsSync, mkdirSync, + mkdtempSync, readdirSync, readFileSync, rmSync, @@ -29,8 +32,21 @@ import { WASIX_CARGO_ARTIFACT_SCHEMA, publicCargoPackageNames as wasixPublicCargoPackageNames, } from "./wasix-cargo-artifact-contract.mjs"; +import { + requiredRuntimeMemberPaths, + requiredToolsMemberPaths, + requiredToolsPackageTools, +} from "./optimize_native_runtime_payload.mjs"; const TOOL = "release-product-dry-run.mjs"; +const LIBOLIPHAUNT_NATIVE_PRODUCT = "liboliphaunt-native"; +const LIBOLIPHAUNT_NATIVE_KIND = "native-runtime"; +const LIBOLIPHAUNT_NATIVE_TOOLS_PRODUCT = "oliphaunt-tools"; +const LIBOLIPHAUNT_NATIVE_TOOLS_KIND = "native-tools"; +const LIBOLIPHAUNT_NATIVE_PACKAGE_ROOT = path.join(ROOT, "src/runtimes/liboliphaunt/native/packages"); +const LIBOLIPHAUNT_NATIVE_TOOLS_PACKAGE_ROOT = path.join(ROOT, "src/runtimes/liboliphaunt/native/tools-packages"); +const LIBOLIPHAUNT_ICU_PACKAGE_NAME = "@oliphaunt/icu"; +const LIBOLIPHAUNT_ICU_PACKAGE_ROOT = path.join(ROOT, "src/runtimes/liboliphaunt/native/icu-npm"); const BROKER_PRODUCT = "oliphaunt-broker"; const BROKER_KIND = "broker-helper"; const BROKER_PACKAGE_ROOT = path.join(ROOT, "src/runtimes/broker/packages"); @@ -42,6 +58,7 @@ const NODE_DIRECT_PACKAGE_ROOT = path.join(ROOT, "src/runtimes/node-direct/packa export const SUPPORTED_BUN_PRODUCT_DRY_RUNS = new Set([ ...SUPPORTED_SDK_PRODUCT_DRY_RUNS, ...exactExtensionProducts(TOOL), + LIBOLIPHAUNT_NATIVE_PRODUCT, BROKER_PRODUCT, WASIX_PRODUCT, NODE_DIRECT_PRODUCT, @@ -184,6 +201,62 @@ function hasWasixReleaseArchive(assetDir) { ); } +function hasLiboliphauntReleaseArchive(assetDir) { + if (!isDirectory(assetDir)) { + return false; + } + return readdirSync(assetDir).some((name) => + ( + name.startsWith("liboliphaunt-") || + name.startsWith("oliphaunt-tools-") + ) && (name.endsWith(".tar.gz") || name.endsWith(".zip") || name.endsWith(".tsv")), + ); +} + +function ensureLiboliphauntReleaseAssets() { + const assetDir = path.join(ROOT, "target/liboliphaunt/release-assets"); + if (!hasLiboliphauntReleaseArchive(assetDir)) { + copyStagedRuntimeAssets({ + product: LIBOLIPHAUNT_NATIVE_PRODUCT, + destination: assetDir, + envName: "OLIPHAUNT_LIBOLIPHAUNT_RELEASE_ASSET_INPUT_DIRS", + patterns: [ + "liboliphaunt-*.tar.gz", + "liboliphaunt-*.zip", + "liboliphaunt-*.tsv", + "liboliphaunt-*.sha256", + "oliphaunt-tools-*.tar.gz", + "oliphaunt-tools-*.zip", + ], + }); + } + const version = currentProductVersionSync(LIBOLIPHAUNT_NATIVE_PRODUCT, TOOL); + run(TOOL, [ + "tools/dev/bun.sh", + "tools/release/write_checksum_manifest.mjs", + "--asset-dir", + rel(assetDir), + "--output", + `liboliphaunt-${version}-release-assets.sha256`, + "--pattern", + "liboliphaunt-*.tar.gz", + "--pattern", + "liboliphaunt-*.zip", + "--pattern", + "liboliphaunt-*.tsv", + "--pattern", + "oliphaunt-tools-*.tar.gz", + "--pattern", + "oliphaunt-tools-*.zip", + ]); + run(TOOL, [ + "tools/dev/bun.sh", + "tools/release/check-liboliphaunt-release-assets.mjs", + "--asset-dir", + rel(assetDir), + ]); +} + function ensureBrokerReleaseAssets() { const assetDir = path.join(ROOT, "target/oliphaunt-broker/release-assets"); if (!hasBrokerReleaseArchive(assetDir)) { @@ -448,11 +521,19 @@ function npmPackageSourceStageDir(packageName) { return path.join(ROOT, "target/release/npm-package-sources", safeNpmPackageFilenamePrefix(packageName)); } -function stageNpmPackageDescriptor(packageName, sourceDir, version, { target = null } = {}) { +function stageNpmPackageDescriptor( + packageName, + sourceDir, + version, + { + extraDescriptors = [], + target = null, + } = {}, +) { const stageDir = npmPackageSourceStageDir(packageName); rmSync(stageDir, { recursive: true, force: true }); mkdirSync(stageDir, { recursive: true }); - for (const descriptor of ["package.json", "README.md"]) { + for (const descriptor of ["package.json", "README.md", ...extraDescriptors]) { const source = path.join(sourceDir, descriptor); if (!isFile(source)) { fail(`${rel(sourceDir)} is missing ${descriptor}`); @@ -513,6 +594,83 @@ function extractReleaseArchiveFile(archive, memberName, destination, { mode = nu } } +function archiveTempDir() { + const root = path.join(ROOT, "target/release/archive-extract"); + mkdirSync(root, { recursive: true }); + return mkdtempSync(path.join(root, "extract-")); +} + +function runArchiveCommand(args, label) { + const result = spawnSync(args[0], args.slice(1), { + cwd: ROOT, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.error !== undefined) { + fail(`${label} failed to start: ${result.error.message}`); + } + return result; +} + +function copyExtractedTree(source, destination) { + if (!isDirectory(source)) { + fail(`release archive is missing extracted tree ${source}`); + } + rmSync(destination, { recursive: true, force: true }); + cpSync(source, destination, { recursive: true }); +} + +function extractReleaseArchiveTree(archive, sourcePrefix, destination) { + const temp = archiveTempDir(); + const prefix = sourcePrefix.replace(/\/+$/u, ""); + try { + for (const candidate of [prefix, `./${prefix}`]) { + const result = archive.endsWith(".zip") + ? runArchiveCommand( + ["unzip", "-q", archive, `${candidate}/*`, "-d", temp], + `extract ${candidate} from ${rel(archive)}`, + ) + : runArchiveCommand( + ["tar", "-xf", archive, "-C", temp, candidate], + `extract ${candidate} from ${rel(archive)}`, + ); + const extracted = path.join(temp, ...candidate.replace(/^\.\//u, "").split("/")); + if (result.status === 0 && isDirectory(extracted)) { + copyExtractedTree(extracted, destination); + return; + } + } + } finally { + rmSync(temp, { recursive: true, force: true }); + } + fail(`${rel(archive)} is missing ${prefix}`); +} + +function runNativePayloadOptimizer(stage, target, toolSet) { + run(TOOL, [ + "tools/dev/bun.sh", + "tools/release/optimize_native_runtime_payload.mjs", + rel(stage), + "--target", + target, + "--tool-set", + toolSet, + ]); +} + +function ensureNativeToolsAbsentFromRuntime(stage, target) { + const runtimeDir = path.join(stage, "runtime"); + const leaked = []; + for (const tool of requiredToolsPackageTools(target, runtimeDir)) { + if (existsSync(path.join(runtimeDir, "bin", tool))) { + leaked.push(`runtime/bin/${tool}`); + } + } + if (leaked.length > 0) { + fail(`${rel(stage)} root runtime package must not contain split native tools: ${leaked.join(", ")}`); + } +} + function pnpmPackForNpmPublish(packageDir) { const packageJson = JSON.parse(readFileSync(path.join(packageDir, "package.json"), "utf8")); const packageName = packageJson.name; @@ -592,6 +750,167 @@ function validatePackedNpmPackage({ } } +function liboliphauntRuntimeNpmPackageTargets(version) { + return artifactNpmPackageTargets({ + product: LIBOLIPHAUNT_NATIVE_PRODUCT, + kind: LIBOLIPHAUNT_NATIVE_KIND, + surface: "typescript-native-direct", + packageRoot: LIBOLIPHAUNT_NATIVE_PACKAGE_ROOT, + version, + }); +} + +function liboliphauntToolsNpmPackageTargets(version) { + return artifactNpmPackageTargets({ + product: LIBOLIPHAUNT_NATIVE_PRODUCT, + kind: LIBOLIPHAUNT_NATIVE_TOOLS_KIND, + surface: "typescript-native-direct", + packageRoot: LIBOLIPHAUNT_NATIVE_TOOLS_PACKAGE_ROOT, + version, + }); +} + +function stageLiboliphauntNpmPayloads(version) { + const assetDir = path.join(ROOT, "target/liboliphaunt/release-assets"); + const stages = new Map(); + for (const [packageName, packageDir, target] of liboliphauntRuntimeNpmPackageTargets(version)) { + const libraryRelativePath = target.libraryRelativePath ?? target.library_relative_path; + if (typeof libraryRelativePath !== "string" || libraryRelativePath.length === 0) { + fail(`${target.id} must declare library_relative_path for npm artifact package publication`); + } + const stage = stageNpmPackageDescriptor(packageName, packageDir, version, { target: target.target }); + const archive = path.join(assetDir, target.asset.replaceAll("{version}", version)); + extractReleaseArchiveFile(archive, libraryRelativePath, path.join(stage, libraryRelativePath)); + extractReleaseArchiveTree(archive, "runtime", path.join(stage, "runtime")); + ensureNativeToolsAbsentFromRuntime(stage, target.target); + runNativePayloadOptimizer(stage, target.target, "runtime"); + stages.set(packageName, stage); + } + return stages; +} + +function stageLiboliphauntToolsNpmPayloads(version) { + const assetDir = path.join(ROOT, "target/liboliphaunt/release-assets"); + const stages = new Map(); + for (const [packageName, packageDir, target] of liboliphauntToolsNpmPackageTargets(version)) { + const stage = stageNpmPackageDescriptor(packageName, packageDir, version, { target: target.target }); + const archive = path.join(assetDir, target.asset.replaceAll("{version}", version)); + for (const member of requiredToolsMemberPaths(target.target, "runtime/bin")) { + extractReleaseArchiveFile(archive, member, path.join(stage, member), { + mode: 0o755, + }); + } + runNativePayloadOptimizer(stage, target.target, "tools"); + stages.set(packageName, stage); + } + return stages; +} + +function stageLiboliphauntIcuNpmPayload(version) { + const stage = stageNpmPackageDescriptor( + LIBOLIPHAUNT_ICU_PACKAGE_NAME, + LIBOLIPHAUNT_ICU_PACKAGE_ROOT, + version, + { + extraDescriptors: ["OliphauntICU.podspec"], + target: "portable", + }, + ); + extractReleaseArchiveTree( + path.join(ROOT, "target/liboliphaunt/release-assets", `liboliphaunt-${version}-icu-data.tar.gz`), + "share/icu", + path.join(stage, "share/icu"), + ); + return stage; +} + +function validatePackedIcuPackage(packageName, version, tarball) { + let entries; + try { + entries = readTarGzEntries(tarball); + } catch (error) { + fail(`${rel(tarball)} is not a valid ICU npm tarball: ${error.message}`); + } + if (!entries.has("package/package.json")) { + fail(`${rel(tarball)} is missing package/package.json`); + } + let packageJson; + try { + const packageData = readTarGzMember(tarball, "package/package.json"); + if (packageData === null) { + fail(`${rel(tarball)} package/package.json could not be read`); + } + packageJson = JSON.parse(packageData.toString("utf8")); + } catch (error) { + fail(`${rel(tarball)} package/package.json is not valid JSON: ${error.message}`); + } + if (packageJson.name !== packageName) { + fail(`${rel(tarball)} package name must be ${packageName}, got ${JSON.stringify(packageJson.name)}`); + } + if (packageJson.version !== version) { + fail(`${rel(tarball)} package version must be ${version}, got ${JSON.stringify(packageJson.version)}`); + } + const metadata = packageJson.oliphaunt; + if ( + metadata?.product !== "oliphaunt-icu" || + metadata?.kind !== "icu-data" || + metadata?.target !== "portable" || + metadata?.dataRelativePath !== "share/icu" + ) { + fail(`${rel(tarball)} package.json must declare portable oliphaunt-icu metadata`); + } + if (!entries.has("package/OliphauntICU.podspec")) { + fail(`${rel(tarball)} is missing package/OliphauntICU.podspec`); + } + const hasIcuData = [...entries.keys()].some((member) => { + if (!member.startsWith("package/share/icu/")) { + return false; + } + const relative = member.slice("package/share/icu/".length).split("/").filter(Boolean); + return relative.length > 0 && relative[0].startsWith("icudt"); + }); + if (!hasIcuData) { + fail(`${rel(tarball)} is missing package/share/icu/icudt* data files`); + } +} + +function liboliphauntNpmTarballs(version) { + const packages = []; + const runtimeStages = stageLiboliphauntNpmPayloads(version); + const toolsStages = stageLiboliphauntToolsNpmPayloads(version); + for (const [packageName, , target] of liboliphauntRuntimeNpmPackageTargets(version)) { + const libraryRelativePath = target.libraryRelativePath ?? target.library_relative_path; + const runtimeMembers = requiredRuntimeMemberPaths(target.target, "package/runtime/bin"); + const requiredMembers = [`package/${libraryRelativePath}`, ...runtimeMembers]; + const tarball = pnpmPackForNpmPublish(runtimeStages.get(packageName)); + validatePackedNpmPackage({ + packageName, + version, + tarball, + requiredMembers, + executableMembers: runtimeMembers, + }); + packages.push([packageName, tarball]); + } + for (const [packageName, , target] of liboliphauntToolsNpmPackageTargets(version)) { + const runtimeMembers = requiredToolsMemberPaths(target.target, "package/runtime/bin"); + const tarball = pnpmPackForNpmPublish(toolsStages.get(packageName)); + validatePackedNpmPackage({ + packageName, + version, + tarball, + requiredMembers: runtimeMembers, + executableMembers: runtimeMembers, + }); + packages.push([packageName, tarball]); + } + const icuStage = stageLiboliphauntIcuNpmPayload(version); + const icuTarball = pnpmPackForNpmPublish(icuStage); + validatePackedIcuPackage(LIBOLIPHAUNT_ICU_PACKAGE_NAME, version, icuTarball); + packages.push([LIBOLIPHAUNT_ICU_PACKAGE_NAME, icuTarball]); + return packages; +} + function brokerNpmTarballs(version) { const tarballs = []; const assetDir = path.join(ROOT, "target/oliphaunt-broker/release-assets"); @@ -696,6 +1015,129 @@ function runBrokerDryRun() { ]); } +function nativeCargoArtifactTargets(kind) { + return artifactTargets(LIBOLIPHAUNT_NATIVE_PRODUCT, kind, TOOL) + .filter((target) => target.surfaces.includes("rust-native-direct")) + .sort((left, right) => compareText(left.target, right.target)); +} + +function validateNativeCargoArtifacts(outputDir) { + const manifestPath = path.join(outputDir, "packages.json"); + if (!isFile(manifestPath)) { + fail(`missing generated ${LIBOLIPHAUNT_NATIVE_PRODUCT} Cargo artifact manifest: ${rel(manifestPath)}`); + } + let data; + try { + data = JSON.parse(readFileSync(manifestPath, "utf8")); + } catch (error) { + fail(`${rel(manifestPath)} is not valid JSON: ${error.message}`); + } + if (data?.schema !== "oliphaunt-liboliphaunt-cargo-artifacts-v1" || !Array.isArray(data.packages)) { + fail(`${rel(manifestPath)} has an invalid liboliphaunt native Cargo artifact schema`); + } + + const expectedAggregators = new Set([ + ...nativeCargoArtifactTargets(LIBOLIPHAUNT_NATIVE_KIND) + .map((target) => `${LIBOLIPHAUNT_NATIVE_PRODUCT}-${target.target}`), + ...nativeCargoArtifactTargets(LIBOLIPHAUNT_NATIVE_TOOLS_KIND) + .map((target) => `${LIBOLIPHAUNT_NATIVE_TOOLS_PRODUCT}-${target.target}`), + ]); + const aggregators = new Set(); + const facades = new Set(); + const expectedCratePaths = new Set(); + + for (const item of data.packages) { + if (item === null || Array.isArray(item) || typeof item !== "object") { + fail(`${rel(manifestPath)} package entries must be objects`); + } + const { name, role, manifestPath: rawManifest, cratePath: rawCrate } = item; + if (![name, role, rawManifest].every((value) => typeof value === "string" && value.length > 0)) { + fail(`${rel(manifestPath)} has an invalid package row: ${JSON.stringify(item)}`); + } + const sourceManifest = path.join(ROOT, rawManifest); + if (!isFile(sourceManifest)) { + fail(`missing generated ${LIBOLIPHAUNT_NATIVE_PRODUCT} Cargo source manifest: ${rawManifest}`); + } + if (role === "part") { + const aggregator = name.replace(/-part-\d{3}$/u, ""); + if (aggregator === name || !expectedAggregators.has(aggregator)) { + fail(`unexpected ${LIBOLIPHAUNT_NATIVE_PRODUCT} Cargo part crate ${name}`); + } + if (typeof rawCrate !== "string" || rawCrate.length === 0) { + fail(`generated ${LIBOLIPHAUNT_NATIVE_PRODUCT} part crate ${name} must have a cratePath`); + } + const cratePath = path.join(ROOT, rawCrate); + if (!isFile(cratePath)) { + fail(`missing generated ${LIBOLIPHAUNT_NATIVE_PRODUCT} Cargo part crate for ${name}: ${rawCrate}`); + } + expectedCratePaths.add(path.resolve(cratePath)); + continue; + } + if (role === "aggregator") { + if (!expectedAggregators.has(name)) { + fail(`unexpected ${LIBOLIPHAUNT_NATIVE_PRODUCT} Cargo aggregator crate ${name}`); + } + if (rawCrate !== null) { + fail(`generated ${LIBOLIPHAUNT_NATIVE_PRODUCT} aggregator crate ${name} must be source-only`); + } + aggregators.add(name); + continue; + } + if (role === "facade") { + if (name !== LIBOLIPHAUNT_NATIVE_TOOLS_PRODUCT) { + fail(`unexpected ${LIBOLIPHAUNT_NATIVE_PRODUCT} Cargo facade crate ${name}`); + } + if (rawCrate !== null) { + fail(`generated ${LIBOLIPHAUNT_NATIVE_PRODUCT} facade crate ${name} must be source-only`); + } + facades.add(name); + continue; + } + fail(`${rel(manifestPath)} has unsupported Cargo artifact role ${JSON.stringify(role)}`); + } + + const missingAggregators = [...expectedAggregators] + .filter((name) => !aggregators.has(name)) + .sort(compareText); + if (missingAggregators.length > 0) { + fail(`generated ${LIBOLIPHAUNT_NATIVE_PRODUCT} Cargo artifacts are missing aggregator crates: ${missingAggregators.join(", ")}`); + } + if (!facades.has(LIBOLIPHAUNT_NATIVE_TOOLS_PRODUCT)) { + fail(`generated ${LIBOLIPHAUNT_NATIVE_PRODUCT} Cargo artifacts are missing ${LIBOLIPHAUNT_NATIVE_TOOLS_PRODUCT} facade crate`); + } + const unexpected = readdirSync(outputDir) + .filter((name) => name.endsWith(".crate")) + .map((name) => path.join(outputDir, name)) + .filter((file) => !expectedCratePaths.has(path.resolve(file))) + .map((file) => path.basename(file)) + .sort(compareText); + if (unexpected.length > 0) { + fail(`unexpected ${LIBOLIPHAUNT_NATIVE_PRODUCT} Cargo artifact crate(s): ${unexpected.join(", ")}`); + } +} + +function runLiboliphauntNativeDryRun() { + const version = currentProductVersionSync(LIBOLIPHAUNT_NATIVE_PRODUCT, TOOL); + const outputDir = path.join(ROOT, "target/liboliphaunt/cargo-artifacts"); + ensureLiboliphauntReleaseAssets(); + run(TOOL, [ + "tools/dev/bun.sh", + "tools/release/package-liboliphaunt-cargo-artifacts.mjs", + "--version", + version, + "--output-dir", + rel(outputDir), + ]); + validateNativeCargoArtifacts(outputDir); + liboliphauntNpmTarballs(version); + const manifest = buildMavenArtifactManifest("liboliphaunt-native-runtime", { runtime: true }); + runMavenArtifactPublisher( + manifest, + ":oliphaunt-maven-artifacts:publishToMavenLocal", + "liboliphaunt-native-maven-dry-run", + ); +} + function isExpectedWasixExtensionPackage(name, kind) { if (kind === "wasix-extension") { return exactExtensionProducts(TOOL).some((product) => name === `${product}-wasix`); @@ -883,6 +1325,10 @@ export async function runBunProductDryRun(product, { allowDirty = false } = {}) await runSdkProductDryRun(product, { allowDirty }); return; } + if (product === LIBOLIPHAUNT_NATIVE_PRODUCT) { + runLiboliphauntNativeDryRun(); + return; + } if (product === BROKER_PRODUCT) { runBrokerDryRun(); return; From 35634ba120c6bd9478189dbc88ec747b46b31013 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 08:00:00 +0000 Subject: [PATCH 281/308] chore: remove react native release.py task inputs --- docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md | 8 ++++++++ src/sdks/react-native/moon.yml | 6 ------ tools/policy/check-tooling-stack.sh | 3 +++ tools/release/check_release_metadata.py | 4 +++- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index a9a221e3..14502c2e 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -82,6 +82,14 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-28: Removed stale direct `tools/release/release.py` inputs from the + React Native Moon tasks. The React Native SDK package and package-artifact + paths already run through `tools/release/build-sdk-ci-artifacts.mjs` and + `tools/release/check-staged-artifacts.mjs`, while product publish dry-runs + are covered by `tools/release/release-sdk-product-dry-run.mjs`. Release + metadata and tooling-stack guards now reject reintroducing direct + React Native task dependencies on the protected Python release + implementation. - 2026-06-28: Ported `liboliphaunt-native` product publish dry-run into `tools/release/release-product-dry-run.mjs`. The Bun path now stages or copies native release assets, rewrites the checksum manifest, validates the diff --git a/src/sdks/react-native/moon.yml b/src/sdks/react-native/moon.yml index 49808861..d4141ad2 100644 --- a/src/sdks/react-native/moon.yml +++ b/src/sdks/react-native/moon.yml @@ -51,7 +51,6 @@ tasks: - "!/src/sdks/react-native/**/build/**" - "!/src/sdks/react-native/**/lib/**" - "!/src/sdks/react-native/ios/vendor/**" - - "/tools/release/release.py" options: cache: true runFromWorkspaceRoot: true @@ -81,7 +80,6 @@ tasks: - "/src/sdks/swift/**/*" - "!/src/sdks/swift/.build" - "!/src/sdks/swift/.build/**" - - "/tools/release/release.py" options: cache: true runFromWorkspaceRoot: true @@ -110,7 +108,6 @@ tasks: - "!/src/sdks/react-native/**/build/**" - "!/src/sdks/react-native/**/lib/**" - "!/src/sdks/react-native/ios/vendor/**" - - "/tools/release/release.py" options: cache: true runFromWorkspaceRoot: true @@ -499,7 +496,6 @@ tasks: - "!/src/sdks/react-native/ios/vendor/**" - "/tools/release/build-sdk-ci-artifacts.mjs" - "/tools/release/check-staged-artifacts.mjs" - - "/tools/release/release.py" outputs: - "/target/liboliphaunt-sdk-check/oliphaunt-react-native/package-shape/src/sdks/react-native/**/*" options: @@ -528,7 +524,6 @@ tasks: - "!/src/sdks/react-native/ios/vendor/**" - "/tools/release/build-sdk-ci-artifacts.mjs" - "/tools/release/check-staged-artifacts.mjs" - - "/tools/release/release.py" outputs: - "/target/sdk-artifacts/oliphaunt-react-native/**/*" options: @@ -623,7 +618,6 @@ tasks: - "/src/sdks/swift/**/*" - "!/src/sdks/swift/.build" - "!/src/sdks/swift/.build/**" - - "/tools/release/release.py" - "/tools/test/**/*" options: cache: local diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 237040a7..a52e1669 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -492,6 +492,9 @@ grep -Fq '/tools/release/release-product-dry-run.mjs' src/sdks/js/moon.yml || if grep -Fq '/tools/release/release.py' src/sdks/js/moon.yml; then fail "TypeScript SDK Moon tasks must not track release.py after Node direct dry-run guards moved to Bun" fi +if grep -Fq '/tools/release/release.py' src/sdks/react-native/moon.yml; then + fail "React Native SDK Moon tasks must not track release.py after SDK artifact and dry-run checks moved to Bun" +fi grep -Fq '"oliphaunt-swift",' tools/release/release-sdk-product-dry-run.mjs || fail "release SDK product dry-run helper must include Swift in Bun-owned low-risk SDK product dry-runs" grep -Fq '"oliphaunt-kotlin",' tools/release/release-sdk-product-dry-run.mjs || diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index ca3471bb..862a6c34 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -989,6 +989,7 @@ def validate_publish_target_coverage() -> None: release_publish = read_text("tools/release/release-publish.mjs") release_product_dry_run = read_text("tools/release/release-product-dry-run.mjs") release_sdk_product_dry_run = read_text("tools/release/release-sdk-product-dry-run.mjs") + react_native_moon = read_text("src/sdks/react-native/moon.yml") 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: @@ -998,6 +999,7 @@ def validate_publish_target_coverage() -> None: or "tools/dev/bun.sh tools/release/release-publish.mjs publish " not in workflow or "tools/release/release.py publish-dry-run" in workflow or "tools/release/release.py publish --" in workflow + or "/tools/release/release.py" in react_native_moon or 'const COMMANDS = new Set(["publish", "publish-dry-run"]);' not in release_publish or 'function isNoProductPublishDryRun(' not in release_publish or 'run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check.mjs"]);' not in release_publish @@ -1042,7 +1044,7 @@ def validate_publish_target_coverage() -> None: or "prepareOliphauntWasixReleaseSource" not in release_sdk_product_dry_run or 'spawnSync("tools/release/release.py", argv' not in release_publish ): - fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product and legacy --wasm publish dry-runs must run through Bun without launching release.py, and low-risk product dry-runs must stay in Bun") + fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product and legacy --wasm publish dry-runs must run through Bun without launching release.py, low-risk product dry-runs must stay in Bun, and React Native SDK tasks must not track release.py directly") 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 From ebbdf1a09fb5a572d2bf1ab02a8573d6d6c6e124 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 08:06:02 +0000 Subject: [PATCH 282/308] fix: keep product dry runs in bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 19 +++++++++--- tools/policy/check-tooling-stack.sh | 23 ++++++++++++++ tools/release/check_release_metadata.py | 4 ++- tools/release/release-publish.mjs | 31 +++++++++++-------- 4 files changed, 58 insertions(+), 19 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 14502c2e..437f224d 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -82,6 +82,15 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-28: Closed the last product-scoped `publish-dry-run` fallback to + `tools/release/release.py`. A direct graph comparison found all 49 release + products in `SUPPORTED_BUN_PRODUCT_DRY_RUNS`, so + `tools/release/release-publish.mjs` now fails invalid product selections in + Bun instead of falling through to the protected Python implementation. The + wrapper still delegates protected `publish` dispatch to `release.py` while + that final publish implementation is ported. Release metadata and + tooling-stack guards reject reintroducing wording or behavior that treats + product dry-runs as Python-owned. - 2026-06-28: Removed stale direct `tools/release/release.py` inputs from the React Native Moon tasks. The React Native SDK package and package-artifact paths already run through `tools/release/build-sdk-ci-artifacts.mjs` and @@ -238,11 +247,11 @@ until the current-state gates here are checked with fresh local evidence. `tools/release/release-publish.mjs` for active release workflow `publish-dry-run` and `publish` calls. The workflow now invokes publish operations through `tools/dev/bun.sh tools/release/release-publish.mjs`. - The no-product publish dry-run now runs `release-check.mjs` directly in Bun; - product-scoped dry-runs and publish dispatch remain behind the protected - `release.py` implementation until publish dispatch is ported. Release - metadata and tooling guards reject direct workflow `release.py publish*` - calls. + The no-product publish dry-run initially ran `release-check.mjs` directly in + Bun; later entries moved product-scoped dry-runs to Bun as well. Protected + publish dispatch remains behind the protected `release.py` implementation + until publish dispatch is ported. Release metadata and tooling guards reject + direct workflow `release.py publish*` calls. - 2026-06-28: Added Bun command surfaces for the remaining active release metadata and consumer-shape validator implementations: `tools/release/check-release-metadata.mjs` and diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index a52e1669..ada93a51 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -439,6 +439,29 @@ grep -Fq 'await runBunProductDryRun(legacyWasmDryRunPlan.product, { allowDirty: if grep -Fq -- '--wasm dry-runs, and protected publish dispatch still delegate to release.py' tools/release/release-publish.mjs; then fail "release-publish must not describe legacy --wasm dry-runs as delegated to release.py" fi +if grep -Fq 'Other product dry-runs' tools/release/release-publish.mjs; then + fail "release-publish must not describe product publish dry-runs as delegated to release.py" +fi +grep -Fq 'publish-dry-run is Bun-owned' tools/release/release-publish.mjs || + fail "release-publish must fail closed before release.py fallback for unsupported publish-dry-run arguments" +tools/dev/bun.sh -e ' +import { spawnSync } from "node:child_process"; +import { SUPPORTED_BUN_PRODUCT_DRY_RUNS } from "./tools/release/release-product-dry-run.mjs"; + +const result = spawnSync("tools/dev/bun.sh", ["tools/release/release_graph_query.mjs", "product-configs"], { + encoding: "utf8", +}); +if (result.status !== 0 || result.error !== undefined) { + console.error(result.stderr || result.error?.message || "release graph query failed"); + process.exit(1); +} +const products = JSON.parse(result.stdout).map((row) => row.product ?? row.id).sort(); +const missing = products.filter((product) => !SUPPORTED_BUN_PRODUCT_DRY_RUNS.has(product)); +if (missing.length > 0) { + console.error(`Bun product publish dry-run support is missing release products: ${missing.join(", ")}`); + process.exit(1); +} +' || fail "release product dry-run bridge must cover every release product" grep -Fq 'SUPPORTED_SDK_PRODUCT_DRY_RUNS' tools/release/release-product-dry-run.mjs || fail "release product dry-run bridge must preserve SDK helper ownership" grep -Fq 'LIBOLIPHAUNT_NATIVE_PRODUCT,' tools/release/release-product-dry-run.mjs || diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 862a6c34..32396a73 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1010,6 +1010,8 @@ def validate_publish_target_coverage() -> None: or 'LEGACY_WASM_DRY_RUN_PRODUCT = "oliphaunt-wasix-rust"' not in release_publish or 'await runBunProductDryRun(legacyWasmDryRunPlan.product, { allowDirty: legacyWasmDryRunPlan.allowDirty });' not in release_publish or "--wasm dry-runs, and protected publish dispatch still delegate to release.py" in release_publish + or "Other product dry-runs" in release_publish + or "publish-dry-run is Bun-owned" not in release_publish or "SUPPORTED_SDK_PRODUCT_DRY_RUNS" not in release_product_dry_run or "LIBOLIPHAUNT_NATIVE_PRODUCT," not in release_product_dry_run or "ensureLiboliphauntReleaseAssets" not in release_product_dry_run @@ -1044,7 +1046,7 @@ def validate_publish_target_coverage() -> None: or "prepareOliphauntWasixReleaseSource" not in release_sdk_product_dry_run or 'spawnSync("tools/release/release.py", argv' not in release_publish ): - fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product and legacy --wasm publish dry-runs must run through Bun without launching release.py, low-risk product dry-runs must stay in Bun, and React Native SDK tasks must not track release.py directly") + fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, and React Native SDK tasks must not track release.py directly") 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 diff --git a/tools/release/release-publish.mjs b/tools/release/release-publish.mjs index 4659320b..eedf1d23 100755 --- a/tools/release/release-publish.mjs +++ b/tools/release/release-publish.mjs @@ -14,11 +14,10 @@ function usage() { console.log(`usage: tools/release/release-publish.mjs [publish args] Runs protected release publish and publish dry-run operations through the Bun -release command surface. The public no-product publish dry-run and selected -low-risk product dry-runs are handled in Bun, including the legacy --wasm -shortcut for the WASIX Rust SDK dry-run. Other product dry-runs and protected -publish dispatch still delegate to release.py while the protected -implementation is ported. +release command surface. The public no-product publish dry-run and product +dry-runs are handled in Bun, including the legacy --wasm shortcut for the WASIX +Rust SDK dry-run. Protected publish dispatch still delegates to release.py while +the protected implementation is ported. `); } @@ -106,14 +105,15 @@ function productPublishDryRunPlan(args) { let requested; try { requested = JSON.parse(productsJson); - } catch { - return null; + } catch (error) { + fail(`--products-json must be valid JSON: ${error.message}`); } if (!Array.isArray(requested) || requested.length === 0 || !requested.every((item) => typeof item === "string")) { - return null; + fail("--products-json must be a non-empty JSON string array"); } - if (!requested.every((product) => SUPPORTED_BUN_PRODUCT_DRY_RUNS.has(product))) { - return null; + const unsupportedRequested = requested.filter((product) => !SUPPORTED_BUN_PRODUCT_DRY_RUNS.has(product)); + if (unsupportedRequested.length > 0) { + fail(`unsupported Bun product publish dry-run selection: ${unsupportedRequested.join(", ")}`); } const ordered = jsonOutput([ "tools/release/release_graph_query.mjs", @@ -122,10 +122,11 @@ function productPublishDryRunPlan(args) { JSON.stringify(requested), ]); if (!Array.isArray(ordered) || ordered.length === 0 || !ordered.every((item) => typeof item === "string")) { - return null; + fail("release graph could not resolve the selected publish dry-run products"); } - if (!ordered.every((product) => SUPPORTED_BUN_PRODUCT_DRY_RUNS.has(product))) { - return null; + const unsupportedOrdered = ordered.filter((product) => !SUPPORTED_BUN_PRODUCT_DRY_RUNS.has(product)); + if (unsupportedOrdered.length > 0) { + fail(`release graph selected unsupported Bun product publish dry-run dependencies: ${unsupportedOrdered.join(", ")}`); } return { allowDirty: args.includes("--allow-dirty"), @@ -163,6 +164,10 @@ if (legacyWasmDryRunPlan !== null) { process.exit(0); } +if (command === "publish-dry-run") { + fail("publish-dry-run is Bun-owned; unsupported arguments must fail before the protected release.py publish fallback"); +} + const result = spawnSync("tools/release/release.py", argv, { cwd: ROOT, stdio: "inherit", From 392f9514ef5d5452e8861b88c69c36d9926b5e88 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 08:11:31 +0000 Subject: [PATCH 283/308] fix: publish staged github assets in bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 9 ++ tools/policy/check-tooling-stack.sh | 8 ++ tools/release/check_release_metadata.py | 8 +- tools/release/release-product-dry-run.mjs | 8 +- tools/release/release-publish.mjs | 121 ++++++++++++++++++ 5 files changed, 149 insertions(+), 5 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 437f224d..5e3152db 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -82,6 +82,15 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-28: Moved staged runtime/helper GitHub release asset publish steps + into the Bun `tools/release/release-publish.mjs` wrapper for + `liboliphaunt-native`, `liboliphaunt-wasix`, `oliphaunt-broker`, and + `oliphaunt-node-direct`. These routes now verify the product tag, reuse the + Bun release-asset staging/validation helpers, and upload through the existing + Bun GitHub release asset uploader before any protected Python fallback. + Protected registry publish steps remain in `release.py` while their package + publication semantics are ported. Release metadata and tooling-stack guards + require the Bun wrapper to keep owning these staged GitHub asset routes. - 2026-06-28: Closed the last product-scoped `publish-dry-run` fallback to `tools/release/release.py`. A direct graph comparison found all 49 release products in `SUPPORTED_BUN_PRODUCT_DRY_RUNS`, so diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index ada93a51..21c19a1e 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -444,6 +444,14 @@ if grep -Fq 'Other product dry-runs' tools/release/release-publish.mjs; then fi grep -Fq 'publish-dry-run is Bun-owned' tools/release/release-publish.mjs || fail "release-publish must fail closed before release.py fallback for unsupported publish-dry-run arguments" +grep -Fq 'GITHUB_RELEASE_ASSET_PUBLISHERS' tools/release/release-publish.mjs || + fail "release-publish must route staged runtime/helper GitHub asset publish steps in Bun" +grep -Fq 'publishGithubReleaseAssets' tools/release/release-publish.mjs || + fail "release-publish must publish staged runtime/helper GitHub release assets through the Bun wrapper" +for github_asset_product in liboliphaunt-native liboliphaunt-wasix oliphaunt-broker oliphaunt-node-direct; do + grep -Fq "\"$github_asset_product\"" tools/release/release-publish.mjs || + fail "release-publish must own $github_asset_product GitHub release asset publishing in Bun" +done tools/dev/bun.sh -e ' import { spawnSync } from "node:child_process"; import { SUPPORTED_BUN_PRODUCT_DRY_RUNS } from "./tools/release/release-product-dry-run.mjs"; diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 32396a73..aa64b419 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1012,6 +1012,12 @@ def validate_publish_target_coverage() -> None: or "--wasm dry-runs, and protected publish dispatch still delegate to release.py" in release_publish or "Other product dry-runs" in release_publish or "publish-dry-run is Bun-owned" not in release_publish + or "GITHUB_RELEASE_ASSET_PUBLISHERS" not in release_publish + or "publishGithubReleaseAssets" not in release_publish + or '"liboliphaunt-native"' not in release_publish + or '"liboliphaunt-wasix"' not in release_publish + or '"oliphaunt-broker"' not in release_publish + or '"oliphaunt-node-direct"' not in release_publish or "SUPPORTED_SDK_PRODUCT_DRY_RUNS" not in release_product_dry_run or "LIBOLIPHAUNT_NATIVE_PRODUCT," not in release_product_dry_run or "ensureLiboliphauntReleaseAssets" not in release_product_dry_run @@ -1046,7 +1052,7 @@ def validate_publish_target_coverage() -> None: or "prepareOliphauntWasixReleaseSource" not in release_sdk_product_dry_run or 'spawnSync("tools/release/release.py", argv' not in release_publish ): - fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, and React Native SDK tasks must not track release.py directly") + fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper GitHub asset publish steps must run in Bun, and React Native SDK tasks must not track release.py directly") 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 diff --git a/tools/release/release-product-dry-run.mjs b/tools/release/release-product-dry-run.mjs index fa4ca0c7..21502657 100644 --- a/tools/release/release-product-dry-run.mjs +++ b/tools/release/release-product-dry-run.mjs @@ -213,7 +213,7 @@ function hasLiboliphauntReleaseArchive(assetDir) { ); } -function ensureLiboliphauntReleaseAssets() { +export function ensureLiboliphauntReleaseAssets() { const assetDir = path.join(ROOT, "target/liboliphaunt/release-assets"); if (!hasLiboliphauntReleaseArchive(assetDir)) { copyStagedRuntimeAssets({ @@ -257,7 +257,7 @@ function ensureLiboliphauntReleaseAssets() { ]); } -function ensureBrokerReleaseAssets() { +export function ensureBrokerReleaseAssets() { const assetDir = path.join(ROOT, "target/oliphaunt-broker/release-assets"); if (!hasBrokerReleaseArchive(assetDir)) { copyStagedRuntimeAssets({ @@ -288,7 +288,7 @@ function ensureBrokerReleaseAssets() { ]); } -function ensureWasixReleaseAssets() { +export function ensureWasixReleaseAssets() { const assetDir = path.join(ROOT, "target/oliphaunt-wasix/release-assets"); if (!hasWasixReleaseArchive(assetDir)) { copyStagedRuntimeAssets({ @@ -319,7 +319,7 @@ function ensureWasixReleaseAssets() { ]); } -function ensureNodeDirectReleaseAssets() { +export function ensureNodeDirectReleaseAssets() { const assetDir = path.join(ROOT, "target/oliphaunt-node-direct/release-assets"); if (!hasNodeDirectReleaseArchive(assetDir)) { copyStagedRuntimeAssets({ diff --git a/tools/release/release-publish.mjs b/tools/release/release-publish.mjs index eedf1d23..7b64105a 100755 --- a/tools/release/release-publish.mjs +++ b/tools/release/release-publish.mjs @@ -1,9 +1,15 @@ #!/usr/bin/env bun import { spawnSync } from "node:child_process"; +import { readdirSync, statSync } from "node:fs"; +import path from "node:path"; import { ROOT, run } from "./release-cli-utils.mjs"; import { SUPPORTED_BUN_PRODUCT_DRY_RUNS, + ensureBrokerReleaseAssets, + ensureLiboliphauntReleaseAssets, + ensureNodeDirectReleaseAssets, + ensureWasixReleaseAssets, runBunProductDryRun, } from "./release-product-dry-run.mjs"; @@ -29,6 +35,40 @@ function fail(message, exitCode = 2) { const argv = Bun.argv.slice(2); const command = argv[0]; const LEGACY_WASM_DRY_RUN_PRODUCT = "oliphaunt-wasix-rust"; +const GITHUB_RELEASE_ASSET_PUBLISHERS = new Map([ + [ + "liboliphaunt-native", + { + assetDir: "target/liboliphaunt/release-assets", + ensure: ensureLiboliphauntReleaseAssets, + suffixes: [".tar.gz", ".tar.zst", ".tsv", ".zip", ".sha256"], + }, + ], + [ + "liboliphaunt-wasix", + { + assetDir: "target/oliphaunt-wasix/release-assets", + ensure: ensureWasixReleaseAssets, + suffixes: [".tar.zst", ".sha256"], + }, + ], + [ + "oliphaunt-broker", + { + assetDir: "target/oliphaunt-broker/release-assets", + ensure: ensureBrokerReleaseAssets, + suffixes: [".tar.gz", ".zip", ".sha256"], + }, + ], + [ + "oliphaunt-node-direct", + { + assetDir: "target/oliphaunt-node-direct/release-assets", + ensure: ensureNodeDirectReleaseAssets, + suffixes: [".tar.gz", ".zip", ".sha256"], + }, + ], +]); if (command === "-h" || command === "--help") { usage(); @@ -64,6 +104,41 @@ function flagValue(args, flag) { return null; } +function rel(file) { + return path.relative(ROOT, file).split(path.sep).join("/"); +} + +function isFile(file) { + try { + return statSync(file).isFile(); + } catch { + return false; + } +} + +function isDirectory(file) { + try { + return statSync(file).isDirectory(); + } catch { + return false; + } +} + +function globReleaseAssets(assetDir, suffixes) { + if (!isDirectory(assetDir)) { + fail(`release asset directory does not exist: ${rel(assetDir)}`); + } + const assets = readdirSync(assetDir) + .map((name) => path.join(assetDir, name)) + .filter((file) => isFile(file) && suffixes.some((suffix) => file.endsWith(suffix))) + .sort((left, right) => rel(left).localeCompare(rel(right))) + .map(rel); + if (assets.length === 0) { + fail(`no release assets found in ${rel(assetDir)}`); + } + return assets; +} + function noProductPublishDryRunPassthrough(args) { if (args.includes("--wasm") || selectsProducts(args)) { return null; @@ -82,6 +157,43 @@ function legacyWasmPublishDryRunPlan(args) { }; } +function publishProductStepPlan(args) { + const product = flagValue(args, "--product"); + const step = flagValue(args, "--step"); + if (product === null && step === null) { + return null; + } + if (product === null || step === null) { + return null; + } + return { + headRef: flagValue(args, "--head-ref") ?? "HEAD", + product, + step, + }; +} + +function verifyReleaseTag(product, headRef) { + run(TOOL, ["tools/dev/bun.sh", "tools/release/verify_product_tag.mjs", product, "--target", headRef]); +} + +function uploadGithubReleaseAssets(product, assets) { + const command = ["tools/dev/bun.sh", "tools/release/upload_github_release_assets.mjs", product]; + for (const asset of assets) { + command.push("--asset", asset); + } + run(TOOL, command); +} + +function publishGithubReleaseAssets(product, headRef, publisher) { + verifyReleaseTag(product, headRef); + publisher.ensure(); + uploadGithubReleaseAssets( + product, + globReleaseAssets(path.join(ROOT, publisher.assetDir), publisher.suffixes), + ); +} + function jsonOutput(args) { const result = spawnSync("tools/dev/bun.sh", args, { cwd: ROOT, @@ -168,6 +280,15 @@ if (command === "publish-dry-run") { fail("publish-dry-run is Bun-owned; unsupported arguments must fail before the protected release.py publish fallback"); } +const publishProductStep = command === "publish" ? publishProductStepPlan(argv.slice(1)) : null; +if (publishProductStep?.step === "github-release-assets") { + const publisher = GITHUB_RELEASE_ASSET_PUBLISHERS.get(publishProductStep.product); + if (publisher !== undefined) { + publishGithubReleaseAssets(publishProductStep.product, publishProductStep.headRef, publisher); + process.exit(0); + } +} + const result = spawnSync("tools/release/release.py", argv, { cwd: ROOT, stdio: "inherit", From 54e53c42c9d25d019ebbc82fab91e0c837acb3c5 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 08:19:33 +0000 Subject: [PATCH 284/308] fix: publish extension github assets in bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 10 ++ tools/policy/check-tooling-stack.sh | 6 ++ tools/release/check_release_metadata.py | 5 +- tools/release/release-product-dry-run.mjs | 2 +- tools/release/release-publish.mjs | 91 +++++++++++++++---- 5 files changed, 92 insertions(+), 22 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 5e3152db..aba13a36 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -82,6 +82,16 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-28: Moved exact-extension GitHub release asset publish routing into + the Bun `tools/release/release-publish.mjs` wrapper for both + `--product --step github-release-assets` and selected-extension + batch publishes from `--products-json`. The Bun path derives extension + products from `exactExtensionProducts(TOOL)`, verifies each product tag, uses + the shared staged exact-extension asset validator, and uploads through the + existing Bun GitHub release asset uploader. Extension Maven publication + remains in protected publish dispatch while registry publish semantics are + ported. Release metadata and tooling-stack guards require this exact-extension + GitHub asset route to stay Bun-owned. - 2026-06-28: Moved staged runtime/helper GitHub release asset publish steps into the Bun `tools/release/release-publish.mjs` wrapper for `liboliphaunt-native`, `liboliphaunt-wasix`, `oliphaunt-broker`, and diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 21c19a1e..7a72b5e2 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -448,6 +448,12 @@ grep -Fq 'GITHUB_RELEASE_ASSET_PUBLISHERS' tools/release/release-publish.mjs || fail "release-publish must route staged runtime/helper GitHub asset publish steps in Bun" grep -Fq 'publishGithubReleaseAssets' tools/release/release-publish.mjs || fail "release-publish must publish staged runtime/helper GitHub release assets through the Bun wrapper" +grep -Fq 'extensionAssetPaths' tools/release/release-publish.mjs || + fail "release-publish must publish staged exact-extension GitHub release assets through the Bun wrapper" +grep -Fq 'publishSelectedExtensionGithubReleaseAssets' tools/release/release-publish.mjs || + fail "release-publish must own selected exact-extension GitHub release asset publish batches in Bun" +grep -Fq 'exactExtensionProducts(TOOL)' tools/release/release-publish.mjs || + fail "release-publish must derive exact-extension publish routing from the canonical extension product set" for github_asset_product in liboliphaunt-native liboliphaunt-wasix oliphaunt-broker oliphaunt-node-direct; do grep -Fq "\"$github_asset_product\"" tools/release/release-publish.mjs || fail "release-publish must own $github_asset_product GitHub release asset publishing in Bun" diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index aa64b419..725f5367 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1014,6 +1014,9 @@ def validate_publish_target_coverage() -> None: or "publish-dry-run is Bun-owned" not in release_publish or "GITHUB_RELEASE_ASSET_PUBLISHERS" not in release_publish or "publishGithubReleaseAssets" not in release_publish + or "extensionAssetPaths" not in release_publish + or "publishSelectedExtensionGithubReleaseAssets" not in release_publish + or "exactExtensionProducts(TOOL)" not in release_publish or '"liboliphaunt-native"' not in release_publish or '"liboliphaunt-wasix"' not in release_publish or '"oliphaunt-broker"' not in release_publish @@ -1052,7 +1055,7 @@ def validate_publish_target_coverage() -> None: or "prepareOliphauntWasixReleaseSource" not in release_sdk_product_dry_run or 'spawnSync("tools/release/release.py", argv' not in release_publish ): - fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper GitHub asset publish steps must run in Bun, and React Native SDK tasks must not track release.py directly") + fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper and exact-extension GitHub asset publish steps must run in Bun, and React Native SDK tasks must not track release.py directly") 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 diff --git a/tools/release/release-product-dry-run.mjs b/tools/release/release-product-dry-run.mjs index 21502657..8652d06b 100644 --- a/tools/release/release-product-dry-run.mjs +++ b/tools/release/release-product-dry-run.mjs @@ -1243,7 +1243,7 @@ function extensionPackageDir(product) { return path.join(ROOT, "target/extension-artifacts", product); } -function extensionAssetPaths(product) { +export function extensionAssetPaths(product) { run(TOOL, [ "tools/dev/bun.sh", "tools/release/check-staged-artifacts.mjs", diff --git a/tools/release/release-publish.mjs b/tools/release/release-publish.mjs index 7b64105a..ebbc8c80 100755 --- a/tools/release/release-publish.mjs +++ b/tools/release/release-publish.mjs @@ -10,8 +10,13 @@ import { ensureLiboliphauntReleaseAssets, ensureNodeDirectReleaseAssets, ensureWasixReleaseAssets, + extensionAssetPaths, runBunProductDryRun, } from "./release-product-dry-run.mjs"; +import { + compareText, + exactExtensionProducts, +} from "./release-artifact-targets.mjs"; const TOOL = "release-publish.mjs"; const COMMANDS = new Set(["publish", "publish-dry-run"]); @@ -35,6 +40,7 @@ function fail(message, exitCode = 2) { const argv = Bun.argv.slice(2); const command = argv[0]; const LEGACY_WASM_DRY_RUN_PRODUCT = "oliphaunt-wasix-rust"; +const EXTENSION_PRODUCTS = new Set(exactExtensionProducts(TOOL)); const GITHUB_RELEASE_ASSET_PUBLISHERS = new Map([ [ "liboliphaunt-native", @@ -157,6 +163,36 @@ function legacyWasmPublishDryRunPlan(args) { }; } +function parseProductsJson(args) { + const productsJson = flagValue(args, "--products-json"); + if (productsJson === null) { + return null; + } + let requested; + try { + requested = JSON.parse(productsJson); + } catch (error) { + fail(`--products-json must be valid JSON: ${error.message}`); + } + if (!Array.isArray(requested) || requested.length === 0 || !requested.every((item) => typeof item === "string")) { + fail("--products-json must be a non-empty JSON string array"); + } + return requested; +} + +function releaseOrderedProducts(requested) { + const ordered = jsonOutput([ + "tools/release/release_graph_query.mjs", + "release-order", + "--products-json", + JSON.stringify(requested), + ]); + if (!Array.isArray(ordered) || ordered.length === 0 || !ordered.every((item) => typeof item === "string")) { + fail("release graph could not resolve the selected publish products"); + } + return ordered; +} + function publishProductStepPlan(args) { const product = flagValue(args, "--product"); const step = flagValue(args, "--step"); @@ -194,6 +230,23 @@ function publishGithubReleaseAssets(product, headRef, publisher) { ); } +function publishExtensionGithubReleaseAssets(product, headRef) { + verifyReleaseTag(product, headRef); + uploadGithubReleaseAssets(product, extensionAssetPaths(product)); +} + +function publishSelectedExtensionGithubReleaseAssets(products, headRef) { + const extensions = products + .filter((product) => EXTENSION_PRODUCTS.has(product)) + .sort(compareText); + if (extensions.length === 0) { + fail("no extension products selected"); + } + for (const product of extensions) { + publishExtensionGithubReleaseAssets(product, headRef); + } +} + function jsonOutput(args) { const result = spawnSync("tools/dev/bun.sh", args, { cwd: ROOT, @@ -210,32 +263,15 @@ function jsonOutput(args) { } function productPublishDryRunPlan(args) { - const productsJson = flagValue(args, "--products-json"); - if (productsJson === null) { + const requested = parseProductsJson(args); + if (requested === null) { return null; } - let requested; - try { - requested = JSON.parse(productsJson); - } catch (error) { - fail(`--products-json must be valid JSON: ${error.message}`); - } - if (!Array.isArray(requested) || requested.length === 0 || !requested.every((item) => typeof item === "string")) { - fail("--products-json must be a non-empty JSON string array"); - } const unsupportedRequested = requested.filter((product) => !SUPPORTED_BUN_PRODUCT_DRY_RUNS.has(product)); if (unsupportedRequested.length > 0) { fail(`unsupported Bun product publish dry-run selection: ${unsupportedRequested.join(", ")}`); } - const ordered = jsonOutput([ - "tools/release/release_graph_query.mjs", - "release-order", - "--products-json", - JSON.stringify(requested), - ]); - if (!Array.isArray(ordered) || ordered.length === 0 || !ordered.every((item) => typeof item === "string")) { - fail("release graph could not resolve the selected publish dry-run products"); - } + const ordered = releaseOrderedProducts(requested); const unsupportedOrdered = ordered.filter((product) => !SUPPORTED_BUN_PRODUCT_DRY_RUNS.has(product)); if (unsupportedOrdered.length > 0) { fail(`release graph selected unsupported Bun product publish dry-run dependencies: ${unsupportedOrdered.join(", ")}`); @@ -287,6 +323,21 @@ if (publishProductStep?.step === "github-release-assets") { publishGithubReleaseAssets(publishProductStep.product, publishProductStep.headRef, publisher); process.exit(0); } + if (EXTENSION_PRODUCTS.has(publishProductStep.product)) { + publishExtensionGithubReleaseAssets(publishProductStep.product, publishProductStep.headRef); + process.exit(0); + } +} + +if (command === "publish" && flagValue(argv.slice(1), "--step") === "github-release-assets" && flagValue(argv.slice(1), "--product") === null) { + const requested = parseProductsJson(argv.slice(1)); + if (requested !== null) { + publishSelectedExtensionGithubReleaseAssets( + releaseOrderedProducts(requested), + flagValue(argv.slice(1), "--head-ref") ?? "HEAD", + ); + process.exit(0); + } } const result = spawnSync("tools/release/release.py", argv, { From 00c4d35bc941c9b4851dc8455c61d30783f94f82 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 08:30:05 +0000 Subject: [PATCH 285/308] fix: publish extension maven artifacts in bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 10 +++ tools/policy/check-tooling-stack.sh | 6 ++ tools/release/check_release_metadata.py | 5 +- tools/release/release-product-dry-run.mjs | 4 +- tools/release/release-publish.mjs | 89 +++++++++++++++++++ 5 files changed, 111 insertions(+), 3 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index aba13a36..05d995a9 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -82,6 +82,16 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-28: Moved exact-extension Maven publication routing into the Bun + `tools/release/release-publish.mjs` wrapper for both + `--product --step maven-central` and selected-extension + `--products-json` batches. The Bun path derives extension products from the + canonical extension product set, verifies release tags, validates staged + exact-extension artifacts, builds the Maven artifact manifest through the + shared Bun helper, runs Gradle `publishAndReleaseToMavenCentral` only when + Maven artifacts are not already published, and verifies publication through + the Bun registry checker. Release metadata and tooling-stack guards require + this exact-extension Maven route to stay Bun-owned. - 2026-06-28: Moved exact-extension GitHub release asset publish routing into the Bun `tools/release/release-publish.mjs` wrapper for both `--product --step github-release-assets` and selected-extension diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 7a72b5e2..3ba80891 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -452,6 +452,12 @@ grep -Fq 'extensionAssetPaths' tools/release/release-publish.mjs || fail "release-publish must publish staged exact-extension GitHub release assets through the Bun wrapper" grep -Fq 'publishSelectedExtensionGithubReleaseAssets' tools/release/release-publish.mjs || fail "release-publish must own selected exact-extension GitHub release asset publish batches in Bun" +grep -Fq 'publishSelectedExtensionMaven' tools/release/release-publish.mjs || + fail "release-publish must own selected exact-extension Maven publication in Bun" +grep -Fq ':oliphaunt-maven-artifacts:publishAndReleaseToMavenCentral' tools/release/release-publish.mjs || + fail "release-publish must run exact-extension Maven Central publication through the Bun wrapper" +grep -Fq 'requireExtensionMavenArtifactsPublished' tools/release/release-publish.mjs || + fail "release-publish must verify exact-extension Maven publication through the registry checker" grep -Fq 'exactExtensionProducts(TOOL)' tools/release/release-publish.mjs || fail "release-publish must derive exact-extension publish routing from the canonical extension product set" for github_asset_product in liboliphaunt-native liboliphaunt-wasix oliphaunt-broker oliphaunt-node-direct; do diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 725f5367..b23b4219 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1016,6 +1016,9 @@ def validate_publish_target_coverage() -> None: or "publishGithubReleaseAssets" not in release_publish or "extensionAssetPaths" not in release_publish or "publishSelectedExtensionGithubReleaseAssets" not in release_publish + or "publishSelectedExtensionMaven" not in release_publish + or ":oliphaunt-maven-artifacts:publishAndReleaseToMavenCentral" not in release_publish + or "requireExtensionMavenArtifactsPublished" not in release_publish or "exactExtensionProducts(TOOL)" not in release_publish or '"liboliphaunt-native"' not in release_publish or '"liboliphaunt-wasix"' not in release_publish @@ -1055,7 +1058,7 @@ def validate_publish_target_coverage() -> None: or "prepareOliphauntWasixReleaseSource" not in release_sdk_product_dry_run or 'spawnSync("tools/release/release.py", argv' not in release_publish ): - fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper and exact-extension GitHub asset publish steps must run in Bun, and React Native SDK tasks must not track release.py directly") + fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper and exact-extension GitHub asset publish steps must run in Bun, exact-extension Maven publication must run in Bun, and React Native SDK tasks must not track release.py directly") 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 diff --git a/tools/release/release-product-dry-run.mjs b/tools/release/release-product-dry-run.mjs index 8652d06b..d24f9296 100644 --- a/tools/release/release-product-dry-run.mjs +++ b/tools/release/release-product-dry-run.mjs @@ -1265,7 +1265,7 @@ export function extensionAssetPaths(product) { return assets.map(rel); } -function buildMavenArtifactManifest(name, { runtime = false, extensions = false, extensionProducts = [] } = {}) { +export function buildMavenArtifactManifest(name, { runtime = false, extensions = false, extensionProducts = [] } = {}) { const outputPath = path.join(ROOT, "target/release/maven-artifacts", `${name}.tsv`); const command = [ "tools/dev/bun.sh", @@ -1286,7 +1286,7 @@ function buildMavenArtifactManifest(name, { runtime = false, extensions = false, return outputPath; } -function runMavenArtifactPublisher(manifest, task, cacheSlug) { +export function runMavenArtifactPublisher(manifest, task, cacheSlug) { run(TOOL, [ "src/sdks/kotlin/gradlew", "-p", diff --git a/tools/release/release-publish.mjs b/tools/release/release-publish.mjs index ebbc8c80..08a613fb 100755 --- a/tools/release/release-publish.mjs +++ b/tools/release/release-publish.mjs @@ -6,12 +6,14 @@ import path from "node:path"; import { ROOT, run } from "./release-cli-utils.mjs"; import { SUPPORTED_BUN_PRODUCT_DRY_RUNS, + buildMavenArtifactManifest, ensureBrokerReleaseAssets, ensureLiboliphauntReleaseAssets, ensureNodeDirectReleaseAssets, ensureWasixReleaseAssets, extensionAssetPaths, runBunProductDryRun, + runMavenArtifactPublisher, } from "./release-product-dry-run.mjs"; import { compareText, @@ -20,6 +22,10 @@ import { const TOOL = "release-publish.mjs"; const COMMANDS = new Set(["publish", "publish-dry-run"]); +const REGISTRY_PUBLICATION_CHECK = [ + "tools/dev/bun.sh", + "tools/release/check_registry_publication.mjs", +]; function usage() { console.log(`usage: tools/release/release-publish.mjs [publish args] @@ -247,6 +253,73 @@ function publishSelectedExtensionGithubReleaseAssets(products, headRef) { } } +function registryPublicationCheck(args) { + run(TOOL, [...REGISTRY_PUBLICATION_CHECK, ...args]); +} + +function registryPublicationCheckSucceeds(args) { + const result = spawnSync(REGISTRY_PUBLICATION_CHECK[0], [...REGISTRY_PUBLICATION_CHECK.slice(1), ...args], { + cwd: ROOT, + encoding: "utf8", + stdio: "ignore", + }); + if (result.error !== undefined) { + fail(`registry publication check failed to start: ${result.error.message}`); + } + return result.status === 0; +} + +function extensionMavenArtifactsPublished(products) { + return registryPublicationCheckSucceeds([ + "--products-json", + JSON.stringify(products), + "--registry-kind", + "maven", + "--require-published", + ]); +} + +function requireExtensionMavenArtifactsPublished(products) { + registryPublicationCheck([ + "--products-json", + JSON.stringify(products), + "--registry-kind", + "maven", + "--require-published", + "--retries", + "12", + "--retry-delay", + "10", + ]); +} + +function publishSelectedExtensionMaven(products, headRef) { + const extensions = products + .filter((product) => EXTENSION_PRODUCTS.has(product)) + .sort(compareText); + if (extensions.length === 0) { + fail("no extension products selected"); + } + for (const product of extensions) { + verifyReleaseTag(product, headRef); + extensionAssetPaths(product); + } + const manifest = buildMavenArtifactManifest("selected-extensions", { + extensions: true, + extensionProducts: extensions, + }); + if (extensionMavenArtifactsPublished(extensions)) { + console.log("selected Oliphaunt extension Android artifacts are already published on Maven Central; skipping publishAndReleaseToMavenCentral."); + } else { + runMavenArtifactPublisher( + manifest, + ":oliphaunt-maven-artifacts:publishAndReleaseToMavenCentral", + "oliphaunt-extensions-maven-release", + ); + } + requireExtensionMavenArtifactsPublished(extensions); +} + function jsonOutput(args) { const result = spawnSync("tools/dev/bun.sh", args, { cwd: ROOT, @@ -340,6 +413,22 @@ if (command === "publish" && flagValue(argv.slice(1), "--step") === "github-rele } } +if (publishProductStep?.step === "maven-central" && EXTENSION_PRODUCTS.has(publishProductStep.product)) { + publishSelectedExtensionMaven([publishProductStep.product], publishProductStep.headRef); + process.exit(0); +} + +if (command === "publish" && flagValue(argv.slice(1), "--step") === "maven-central" && flagValue(argv.slice(1), "--product") === null) { + const requested = parseProductsJson(argv.slice(1)); + if (requested !== null) { + publishSelectedExtensionMaven( + releaseOrderedProducts(requested), + flagValue(argv.slice(1), "--head-ref") ?? "HEAD", + ); + process.exit(0); + } +} + const result = spawnSync("tools/release/release.py", argv, { cwd: ROOT, stdio: "inherit", From c6de76a1efeac85361514ab9bd743b7665c953a4 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 08:35:09 +0000 Subject: [PATCH 286/308] fix: publish runtime maven artifacts in bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 8 +++ tools/policy/check-tooling-stack.sh | 6 +++ tools/release/check_release_metadata.py | 5 +- tools/release/release-publish.mjs | 50 +++++++++++++++++++ 4 files changed, 68 insertions(+), 1 deletion(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 05d995a9..df14ffdd 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -82,6 +82,14 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-28: Moved `liboliphaunt-native --step maven-central` + publication routing into the Bun `tools/release/release-publish.mjs` wrapper. + The Bun path verifies the release tag, validates/stages liboliphaunt release + assets, builds the runtime Maven artifact manifest through the shared helper, + skips the Gradle Maven Central publish when the runtime Maven artifacts are + already published, and verifies Maven publication through the Bun registry + checker. Release metadata and tooling-stack guards require this route to stay + Bun-owned. - 2026-06-28: Moved exact-extension Maven publication routing into the Bun `tools/release/release-publish.mjs` wrapper for both `--product --step maven-central` and selected-extension diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 3ba80891..89e51e0c 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -458,6 +458,12 @@ grep -Fq ':oliphaunt-maven-artifacts:publishAndReleaseToMavenCentral' tools/rele fail "release-publish must run exact-extension Maven Central publication through the Bun wrapper" grep -Fq 'requireExtensionMavenArtifactsPublished' tools/release/release-publish.mjs || fail "release-publish must verify exact-extension Maven publication through the registry checker" +grep -Fq 'publishLiboliphauntRuntimeMaven' tools/release/release-publish.mjs || + fail "release-publish must own liboliphaunt-native Maven Central publication in Bun" +grep -Fq 'liboliphaunt-native-maven-release' tools/release/release-publish.mjs || + fail "release-publish must run liboliphaunt-native Maven Central publication through the Bun wrapper" +grep -Fq 'requireProductRegistryPublished(product, "maven")' tools/release/release-publish.mjs || + fail "release-publish must verify liboliphaunt-native Maven publication through the registry checker" grep -Fq 'exactExtensionProducts(TOOL)' tools/release/release-publish.mjs || fail "release-publish must derive exact-extension publish routing from the canonical extension product set" for github_asset_product in liboliphaunt-native liboliphaunt-wasix oliphaunt-broker oliphaunt-node-direct; do diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index b23b4219..f4506ca3 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1019,6 +1019,9 @@ def validate_publish_target_coverage() -> None: or "publishSelectedExtensionMaven" not in release_publish or ":oliphaunt-maven-artifacts:publishAndReleaseToMavenCentral" not in release_publish or "requireExtensionMavenArtifactsPublished" not in release_publish + or "publishLiboliphauntRuntimeMaven" not in release_publish + or "liboliphaunt-native-maven-release" not in release_publish + or 'requireProductRegistryPublished(product, "maven")' not in release_publish or "exactExtensionProducts(TOOL)" not in release_publish or '"liboliphaunt-native"' not in release_publish or '"liboliphaunt-wasix"' not in release_publish @@ -1058,7 +1061,7 @@ def validate_publish_target_coverage() -> None: or "prepareOliphauntWasixReleaseSource" not in release_sdk_product_dry_run or 'spawnSync("tools/release/release.py", argv' not in release_publish ): - fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper and exact-extension GitHub asset publish steps must run in Bun, exact-extension Maven publication must run in Bun, and React Native SDK tasks must not track release.py directly") + fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper and exact-extension GitHub asset publish steps must run in Bun, liboliphaunt-native and exact-extension Maven publication must run in Bun, and React Native SDK tasks must not track release.py directly") 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 diff --git a/tools/release/release-publish.mjs b/tools/release/release-publish.mjs index 08a613fb..0003d05e 100755 --- a/tools/release/release-publish.mjs +++ b/tools/release/release-publish.mjs @@ -17,6 +17,7 @@ import { } from "./release-product-dry-run.mjs"; import { compareText, + currentProductVersionSync, exactExtensionProducts, } from "./release-artifact-targets.mjs"; @@ -293,6 +294,50 @@ function requireExtensionMavenArtifactsPublished(products) { ]); } +function productRegistryPublished(product, registryKind) { + return registryPublicationCheckSucceeds([ + "--product", + product, + "--registry-kind", + registryKind, + "--require-published", + ]); +} + +function requireProductRegistryPublished(product, registryKind) { + registryPublicationCheck([ + "--product", + product, + "--registry-kind", + registryKind, + "--require-published", + "--retries", + "12", + "--retry-delay", + "10", + ]); +} + +function publishLiboliphauntRuntimeMaven(headRef) { + const product = "liboliphaunt-native"; + verifyReleaseTag(product, headRef); + ensureLiboliphauntReleaseAssets(); + const manifest = buildMavenArtifactManifest("liboliphaunt-native-runtime", { + runtime: true, + }); + const version = currentProductVersionSync(product, TOOL); + if (productRegistryPublished(product, "maven")) { + console.log(`dev.oliphaunt.runtime artifacts ${version} are already published on Maven Central; skipping publishAndReleaseToMavenCentral.`); + } else { + runMavenArtifactPublisher( + manifest, + ":oliphaunt-maven-artifacts:publishAndReleaseToMavenCentral", + "liboliphaunt-native-maven-release", + ); + } + requireProductRegistryPublished(product, "maven"); +} + function publishSelectedExtensionMaven(products, headRef) { const extensions = products .filter((product) => EXTENSION_PRODUCTS.has(product)) @@ -413,6 +458,11 @@ if (command === "publish" && flagValue(argv.slice(1), "--step") === "github-rele } } +if (publishProductStep?.product === "liboliphaunt-native" && publishProductStep.step === "maven-central") { + publishLiboliphauntRuntimeMaven(publishProductStep.headRef); + process.exit(0); +} + if (publishProductStep?.step === "maven-central" && EXTENSION_PRODUCTS.has(publishProductStep.product)) { publishSelectedExtensionMaven([publishProductStep.product], publishProductStep.headRef); process.exit(0); From 52b6160812b74dc98a72f0d77470cfd44fa163bc Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 08:39:18 +0000 Subject: [PATCH 287/308] fix: publish node direct npm artifacts in bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 7 +++ tools/policy/check-tooling-stack.sh | 8 ++++ tools/release/check_release_metadata.py | 6 ++- tools/release/release-product-dry-run.mjs | 2 +- tools/release/release-publish.mjs | 48 +++++++++++++++++-- 5 files changed, 65 insertions(+), 6 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index df14ffdd..1b9e5f0b 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -82,6 +82,13 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-28: Moved `oliphaunt-node-direct --step npm` publication + routing into the Bun `tools/release/release-publish.mjs` wrapper. The Bun + path verifies the release tag, validates the staged optional npm tarballs + through the same helper used by product dry-run, skips already-published npm + packages with `npm view`, publishes the CI-built tarballs directly, and + verifies registry publication through the Bun registry checker. Release + metadata and tooling-stack guards require this route to stay Bun-owned. - 2026-06-28: Moved `liboliphaunt-native --step maven-central` publication routing into the Bun `tools/release/release-publish.mjs` wrapper. The Bun path verifies the release tag, validates/stages liboliphaunt release diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 89e51e0c..3335f6f4 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -464,6 +464,14 @@ grep -Fq 'liboliphaunt-native-maven-release' tools/release/release-publish.mjs | fail "release-publish must run liboliphaunt-native Maven Central publication through the Bun wrapper" grep -Fq 'requireProductRegistryPublished(product, "maven")' tools/release/release-publish.mjs || fail "release-publish must verify liboliphaunt-native Maven publication through the registry checker" +grep -Fq 'publishNodeDirectNpmOptionalPackages' tools/release/release-publish.mjs || + fail "release-publish must own Node direct optional npm package publication in Bun" +grep -Fq 'nodeDirectOptionalNpmTarballs' tools/release/release-publish.mjs || + fail "release-publish must validate staged Node direct optional npm tarballs before publish" +grep -Fq 'npmPublishTarball(packageName, tarball, version)' tools/release/release-publish.mjs || + fail "release-publish must publish Node direct optional npm tarballs through the Bun wrapper" +grep -Fq 'requireProductRegistryPublished(product, null)' tools/release/release-publish.mjs || + fail "release-publish must verify Node direct npm publication through the registry checker" grep -Fq 'exactExtensionProducts(TOOL)' tools/release/release-publish.mjs || fail "release-publish must derive exact-extension publish routing from the canonical extension product set" for github_asset_product in liboliphaunt-native liboliphaunt-wasix oliphaunt-broker oliphaunt-node-direct; do diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index f4506ca3..e1ba74af 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1022,6 +1022,10 @@ def validate_publish_target_coverage() -> None: or "publishLiboliphauntRuntimeMaven" not in release_publish or "liboliphaunt-native-maven-release" not in release_publish or 'requireProductRegistryPublished(product, "maven")' not in release_publish + or "publishNodeDirectNpmOptionalPackages" not in release_publish + or "nodeDirectOptionalNpmTarballs" not in release_publish + or "npmPublishTarball(packageName, tarball, version)" not in release_publish + or "requireProductRegistryPublished(product, null)" not in release_publish or "exactExtensionProducts(TOOL)" not in release_publish or '"liboliphaunt-native"' not in release_publish or '"liboliphaunt-wasix"' not in release_publish @@ -1061,7 +1065,7 @@ def validate_publish_target_coverage() -> None: or "prepareOliphauntWasixReleaseSource" not in release_sdk_product_dry_run or 'spawnSync("tools/release/release.py", argv' not in release_publish ): - fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper and exact-extension GitHub asset publish steps must run in Bun, liboliphaunt-native and exact-extension Maven publication must run in Bun, and React Native SDK tasks must not track release.py directly") + fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper and exact-extension GitHub asset publish steps must run in Bun, liboliphaunt-native and exact-extension Maven publication must run in Bun, Node direct npm publication must run in Bun, and React Native SDK tasks must not track release.py directly") 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 diff --git a/tools/release/release-product-dry-run.mjs b/tools/release/release-product-dry-run.mjs index d24f9296..834d939f 100644 --- a/tools/release/release-product-dry-run.mjs +++ b/tools/release/release-product-dry-run.mjs @@ -973,7 +973,7 @@ async function validateNodeDirectOptionalTarball(packageName, version, tarball) } } -async function nodeDirectOptionalNpmTarballs(version) { +export async function nodeDirectOptionalNpmTarballs(version) { const tarballs = []; for (const [packageName] of nodeDirectOptionalPackageTargets(version)) { const tarball = expectedNodeDirectNpmTarball(packageName, version); diff --git a/tools/release/release-publish.mjs b/tools/release/release-publish.mjs index 0003d05e..2e1bc95b 100755 --- a/tools/release/release-publish.mjs +++ b/tools/release/release-publish.mjs @@ -12,6 +12,7 @@ import { ensureNodeDirectReleaseAssets, ensureWasixReleaseAssets, extensionAssetPaths, + nodeDirectOptionalNpmTarballs, runBunProductDryRun, runMavenArtifactPublisher, } from "./release-product-dry-run.mjs"; @@ -305,17 +306,51 @@ function productRegistryPublished(product, registryKind) { } function requireProductRegistryPublished(product, registryKind) { - registryPublicationCheck([ + const args = [ "--product", product, - "--registry-kind", - registryKind, "--require-published", "--retries", "12", "--retry-delay", "10", - ]); + ]; + if (registryKind !== null) { + args.splice(2, 0, "--registry-kind", registryKind); + } + registryPublicationCheck(args); +} + +function npmPackagePublished(packageName, version) { + const result = spawnSync("npm", ["view", `${packageName}@${version}`, "version"], { + cwd: ROOT, + encoding: "utf8", + stdio: "ignore", + }); + if (result.error !== undefined) { + fail(`npm view failed to start: ${result.error.message}`); + } + return result.status === 0; +} + +function npmPublishTarball(packageName, tarball, version) { + if (npmPackagePublished(packageName, version)) { + console.log(`${packageName} ${version} is already published on npm; skipping npm publish.`); + return; + } + run(TOOL, ["npm", "publish", tarball, "--access", "public", "--provenance"]); +} + +async function publishNodeDirectNpmOptionalPackages(headRef) { + const product = "oliphaunt-node-direct"; + verifyReleaseTag(product, headRef); + const version = currentProductVersionSync(product, TOOL); + ensureNodeDirectReleaseAssets(); + const tarballs = await nodeDirectOptionalNpmTarballs(version); + for (const [packageName, tarball] of tarballs) { + npmPublishTarball(packageName, tarball, version); + } + requireProductRegistryPublished(product, null); } function publishLiboliphauntRuntimeMaven(headRef) { @@ -463,6 +498,11 @@ if (publishProductStep?.product === "liboliphaunt-native" && publishProductStep. process.exit(0); } +if (publishProductStep?.product === "oliphaunt-node-direct" && publishProductStep.step === "npm") { + await publishNodeDirectNpmOptionalPackages(publishProductStep.headRef); + process.exit(0); +} + if (publishProductStep?.step === "maven-central" && EXTENSION_PRODUCTS.has(publishProductStep.product)) { publishSelectedExtensionMaven([publishProductStep.product], publishProductStep.headRef); process.exit(0); From a934bcd4d0b51571418c8dad780b62356c7eaef3 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 08:42:55 +0000 Subject: [PATCH 288/308] fix: publish broker npm artifacts in bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 7 +++++++ tools/policy/check-tooling-stack.sh | 6 ++++++ tools/release/check_release_metadata.py | 5 ++++- tools/release/release-product-dry-run.mjs | 2 +- tools/release/release-publish.mjs | 17 +++++++++++++++++ 5 files changed, 35 insertions(+), 2 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 1b9e5f0b..680679d1 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -82,6 +82,13 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-28: Moved `oliphaunt-broker --step npm` publication routing into + the Bun `tools/release/release-publish.mjs` wrapper. The Bun path verifies + the release tag, validates and packs broker npm artifacts through the same + helper used by product dry-run, skips already-published npm packages with + `npm view`, publishes the generated tarballs directly, and verifies npm + publication through the Bun registry checker. Release metadata and + tooling-stack guards require this route to stay Bun-owned. - 2026-06-28: Moved `oliphaunt-node-direct --step npm` publication routing into the Bun `tools/release/release-publish.mjs` wrapper. The Bun path verifies the release tag, validates the staged optional npm tarballs diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 3335f6f4..6930bc87 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -472,6 +472,12 @@ grep -Fq 'npmPublishTarball(packageName, tarball, version)' tools/release/releas fail "release-publish must publish Node direct optional npm tarballs through the Bun wrapper" grep -Fq 'requireProductRegistryPublished(product, null)' tools/release/release-publish.mjs || fail "release-publish must verify Node direct npm publication through the registry checker" +grep -Fq 'publishBrokerNpmPackages' tools/release/release-publish.mjs || + fail "release-publish must own broker npm package publication in Bun" +grep -Fq 'brokerNpmTarballs(version)' tools/release/release-publish.mjs || + fail "release-publish must validate staged broker npm tarballs before publish" +grep -Fq 'requireProductRegistryPublished(product, "npm")' tools/release/release-publish.mjs || + fail "release-publish must verify broker npm publication through the registry checker" grep -Fq 'exactExtensionProducts(TOOL)' tools/release/release-publish.mjs || fail "release-publish must derive exact-extension publish routing from the canonical extension product set" for github_asset_product in liboliphaunt-native liboliphaunt-wasix oliphaunt-broker oliphaunt-node-direct; do diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index e1ba74af..7086f225 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1026,6 +1026,9 @@ def validate_publish_target_coverage() -> None: or "nodeDirectOptionalNpmTarballs" not in release_publish or "npmPublishTarball(packageName, tarball, version)" not in release_publish or "requireProductRegistryPublished(product, null)" not in release_publish + or "publishBrokerNpmPackages" not in release_publish + or "brokerNpmTarballs(version)" not in release_publish + or 'requireProductRegistryPublished(product, "npm")' not in release_publish or "exactExtensionProducts(TOOL)" not in release_publish or '"liboliphaunt-native"' not in release_publish or '"liboliphaunt-wasix"' not in release_publish @@ -1065,7 +1068,7 @@ def validate_publish_target_coverage() -> None: or "prepareOliphauntWasixReleaseSource" not in release_sdk_product_dry_run or 'spawnSync("tools/release/release.py", argv' not in release_publish ): - fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper and exact-extension GitHub asset publish steps must run in Bun, liboliphaunt-native and exact-extension Maven publication must run in Bun, Node direct npm publication must run in Bun, and React Native SDK tasks must not track release.py directly") + fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper and exact-extension GitHub asset publish steps must run in Bun, liboliphaunt-native and exact-extension Maven publication must run in Bun, broker and Node direct npm publication must run in Bun, and React Native SDK tasks must not track release.py directly") 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 diff --git a/tools/release/release-product-dry-run.mjs b/tools/release/release-product-dry-run.mjs index 834d939f..47b268e2 100644 --- a/tools/release/release-product-dry-run.mjs +++ b/tools/release/release-product-dry-run.mjs @@ -911,7 +911,7 @@ function liboliphauntNpmTarballs(version) { return packages; } -function brokerNpmTarballs(version) { +export function brokerNpmTarballs(version) { const tarballs = []; const assetDir = path.join(ROOT, "target/oliphaunt-broker/release-assets"); for (const [packageName, packageDir, target] of brokerNpmPackageTargets(version)) { diff --git a/tools/release/release-publish.mjs b/tools/release/release-publish.mjs index 2e1bc95b..bcff35cd 100755 --- a/tools/release/release-publish.mjs +++ b/tools/release/release-publish.mjs @@ -6,6 +6,7 @@ import path from "node:path"; import { ROOT, run } from "./release-cli-utils.mjs"; import { SUPPORTED_BUN_PRODUCT_DRY_RUNS, + brokerNpmTarballs, buildMavenArtifactManifest, ensureBrokerReleaseAssets, ensureLiboliphauntReleaseAssets, @@ -353,6 +354,17 @@ async function publishNodeDirectNpmOptionalPackages(headRef) { requireProductRegistryPublished(product, null); } +function publishBrokerNpmPackages(headRef) { + const product = "oliphaunt-broker"; + verifyReleaseTag(product, headRef); + const version = currentProductVersionSync(product, TOOL); + ensureBrokerReleaseAssets(); + for (const [packageName, tarball] of brokerNpmTarballs(version)) { + npmPublishTarball(packageName, tarball, version); + } + requireProductRegistryPublished(product, "npm"); +} + function publishLiboliphauntRuntimeMaven(headRef) { const product = "liboliphaunt-native"; verifyReleaseTag(product, headRef); @@ -503,6 +515,11 @@ if (publishProductStep?.product === "oliphaunt-node-direct" && publishProductSte process.exit(0); } +if (publishProductStep?.product === "oliphaunt-broker" && publishProductStep.step === "npm") { + publishBrokerNpmPackages(publishProductStep.headRef); + process.exit(0); +} + if (publishProductStep?.step === "maven-central" && EXTENSION_PRODUCTS.has(publishProductStep.product)) { publishSelectedExtensionMaven([publishProductStep.product], publishProductStep.headRef); process.exit(0); From d1916b5351766159aec0a07d28418d6c36172ae5 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 08:46:41 +0000 Subject: [PATCH 289/308] fix: publish liboliphaunt npm artifacts in bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 8 ++++++++ tools/policy/check-tooling-stack.sh | 4 ++++ tools/release/check_release_metadata.py | 4 +++- tools/release/release-product-dry-run.mjs | 2 +- tools/release/release-publish.mjs | 17 +++++++++++++++++ 5 files changed, 33 insertions(+), 2 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 680679d1..9bf6ebb5 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -82,6 +82,14 @@ until the current-state gates here are checked with fresh local evidence. ### Current Fresh Evidence +- 2026-06-28: Moved `liboliphaunt-native --step npm` publication routing + into the Bun `tools/release/release-publish.mjs` wrapper. The Bun path + verifies the release tag, validates and packs runtime, split tools, and ICU + npm artifacts through the same helper used by product dry-run, skips + already-published npm packages with `npm view`, publishes the generated + tarballs directly, and verifies npm publication through the Bun registry + checker. Release metadata and tooling-stack guards require this route to stay + Bun-owned. - 2026-06-28: Moved `oliphaunt-broker --step npm` publication routing into the Bun `tools/release/release-publish.mjs` wrapper. The Bun path verifies the release tag, validates and packs broker npm artifacts through the same diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 6930bc87..319bdb73 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -478,6 +478,10 @@ grep -Fq 'brokerNpmTarballs(version)' tools/release/release-publish.mjs || fail "release-publish must validate staged broker npm tarballs before publish" grep -Fq 'requireProductRegistryPublished(product, "npm")' tools/release/release-publish.mjs || fail "release-publish must verify broker npm publication through the registry checker" +grep -Fq 'publishLiboliphauntNpmPackages' tools/release/release-publish.mjs || + fail "release-publish must own liboliphaunt-native npm package publication in Bun" +grep -Fq 'liboliphauntNpmTarballs(version)' tools/release/release-publish.mjs || + fail "release-publish must validate staged liboliphaunt-native npm tarballs before publish" grep -Fq 'exactExtensionProducts(TOOL)' tools/release/release-publish.mjs || fail "release-publish must derive exact-extension publish routing from the canonical extension product set" for github_asset_product in liboliphaunt-native liboliphaunt-wasix oliphaunt-broker oliphaunt-node-direct; do diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 7086f225..fc157b91 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1029,6 +1029,8 @@ def validate_publish_target_coverage() -> None: or "publishBrokerNpmPackages" not in release_publish or "brokerNpmTarballs(version)" not in release_publish or 'requireProductRegistryPublished(product, "npm")' not in release_publish + or "publishLiboliphauntNpmPackages" not in release_publish + or "liboliphauntNpmTarballs(version)" not in release_publish or "exactExtensionProducts(TOOL)" not in release_publish or '"liboliphaunt-native"' not in release_publish or '"liboliphaunt-wasix"' not in release_publish @@ -1068,7 +1070,7 @@ def validate_publish_target_coverage() -> None: or "prepareOliphauntWasixReleaseSource" not in release_sdk_product_dry_run or 'spawnSync("tools/release/release.py", argv' not in release_publish ): - fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper and exact-extension GitHub asset publish steps must run in Bun, liboliphaunt-native and exact-extension Maven publication must run in Bun, broker and Node direct npm publication must run in Bun, and React Native SDK tasks must not track release.py directly") + fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper and exact-extension GitHub asset publish steps must run in Bun, liboliphaunt-native and exact-extension Maven publication must run in Bun, liboliphaunt-native, broker, and Node direct npm publication must run in Bun, and React Native SDK tasks must not track release.py directly") 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 diff --git a/tools/release/release-product-dry-run.mjs b/tools/release/release-product-dry-run.mjs index 47b268e2..64116eea 100644 --- a/tools/release/release-product-dry-run.mjs +++ b/tools/release/release-product-dry-run.mjs @@ -874,7 +874,7 @@ function validatePackedIcuPackage(packageName, version, tarball) { } } -function liboliphauntNpmTarballs(version) { +export function liboliphauntNpmTarballs(version) { const packages = []; const runtimeStages = stageLiboliphauntNpmPayloads(version); const toolsStages = stageLiboliphauntToolsNpmPayloads(version); diff --git a/tools/release/release-publish.mjs b/tools/release/release-publish.mjs index bcff35cd..8772f7f4 100755 --- a/tools/release/release-publish.mjs +++ b/tools/release/release-publish.mjs @@ -13,6 +13,7 @@ import { ensureNodeDirectReleaseAssets, ensureWasixReleaseAssets, extensionAssetPaths, + liboliphauntNpmTarballs, nodeDirectOptionalNpmTarballs, runBunProductDryRun, runMavenArtifactPublisher, @@ -365,6 +366,17 @@ function publishBrokerNpmPackages(headRef) { requireProductRegistryPublished(product, "npm"); } +function publishLiboliphauntNpmPackages(headRef) { + const product = "liboliphaunt-native"; + verifyReleaseTag(product, headRef); + const version = currentProductVersionSync(product, TOOL); + ensureLiboliphauntReleaseAssets(); + for (const [packageName, tarball] of liboliphauntNpmTarballs(version)) { + npmPublishTarball(packageName, tarball, version); + } + requireProductRegistryPublished(product, "npm"); +} + function publishLiboliphauntRuntimeMaven(headRef) { const product = "liboliphaunt-native"; verifyReleaseTag(product, headRef); @@ -510,6 +522,11 @@ if (publishProductStep?.product === "liboliphaunt-native" && publishProductStep. process.exit(0); } +if (publishProductStep?.product === "liboliphaunt-native" && publishProductStep.step === "npm") { + publishLiboliphauntNpmPackages(publishProductStep.headRef); + process.exit(0); +} + if (publishProductStep?.product === "oliphaunt-node-direct" && publishProductStep.step === "npm") { await publishNodeDirectNpmOptionalPackages(publishProductStep.headRef); process.exit(0); From ac6e93d0ce1e7620386c4810c47a25e4701087d8 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 09:00:04 +0000 Subject: [PATCH 290/308] fix: publish react native npm artifacts in bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 17 +++ tools/policy/check-tooling-stack.sh | 8 ++ tools/release/check_release_metadata.py | 6 +- tools/release/release-publish.mjs | 20 +++ tools/release/release-sdk-product-dry-run.mjs | 115 +++++++++++++++++- 5 files changed, 162 insertions(+), 4 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 9bf6ebb5..8e3c686f 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -3004,3 +3004,20 @@ until the current-state gates here are checked with fresh local evidence. `oliphaunt-wasix` `tools` feature, or set `[package.metadata.oliphaunt] tools = true`, stage `oliphaunt-wasix-tools` and tools-AOT separately. The build-helper tests now cover both root-only and split-tools WASIX staging. +- On 2026-06-28, the protected React Native npm publish step moved onto the + Bun `release-publish.mjs` surface. The route preserves the Python behavior: + verify the product tag, skip tarball requirements when + `@oliphaunt/react-native` is already on npm, otherwise publish the single + staged SDK `.tgz`, verify registry publication, and upload the no-asset + GitHub release marker. `release-sdk-product-dry-run.mjs` now validates staged + SDK npm tarballs for exact filename, package name/version, absence of + `workspace:` specs, and built `package/lib` output before publish or dry-run. + Fresh local evidence passed for `node --check tools/release/release-publish.mjs`, + `node --check tools/release/release-sdk-product-dry-run.mjs`, + `tools/dev/bun.sh tools/release/release-publish.mjs publish --product oliphaunt-react-native --step npm --head-ref oliphaunt-not-a-ref` + failing at Bun tag verification, local `pnpm --dir src/sdks/react-native pack` + plus `tools/dev/bun.sh tools/release/release-sdk-product-dry-run.mjs --product oliphaunt-react-native --allow-dirty`, + `bash tools/policy/check-tooling-stack.sh`, + `tools/dev/bun.sh tools/release/check-release-metadata.mjs`, + `tools/dev/bun.sh tools/release/check_artifact_targets.mjs`, and + `tools/dev/bun.sh tools/release/check-consumer-shape.mjs`. diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 319bdb73..81c1a53f 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -482,6 +482,12 @@ grep -Fq 'publishLiboliphauntNpmPackages' tools/release/release-publish.mjs || fail "release-publish must own liboliphaunt-native npm package publication in Bun" grep -Fq 'liboliphauntNpmTarballs(version)' tools/release/release-publish.mjs || fail "release-publish must validate staged liboliphaunt-native npm tarballs before publish" +grep -Fq 'publishReactNativeNpm' tools/release/release-publish.mjs || + fail "release-publish must own React Native npm package publication in Bun" +grep -Fq 'stagedSdkNpmPackageTarball(product)' tools/release/release-publish.mjs || + fail "release-publish must validate the staged React Native npm tarball before publish" +grep -Fq 'uploadGithubReleaseAssets(product, [])' tools/release/release-publish.mjs || + fail "release-publish must preserve React Native no-asset GitHub release publication in Bun" grep -Fq 'exactExtensionProducts(TOOL)' tools/release/release-publish.mjs || fail "release-publish must derive exact-extension publish routing from the canonical extension product set" for github_asset_product in liboliphaunt-native liboliphaunt-wasix oliphaunt-broker oliphaunt-node-direct; do @@ -580,6 +586,8 @@ grep -Fq 'prepareStagedSwiftReleaseManifest' tools/release/release-sdk-product-d fail "Bun SDK product dry-runs must preserve Swift staged release manifest validation" grep -Fq 'stagedKotlinMavenRepo' tools/release/release-sdk-product-dry-run.mjs || fail "Bun SDK product dry-runs must preserve Kotlin staged Maven repository validation" +grep -Fq 'stagedSdkNpmPackageTarball(product)' tools/release/release-sdk-product-dry-run.mjs || + fail "Bun SDK product dry-runs must validate staged npm tarball identity and built output" grep -Fq 'verifyStagedCargoProductCrates("oliphaunt-rust")' tools/release/release-sdk-product-dry-run.mjs || fail "Bun SDK product dry-runs must preserve Rust staged Cargo crate validation" grep -Fq 'tools/release/prepare-rust-release-source.mjs' tools/release/release-sdk-product-dry-run.mjs || diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index fc157b91..de9d1b36 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1031,6 +1031,9 @@ def validate_publish_target_coverage() -> None: or 'requireProductRegistryPublished(product, "npm")' not in release_publish or "publishLiboliphauntNpmPackages" not in release_publish or "liboliphauntNpmTarballs(version)" not in release_publish + or "publishReactNativeNpm" not in release_publish + or "stagedSdkNpmPackageTarball(product)" not in release_publish + or "uploadGithubReleaseAssets(product, [])" not in release_publish or "exactExtensionProducts(TOOL)" not in release_publish or '"liboliphaunt-native"' not in release_publish or '"liboliphaunt-wasix"' not in release_publish @@ -1065,12 +1068,13 @@ def validate_publish_target_coverage() -> None: or 'tools/release/check-staged-artifacts.mjs", "--require-sdk-product", product' not in release_sdk_product_dry_run or "prepareStagedSwiftReleaseManifest" not in release_sdk_product_dry_run or "stagedKotlinMavenRepo" not in release_sdk_product_dry_run + or "stagedSdkNpmPackageTarball(product)" not in release_sdk_product_dry_run or 'verifyStagedCargoProductCrates("oliphaunt-rust")' not in release_sdk_product_dry_run or "tools/release/prepare-rust-release-source.mjs" not in release_sdk_product_dry_run or "prepareOliphauntWasixReleaseSource" not in release_sdk_product_dry_run or 'spawnSync("tools/release/release.py", argv' not in release_publish ): - fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper and exact-extension GitHub asset publish steps must run in Bun, liboliphaunt-native and exact-extension Maven publication must run in Bun, liboliphaunt-native, broker, and Node direct npm publication must run in Bun, and React Native SDK tasks must not track release.py directly") + fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper and exact-extension GitHub asset publish steps must run in Bun, liboliphaunt-native and exact-extension Maven publication must run in Bun, liboliphaunt-native, broker, Node direct, and React Native npm publication must run in Bun, and React Native SDK tasks must not track release.py directly") 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 diff --git a/tools/release/release-publish.mjs b/tools/release/release-publish.mjs index 8772f7f4..2a83299e 100755 --- a/tools/release/release-publish.mjs +++ b/tools/release/release-publish.mjs @@ -18,6 +18,7 @@ import { runBunProductDryRun, runMavenArtifactPublisher, } from "./release-product-dry-run.mjs"; +import { stagedSdkNpmPackageTarball } from "./release-sdk-product-dry-run.mjs"; import { compareText, currentProductVersionSync, @@ -377,6 +378,20 @@ function publishLiboliphauntNpmPackages(headRef) { requireProductRegistryPublished(product, "npm"); } +function publishReactNativeNpm(headRef) { + const product = "oliphaunt-react-native"; + const packageName = "@oliphaunt/react-native"; + verifyReleaseTag(product, headRef); + const version = currentProductVersionSync(product, TOOL); + if (npmPackagePublished(packageName, version)) { + console.log(`${packageName} ${version} is already published on npm; skipping npm publish.`); + } else { + npmPublishTarball(packageName, stagedSdkNpmPackageTarball(product), version); + } + requireProductRegistryPublished(product, null); + uploadGithubReleaseAssets(product, []); +} + function publishLiboliphauntRuntimeMaven(headRef) { const product = "liboliphaunt-native"; verifyReleaseTag(product, headRef); @@ -537,6 +552,11 @@ if (publishProductStep?.product === "oliphaunt-broker" && publishProductStep.ste process.exit(0); } +if (publishProductStep?.product === "oliphaunt-react-native" && publishProductStep.step === "npm") { + publishReactNativeNpm(publishProductStep.headRef); + process.exit(0); +} + if (publishProductStep?.step === "maven-central" && EXTENSION_PRODUCTS.has(publishProductStep.product)) { publishSelectedExtensionMaven([publishProductStep.product], publishProductStep.headRef); process.exit(0); diff --git a/tools/release/release-sdk-product-dry-run.mjs b/tools/release/release-sdk-product-dry-run.mjs index 8bb51bff..cf79292e 100644 --- a/tools/release/release-sdk-product-dry-run.mjs +++ b/tools/release/release-sdk-product-dry-run.mjs @@ -1,9 +1,10 @@ #!/usr/bin/env bun import { cpSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs"; import path from "node:path"; +import { gunzipSync } from "node:zlib"; import { ROOT, run } from "./release-cli-utils.mjs"; -import { currentProductVersionSync, registryPackageRows } from "./release-artifact-targets.mjs"; +import { currentProductVersionSync, registryPackageRows, releaseMetadata } from "./release-artifact-targets.mjs"; import { currentOliphauntWasixSdkVersion, prepareOliphauntWasixReleaseSource, @@ -184,6 +185,114 @@ function stagedKotlinMavenRepo() { console.log(`validated staged Kotlin Maven repository: ${rel(root)}`); } +function safeNpmPackageFilenamePrefix(packageName) { + return packageName.replace(/^@/u, "").replaceAll("/", "-"); +} + +function jsonContainsWorkspaceProtocol(value) { + if (typeof value === "string") { + return value.startsWith("workspace:"); + } + if (Array.isArray(value)) { + return value.some((item) => jsonContainsWorkspaceProtocol(item)); + } + if (value !== null && typeof value === "object") { + return Object.values(value).some((item) => jsonContainsWorkspaceProtocol(item)); + } + return false; +} + +function tarString(bytes, offset, length) { + const end = bytes.indexOf(0, offset); + const effectiveEnd = end >= offset && end < offset + length ? end : offset + length; + return bytes.toString("utf8", offset, effectiveEnd); +} + +function tarOctal(bytes, offset, length) { + const raw = tarString(bytes, offset, length).trim().replace(/\0.*$/u, ""); + if (raw.length === 0) { + return 0; + } + const value = Number.parseInt(raw, 8); + if (!Number.isFinite(value)) { + throw new Error(`invalid tar octal field ${JSON.stringify(raw)}`); + } + return value; +} + +function tarEntryName(bytes, offset) { + const name = tarString(bytes, offset, 100); + const prefix = tarString(bytes, offset + 345, 155); + return prefix ? `${prefix}/${name}` : name; +} + +function readTarGzEntries(tarball) { + const bytes = gunzipSync(readFileSync(tarball)); + const entries = new Map(); + for (let offset = 0; offset + 512 <= bytes.length;) { + const header = bytes.subarray(offset, offset + 512); + if (header.every((byte) => byte === 0)) { + break; + } + const name = tarEntryName(bytes, offset); + const size = tarOctal(bytes, offset + 124, 12); + const bodyStart = offset + 512; + const bodyEnd = bodyStart + size; + if (bodyEnd > bytes.length) { + throw new Error(`${rel(tarball)} has a truncated tar entry ${name}`); + } + entries.set(name, bytes.subarray(bodyStart, bodyEnd)); + offset = bodyStart + Math.ceil(size / 512) * 512; + } + return entries; +} + +function validateStagedNpmPackageTarball(product, tarball) { + const packageRoot = path.join(ROOT, releaseMetadata(product, TOOL).packagePath); + const packageJsonPath = path.join(packageRoot, "package.json"); + requireFile(packageJsonPath, `${product} has no package.json at ${rel(packageJsonPath)}`); + const sourcePackage = JSON.parse(readFileSync(packageJsonPath, "utf8")); + const expectedName = sourcePackage.name; + const expectedVersion = currentProductVersionSync(product, TOOL); + if (typeof expectedName !== "string" || expectedName.length === 0) { + fail(`${rel(packageJsonPath)} must declare a package name`); + } + const expectedFilename = `${safeNpmPackageFilenamePrefix(expectedName)}-${expectedVersion}.tgz`; + if (path.basename(tarball) !== expectedFilename) { + fail(`${product} staged npm tarball must be named ${expectedFilename}, got ${path.basename(tarball)}`); + } + try { + const entries = readTarGzEntries(tarball); + if (!entries.has("package/package.json")) { + fail(`${rel(tarball)} is missing package/package.json`); + } + const packedPackage = JSON.parse(entries.get("package/package.json").toString("utf8")); + if (packedPackage.name !== expectedName) { + fail(`${rel(tarball)} package name must be ${expectedName}, got ${JSON.stringify(packedPackage.name)}`); + } + if (packedPackage.version !== expectedVersion) { + fail(`${rel(tarball)} package version must be ${expectedVersion}, got ${JSON.stringify(packedPackage.version)}`); + } + if (jsonContainsWorkspaceProtocol(packedPackage)) { + fail(`${rel(tarball)} must not contain workspace: dependency specifiers`); + } + if (![...entries.keys()].some((name) => name.startsWith("package/lib/"))) { + fail(`${rel(tarball)} must contain built package/lib output`); + } + } catch (error) { + fail(`${rel(tarball)} is not a valid staged npm package tarball: ${error.message}`); + } +} + +export function stagedSdkNpmPackageTarball(product) { + const matches = requireStagedSdkArtifact(product, "npm package", [".tgz"]); + if (matches.length !== 1) { + fail(`${product} release requires exactly one staged npm package tarball, found ${matches.length}: ${matches.map(rel).join(", ")}`); + } + validateStagedNpmPackageTarball(product, matches[0]); + return matches[0]; +} + function stagedCargoCrates(product) { const matches = requireStagedSdkArtifact(product, "Cargo package", [".crate"]); const names = matches.map((file) => path.basename(file)); @@ -260,11 +369,11 @@ export async function runSdkProductDryRun(product, { allowDirty = false } = {}) return; } if (product === "oliphaunt-react-native") { - requireStagedSdkArtifact(product, "npm package", [".tgz"]); + stagedSdkNpmPackageTarball(product); return; } if (product === "oliphaunt-js") { - requireStagedSdkArtifact(product, "npm package", [".tgz"]); + stagedSdkNpmPackageTarball(product); const command = ["pnpm", "exec", "jsr", "publish", "--dry-run"]; if (allowDirty) { command.push("--allow-dirty"); From 0590365a1ce87c1b0c6a5e0d5bab9127e9f7164b Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 09:06:36 +0000 Subject: [PATCH 291/308] fix: publish broker cargo artifacts in bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 16 +++ tools/policy/check-tooling-stack.sh | 8 ++ tools/release/check_release_metadata.py | 6 +- tools/release/release-publish.mjs | 121 ++++++++++++++++++ 4 files changed, 150 insertions(+), 1 deletion(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 8e3c686f..12c58d3a 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -3021,3 +3021,19 @@ until the current-state gates here are checked with fresh local evidence. `tools/dev/bun.sh tools/release/check-release-metadata.mjs`, `tools/dev/bun.sh tools/release/check_artifact_targets.mjs`, and `tools/dev/bun.sh tools/release/check-consumer-shape.mjs`. +- On 2026-06-28, the protected Broker Cargo artifact publish step moved onto + the Bun `release-publish.mjs` surface. The route preserves the Python + behavior: verify the Broker product tag, regenerate the four + `oliphaunt-broker-*` Cargo artifact crates from staged release assets, verify + the generated crate set against release graph targets and + `registry_packages`, skip crates already present on crates.io, publish each + generated manifest with `cargo publish --manifest-path`, wait for crates.io + visibility, and verify Broker crates publication through the shared registry + checker. Fresh local evidence passed for `node --check tools/release/release-publish.mjs`, + `PYTHONPYCACHEPREFIX=target/python-pycache python3 -m py_compile tools/release/check_release_metadata.py`, + `tools/dev/bun.sh tools/release/release-publish.mjs publish --product oliphaunt-broker --step crates-io --head-ref oliphaunt-not-a-ref` + failing at Bun tag verification, `tools/dev/bun.sh tools/release/release-product-dry-run.mjs --product oliphaunt-broker --allow-dirty`, + `bash tools/policy/check-tooling-stack.sh`, + `tools/dev/bun.sh tools/release/check-release-metadata.mjs`, + `tools/dev/bun.sh tools/release/check_artifact_targets.mjs`, and + `tools/dev/bun.sh tools/release/check-consumer-shape.mjs`. diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 81c1a53f..ef4439ec 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -478,6 +478,14 @@ grep -Fq 'brokerNpmTarballs(version)' tools/release/release-publish.mjs || fail "release-publish must validate staged broker npm tarballs before publish" grep -Fq 'requireProductRegistryPublished(product, "npm")' tools/release/release-publish.mjs || fail "release-publish must verify broker npm publication through the registry checker" +grep -Fq 'publishBrokerCargoArtifacts' tools/release/release-publish.mjs || + fail "release-publish must own broker Cargo artifact publication in Bun" +grep -Fq 'brokerCargoArtifactCrates(version)' tools/release/release-publish.mjs || + fail "release-publish must validate generated broker Cargo artifact crates before publish" +grep -Fq 'await cargoPublishManifest(crateName, version, manifestPath)' tools/release/release-publish.mjs || + fail "release-publish must publish generated broker Cargo artifact manifests through the Bun wrapper" +grep -Fq 'requireProductRegistryPublished(product, "crates")' tools/release/release-publish.mjs || + fail "release-publish must verify broker Cargo artifact publication through the registry checker" grep -Fq 'publishLiboliphauntNpmPackages' tools/release/release-publish.mjs || fail "release-publish must own liboliphaunt-native npm package publication in Bun" grep -Fq 'liboliphauntNpmTarballs(version)' tools/release/release-publish.mjs || diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index de9d1b36..8c46db24 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1029,6 +1029,10 @@ def validate_publish_target_coverage() -> None: or "publishBrokerNpmPackages" not in release_publish or "brokerNpmTarballs(version)" not in release_publish or 'requireProductRegistryPublished(product, "npm")' not in release_publish + or "publishBrokerCargoArtifacts" not in release_publish + or "brokerCargoArtifactCrates(version)" not in release_publish + or "await cargoPublishManifest(crateName, version, manifestPath)" not in release_publish + or 'requireProductRegistryPublished(product, "crates")' not in release_publish or "publishLiboliphauntNpmPackages" not in release_publish or "liboliphauntNpmTarballs(version)" not in release_publish or "publishReactNativeNpm" not in release_publish @@ -1074,7 +1078,7 @@ def validate_publish_target_coverage() -> None: or "prepareOliphauntWasixReleaseSource" not in release_sdk_product_dry_run or 'spawnSync("tools/release/release.py", argv' not in release_publish ): - fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper and exact-extension GitHub asset publish steps must run in Bun, liboliphaunt-native and exact-extension Maven publication must run in Bun, liboliphaunt-native, broker, Node direct, and React Native npm publication must run in Bun, and React Native SDK tasks must not track release.py directly") + fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper and exact-extension GitHub asset publish steps must run in Bun, liboliphaunt-native and exact-extension Maven publication must run in Bun, liboliphaunt-native, broker, Node direct, and React Native npm publication must run in Bun, broker Cargo artifact publication must run in Bun, and React Native SDK tasks must not track release.py directly") 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 diff --git a/tools/release/release-publish.mjs b/tools/release/release-publish.mjs index 2a83299e..ed341384 100755 --- a/tools/release/release-publish.mjs +++ b/tools/release/release-publish.mjs @@ -20,9 +20,11 @@ import { } from "./release-product-dry-run.mjs"; import { stagedSdkNpmPackageTarball } from "./release-sdk-product-dry-run.mjs"; import { + artifactTargets, compareText, currentProductVersionSync, exactExtensionProducts, + registryPackageRows, } from "./release-artifact-targets.mjs"; const TOOL = "release-publish.mjs"; @@ -156,6 +158,18 @@ function globReleaseAssets(assetDir, suffixes) { return assets; } +function sortedStrings(values) { + return [...values].sort(compareText); +} + +function assertSameStringSet(label, actual, expected) { + const actualSorted = sortedStrings(actual); + const expectedSorted = sortedStrings(expected); + if (JSON.stringify(actualSorted) !== JSON.stringify(expectedSorted)) { + fail(`${label}: expected=${JSON.stringify(expectedSorted)}, actual=${JSON.stringify(actualSorted)}`); + } +} + function noProductPublishDryRunPassthrough(args) { if (args.includes("--wasm") || selectsProducts(args)) { return null; @@ -336,6 +350,47 @@ function npmPackagePublished(packageName, version) { return result.status === 0; } +function cratesioCrateVersionPublished(crateName, version) { + const result = jsonOutput([ + "tools/release/check_registry_publication.mjs", + "crate-version-exists", + "--crate", + crateName, + "--version", + version, + ]); + if (result === null || typeof result.exists !== "boolean") { + fail(`crate-version-exists returned invalid JSON for ${crateName} ${version}`); + } + return result.exists; +} + +async function waitForCratesioCrate(crateName, version) { + for (let attempt = 0; attempt < 12; attempt += 1) { + if (cratesioCrateVersionPublished(crateName, version)) { + return; + } + await Bun.sleep(10_000); + } + fail(`${crateName} ${version} did not appear on crates.io after publish`); +} + +async function cargoPublishManifest(crateName, version, manifestPath) { + if (cratesioCrateVersionPublished(crateName, version)) { + console.log(`${crateName} ${version} is already published on crates.io; skipping cargo publish.`); + return; + } + run(TOOL, [ + "cargo", + "publish", + "--manifest-path", + manifestPath, + "--target-dir", + path.join(ROOT, "target/release/cargo-publish"), + ]); + await waitForCratesioCrate(crateName, version); +} + function npmPublishTarball(packageName, tarball, version) { if (npmPackagePublished(packageName, version)) { console.log(`${packageName} ${version} is already published on npm; skipping npm publish.`); @@ -344,6 +399,57 @@ function npmPublishTarball(packageName, tarball, version) { run(TOOL, ["npm", "publish", tarball, "--access", "public", "--provenance"]); } +function brokerCargoArtifactCrates(version) { + const product = "oliphaunt-broker"; + ensureBrokerReleaseAssets(); + const outputDir = path.join(ROOT, "target/oliphaunt-broker/cargo-artifacts"); + run(TOOL, [ + "tools/dev/bun.sh", + "tools/release/package_broker_cargo_artifacts.mjs", + "--version", + version, + "--output-dir", + rel(outputDir), + ]); + + const expectedCrates = new Set( + artifactTargets(product, "broker-helper", TOOL) + .filter((target) => target.surfaces.includes("rust-broker")) + .map((target) => `${product}-${target.target}`), + ); + const configuredCrates = new Set( + registryPackageRows({ product, packageKind: "crates" }, TOOL) + .map((row) => row.packageName), + ); + assertSameStringSet(`${product} crates.io packages must match broker artifact targets`, configuredCrates, expectedCrates); + + const sourceRoot = path.join(ROOT, "target/oliphaunt-broker/cargo-package-sources"); + const expectedPaths = new Set(); + const packages = []; + for (const crateName of sortedStrings(expectedCrates)) { + const cratePath = path.join(outputDir, `${crateName}-${version}.crate`); + const manifestPath = path.join(sourceRoot, crateName, "Cargo.toml"); + expectedPaths.add(path.resolve(cratePath)); + if (!isFile(cratePath)) { + fail(`missing generated broker Cargo artifact crate: ${rel(cratePath)}`); + } + if (!isFile(manifestPath)) { + fail(`missing generated broker Cargo artifact manifest: ${rel(manifestPath)}`); + } + packages.push([crateName, cratePath, manifestPath]); + } + const unexpected = readdirSync(outputDir) + .filter((name) => name.endsWith(".crate")) + .map((name) => path.join(outputDir, name)) + .filter((file) => !expectedPaths.has(path.resolve(file))) + .map((file) => path.basename(file)) + .sort(compareText); + if (unexpected.length > 0) { + fail(`unexpected broker Cargo artifact crate(s): ${unexpected.join(", ")}`); + } + return packages; +} + async function publishNodeDirectNpmOptionalPackages(headRef) { const product = "oliphaunt-node-direct"; verifyReleaseTag(product, headRef); @@ -367,6 +473,16 @@ function publishBrokerNpmPackages(headRef) { requireProductRegistryPublished(product, "npm"); } +async function publishBrokerCargoArtifacts(headRef) { + const product = "oliphaunt-broker"; + verifyReleaseTag(product, headRef); + const version = currentProductVersionSync(product, TOOL); + for (const [crateName, , manifestPath] of brokerCargoArtifactCrates(version)) { + await cargoPublishManifest(crateName, version, manifestPath); + } + requireProductRegistryPublished(product, "crates"); +} + function publishLiboliphauntNpmPackages(headRef) { const product = "liboliphaunt-native"; verifyReleaseTag(product, headRef); @@ -552,6 +668,11 @@ if (publishProductStep?.product === "oliphaunt-broker" && publishProductStep.ste process.exit(0); } +if (publishProductStep?.product === "oliphaunt-broker" && publishProductStep.step === "crates-io") { + await publishBrokerCargoArtifacts(publishProductStep.headRef); + process.exit(0); +} + if (publishProductStep?.product === "oliphaunt-react-native" && publishProductStep.step === "npm") { publishReactNativeNpm(publishProductStep.headRef); process.exit(0); From 43727c9e223733efb5427986d243c194688a12d8 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 09:16:58 +0000 Subject: [PATCH 292/308] fix: publish wasix cargo artifacts in bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 25 ++++++++++++++ tools/policy/check-tooling-stack.sh | 10 ++++++ tools/release/check_release_metadata.py | 7 +++- tools/release/release-product-dry-run.mjs | 34 +++++++++++++++++-- tools/release/release-publish.mjs | 16 +++++++++ 5 files changed, 88 insertions(+), 4 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 12c58d3a..040c0ce6 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -3037,3 +3037,28 @@ until the current-state gates here are checked with fresh local evidence. `tools/dev/bun.sh tools/release/check-release-metadata.mjs`, `tools/dev/bun.sh tools/release/check_artifact_targets.mjs`, and `tools/dev/bun.sh tools/release/check-consumer-shape.mjs`. +- On 2026-06-28, the protected `liboliphaunt-wasix` Cargo artifact publish + step moved onto the Bun `release-publish.mjs` surface. The route verifies the + product tag, regenerates the validated WASIX runtime/tools/AOT/ICU Cargo + artifact crates through the shared product dry-run helper, compares generated + crates with `registry_packages`, skips crates already present on crates.io, + publishes each manifest with `cargo publish --manifest-path`, waits for + crates.io visibility, and verifies product crates publication through the + shared registry checker. The split tool shape was rechecked at the same time: + native root crates keep `postgres`, `initdb`, and `pg_ctl` while + `oliphaunt-tools` carries `pg_dump` and `psql`; WASIX root crates keep + `postgres` and `initdb` while `oliphaunt-wasix-tools` carries + `pg_dump.wasix.wasm` and `psql.wasix.wasm` with no WASIX `pg_ctl`. Fresh local + evidence passed for `node --check tools/release/release-product-dry-run.mjs`, + `node --check tools/release/release-publish.mjs`, + `PYTHONPYCACHEPREFIX=target/python-pycache python3 -m py_compile tools/release/check_release_metadata.py`, + `tools/dev/bun.sh tools/release/release-publish.mjs publish --product liboliphaunt-wasix --step crates-io --head-ref oliphaunt-not-a-ref` + failing at Bun tag verification, + `tools/dev/bun.sh tools/release/release-product-dry-run.mjs --product liboliphaunt-wasix --allow-dirty`, + `bash tools/policy/check-tooling-stack.sh`, + `tools/dev/bun.sh tools/release/check-release-metadata.mjs`, + `tools/dev/bun.sh tools/release/check_artifact_targets.mjs`, + `tools/dev/bun.sh tools/release/check-consumer-shape.mjs`, and + `cargo check -p oliphaunt-tools -p oliphaunt-wasix-tools --locked`, + `cargo check -p oliphaunt-wasix --features tools --locked`, and + `cargo check -p oliphaunt --locked`. diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index ef4439ec..d7d21073 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -490,6 +490,12 @@ grep -Fq 'publishLiboliphauntNpmPackages' tools/release/release-publish.mjs || fail "release-publish must own liboliphaunt-native npm package publication in Bun" grep -Fq 'liboliphauntNpmTarballs(version)' tools/release/release-publish.mjs || fail "release-publish must validate staged liboliphaunt-native npm tarballs before publish" +grep -Fq 'publishLiboliphauntWasixCargoArtifacts' tools/release/release-publish.mjs || + fail "release-publish must own liboliphaunt-wasix Cargo artifact publication in Bun" +grep -Fq 'liboliphauntWasixCargoArtifactPackages(version)' tools/release/release-publish.mjs || + fail "release-publish must validate generated WASIX Cargo artifact crates before publish" +grep -Fq 'for (const { name, manifestPath } of liboliphauntWasixCargoArtifactPackages(version))' tools/release/release-publish.mjs || + fail "release-publish must publish each generated WASIX Cargo artifact manifest through the Bun wrapper" grep -Fq 'publishReactNativeNpm' tools/release/release-publish.mjs || fail "release-publish must own React Native npm package publication in Bun" grep -Fq 'stagedSdkNpmPackageTarball(product)' tools/release/release-publish.mjs || @@ -554,6 +560,10 @@ grep -Fq 'tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs' tools/re fail "Bun WASIX runtime product dry-run must generate WASIX Cargo artifact crates" grep -Fq 'validateWasixCargoArtifacts' tools/release/release-product-dry-run.mjs || fail "Bun WASIX runtime product dry-run must validate generated Cargo artifact manifest rows" +grep -Fq 'registryPackageRows({ product: WASIX_PRODUCT, packageKind: "crates" }' tools/release/release-product-dry-run.mjs || + fail "Bun WASIX runtime Cargo artifact validation must compare generated crates with registry package metadata" +grep -Fq 'export function liboliphauntWasixCargoArtifactPackages' tools/release/release-product-dry-run.mjs || + fail "Bun WASIX runtime product dry-run must expose the shared validated Cargo artifact package list" grep -Fq 'NODE_DIRECT_PRODUCT,' tools/release/release-product-dry-run.mjs || fail "release product dry-run bridge must include Node direct in Bun-owned product dry-runs" grep -Fq 'ensureNodeDirectReleaseAssets' tools/release/release-product-dry-run.mjs || diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 8c46db24..64281388 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1035,6 +1035,9 @@ def validate_publish_target_coverage() -> None: or 'requireProductRegistryPublished(product, "crates")' not in release_publish or "publishLiboliphauntNpmPackages" not in release_publish or "liboliphauntNpmTarballs(version)" not in release_publish + or "publishLiboliphauntWasixCargoArtifacts" not in release_publish + or "liboliphauntWasixCargoArtifactPackages(version)" not in release_publish + or "for (const { name, manifestPath } of liboliphauntWasixCargoArtifactPackages(version))" not in release_publish or "publishReactNativeNpm" not in release_publish or "stagedSdkNpmPackageTarball(product)" not in release_publish or "uploadGithubReleaseAssets(product, [])" not in release_publish @@ -1060,6 +1063,8 @@ def validate_publish_target_coverage() -> None: or "tools/release/check-liboliphaunt-wasix-release-assets.mjs" not in release_product_dry_run or "tools/release/package_liboliphaunt_wasix_cargo_artifacts.mjs" not in release_product_dry_run or "validateWasixCargoArtifacts" not in release_product_dry_run + or 'registryPackageRows({ product: WASIX_PRODUCT, packageKind: "crates" }' not in release_product_dry_run + or "export function liboliphauntWasixCargoArtifactPackages" not in release_product_dry_run or "NODE_DIRECT_PRODUCT," not in release_product_dry_run or "ensureNodeDirectReleaseAssets" not in release_product_dry_run or "nodeDirectOptionalNpmTarballs" not in release_product_dry_run @@ -1078,7 +1083,7 @@ def validate_publish_target_coverage() -> None: or "prepareOliphauntWasixReleaseSource" not in release_sdk_product_dry_run or 'spawnSync("tools/release/release.py", argv' not in release_publish ): - fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper and exact-extension GitHub asset publish steps must run in Bun, liboliphaunt-native and exact-extension Maven publication must run in Bun, liboliphaunt-native, broker, Node direct, and React Native npm publication must run in Bun, broker Cargo artifact publication must run in Bun, and React Native SDK tasks must not track release.py directly") + fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper and exact-extension GitHub asset publish steps must run in Bun, liboliphaunt-native and exact-extension Maven publication must run in Bun, liboliphaunt-native, broker, Node direct, and React Native npm publication must run in Bun, Broker and WASIX Cargo artifact publication must run in Bun, and React Native SDK tasks must not track release.py directly") 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 diff --git a/tools/release/release-product-dry-run.mjs b/tools/release/release-product-dry-run.mjs index 64116eea..f1b8decf 100644 --- a/tools/release/release-product-dry-run.mjs +++ b/tools/release/release-product-dry-run.mjs @@ -27,6 +27,7 @@ import { compareText, currentProductVersionSync, exactExtensionProducts, + registryPackageRows, } from "./release-artifact-targets.mjs"; import { WASIX_CARGO_ARTIFACT_SCHEMA, @@ -73,6 +74,18 @@ function rel(file) { return path.relative(ROOT, file).split(path.sep).join("/"); } +function sortedStrings(values) { + return [...values].sort(compareText); +} + +function assertSameStringSet(label, actual, expected) { + const actualSorted = sortedStrings(actual); + const expectedSorted = sortedStrings(expected); + if (JSON.stringify(actualSorted) !== JSON.stringify(expectedSorted)) { + fail(`${label}: expected=${JSON.stringify(expectedSorted)}, actual=${JSON.stringify(actualSorted)}`); + } +} + function isFile(file) { try { return statSync(file).isFile(); @@ -1164,8 +1177,18 @@ function validateWasixCargoArtifacts(outputDir) { } const expectedBaseCrates = new Set(wasixPublicCargoPackageNames()); + const configuredCrates = new Set( + registryPackageRows({ product: WASIX_PRODUCT, packageKind: "crates" }, TOOL) + .map((row) => row.packageName), + ); + assertSameStringSet( + `${WASIX_PRODUCT} crates.io packages must match WASIX runtime/AOT artifact packages`, + configuredCrates, + expectedBaseCrates, + ); const generatedCrates = new Set(); const expectedCratePaths = new Set(); + const packages = []; const allowedKinds = new Set([ "wasix-runtime", "wasix-tools", @@ -1205,6 +1228,7 @@ function validateWasixCargoArtifacts(outputDir) { } generatedCrates.add(name); expectedCratePaths.add(path.resolve(cratePath)); + packages.push({ name, cratePath, manifestPath: sourceManifest }); } const missingBaseCrates = [...expectedBaseCrates] @@ -1222,10 +1246,10 @@ function validateWasixCargoArtifacts(outputDir) { if (unexpected.length > 0) { fail(`unexpected ${WASIX_PRODUCT} Cargo artifact crate(s): ${unexpected.join(", ")}`); } + return packages.sort((left, right) => compareText(left.name, right.name)); } -function runWasixRuntimeDryRun() { - const version = currentProductVersionSync(WASIX_PRODUCT, TOOL); +export function liboliphauntWasixCargoArtifactPackages(version = currentProductVersionSync(WASIX_PRODUCT, TOOL)) { const outputDir = path.join(ROOT, "target/oliphaunt-wasix/cargo-artifacts"); ensureWasixReleaseAssets(); run(TOOL, [ @@ -1236,7 +1260,11 @@ function runWasixRuntimeDryRun() { "--output-dir", rel(outputDir), ]); - validateWasixCargoArtifacts(outputDir); + return validateWasixCargoArtifacts(outputDir); +} + +function runWasixRuntimeDryRun() { + liboliphauntWasixCargoArtifactPackages(currentProductVersionSync(WASIX_PRODUCT, TOOL)); } function extensionPackageDir(product) { diff --git a/tools/release/release-publish.mjs b/tools/release/release-publish.mjs index ed341384..931283b2 100755 --- a/tools/release/release-publish.mjs +++ b/tools/release/release-publish.mjs @@ -14,6 +14,7 @@ import { ensureWasixReleaseAssets, extensionAssetPaths, liboliphauntNpmTarballs, + liboliphauntWasixCargoArtifactPackages, nodeDirectOptionalNpmTarballs, runBunProductDryRun, runMavenArtifactPublisher, @@ -483,6 +484,16 @@ async function publishBrokerCargoArtifacts(headRef) { requireProductRegistryPublished(product, "crates"); } +async function publishLiboliphauntWasixCargoArtifacts(headRef) { + const product = "liboliphaunt-wasix"; + verifyReleaseTag(product, headRef); + const version = currentProductVersionSync(product, TOOL); + for (const { name, manifestPath } of liboliphauntWasixCargoArtifactPackages(version)) { + await cargoPublishManifest(name, version, manifestPath); + } + requireProductRegistryPublished(product, "crates"); +} + function publishLiboliphauntNpmPackages(headRef) { const product = "liboliphaunt-native"; verifyReleaseTag(product, headRef); @@ -658,6 +669,11 @@ if (publishProductStep?.product === "liboliphaunt-native" && publishProductStep. process.exit(0); } +if (publishProductStep?.product === "liboliphaunt-wasix" && publishProductStep.step === "crates-io") { + await publishLiboliphauntWasixCargoArtifacts(publishProductStep.headRef); + process.exit(0); +} + if (publishProductStep?.product === "oliphaunt-node-direct" && publishProductStep.step === "npm") { await publishNodeDirectNpmOptionalPackages(publishProductStep.headRef); process.exit(0); From 08b529b1b1b852959375c07eac341774463775c3 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 09:22:32 +0000 Subject: [PATCH 293/308] fix: publish native cargo artifacts in bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 21 ++++++++++++ tools/policy/check-tooling-stack.sh | 10 ++++++ tools/release/check_release_metadata.py | 7 +++- tools/release/release-product-dry-run.mjs | 33 +++++++++++++++++-- tools/release/release-publish.mjs | 16 +++++++++ 5 files changed, 83 insertions(+), 4 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 040c0ce6..3ecec680 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -3062,3 +3062,24 @@ until the current-state gates here are checked with fresh local evidence. `cargo check -p oliphaunt-tools -p oliphaunt-wasix-tools --locked`, `cargo check -p oliphaunt-wasix --features tools --locked`, and `cargo check -p oliphaunt --locked`. +- On 2026-06-28, the protected `liboliphaunt-native` Cargo artifact publish + step moved onto the Bun `release-publish.mjs` surface. The shared native + product dry-run helper now returns the validated Cargo package list, compares + generated runtime/tool aggregators plus the `oliphaunt-tools` facade with + `registry_packages`, and preserves the publish order required by generated + part crates: parts first, aggregators second, facade last. The publish route + verifies the product tag, regenerates native runtime/tool Cargo artifact + crates from staged release assets, skips crates already present on crates.io, + publishes each manifest with `cargo publish --manifest-path`, waits for + crates.io visibility, and verifies configured product crates through the + shared registry checker. Fresh local evidence passed for + `node --check tools/release/release-product-dry-run.mjs`, + `node --check tools/release/release-publish.mjs`, + `PYTHONPYCACHEPREFIX=target/python-pycache python3 -m py_compile tools/release/check_release_metadata.py`, + `tools/dev/bun.sh tools/release/release-publish.mjs publish --product liboliphaunt-native --step crates-io --head-ref oliphaunt-not-a-ref` + failing at Bun tag verification, + `tools/dev/bun.sh tools/release/release-product-dry-run.mjs --product liboliphaunt-native --allow-dirty`, + `bash tools/policy/check-tooling-stack.sh`, + `tools/dev/bun.sh tools/release/check-release-metadata.mjs`, + `tools/dev/bun.sh tools/release/check_artifact_targets.mjs`, and + `tools/dev/bun.sh tools/release/check-consumer-shape.mjs`. diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index d7d21073..847b5257 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -490,6 +490,12 @@ grep -Fq 'publishLiboliphauntNpmPackages' tools/release/release-publish.mjs || fail "release-publish must own liboliphaunt-native npm package publication in Bun" grep -Fq 'liboliphauntNpmTarballs(version)' tools/release/release-publish.mjs || fail "release-publish must validate staged liboliphaunt-native npm tarballs before publish" +grep -Fq 'publishLiboliphauntNativeCargoArtifacts' tools/release/release-publish.mjs || + fail "release-publish must own liboliphaunt-native Cargo artifact publication in Bun" +grep -Fq 'liboliphauntNativeCargoArtifactPackages(version)' tools/release/release-publish.mjs || + fail "release-publish must validate generated native Cargo artifact crates before publish" +grep -Fq 'for (const { name, manifestPath } of liboliphauntNativeCargoArtifactPackages(version))' tools/release/release-publish.mjs || + fail "release-publish must publish each generated native Cargo artifact manifest through the Bun wrapper" grep -Fq 'publishLiboliphauntWasixCargoArtifacts' tools/release/release-publish.mjs || fail "release-publish must own liboliphaunt-wasix Cargo artifact publication in Bun" grep -Fq 'liboliphauntWasixCargoArtifactPackages(version)' tools/release/release-publish.mjs || @@ -538,6 +544,10 @@ grep -Fq 'tools/release/package-liboliphaunt-cargo-artifacts.mjs' tools/release/ fail "Bun liboliphaunt-native product dry-run must generate native Cargo artifact crates" grep -Fq 'validateNativeCargoArtifacts' tools/release/release-product-dry-run.mjs || fail "Bun liboliphaunt-native product dry-run must validate generated native Cargo artifact manifest rows" +grep -Fq 'registryPackageRows({ product: LIBOLIPHAUNT_NATIVE_PRODUCT, packageKind: "crates" }' tools/release/release-product-dry-run.mjs || + fail "Bun liboliphaunt-native Cargo artifact validation must compare generated crates with registry package metadata" +grep -Fq 'export function liboliphauntNativeCargoArtifactPackages' tools/release/release-product-dry-run.mjs || + fail "Bun liboliphaunt-native product dry-run must expose the shared validated Cargo artifact package list" grep -Fq 'liboliphauntNpmTarballs' tools/release/release-product-dry-run.mjs || fail "Bun liboliphaunt-native product dry-run must validate native runtime/tools/ICU npm tarballs" grep -Fq 'liboliphaunt-native-maven-dry-run' tools/release/release-product-dry-run.mjs || diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 64281388..3c3ebad4 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1035,6 +1035,9 @@ def validate_publish_target_coverage() -> None: or 'requireProductRegistryPublished(product, "crates")' not in release_publish or "publishLiboliphauntNpmPackages" not in release_publish or "liboliphauntNpmTarballs(version)" not in release_publish + or "publishLiboliphauntNativeCargoArtifacts" not in release_publish + or "liboliphauntNativeCargoArtifactPackages(version)" not in release_publish + or "for (const { name, manifestPath } of liboliphauntNativeCargoArtifactPackages(version))" not in release_publish or "publishLiboliphauntWasixCargoArtifacts" not in release_publish or "liboliphauntWasixCargoArtifactPackages(version)" not in release_publish or "for (const { name, manifestPath } of liboliphauntWasixCargoArtifactPackages(version))" not in release_publish @@ -1052,6 +1055,8 @@ def validate_publish_target_coverage() -> None: or "tools/release/check-liboliphaunt-release-assets.mjs" not in release_product_dry_run or "tools/release/package-liboliphaunt-cargo-artifacts.mjs" not in release_product_dry_run or "validateNativeCargoArtifacts" not in release_product_dry_run + or 'registryPackageRows({ product: LIBOLIPHAUNT_NATIVE_PRODUCT, packageKind: "crates" }' not in release_product_dry_run + or "export function liboliphauntNativeCargoArtifactPackages" not in release_product_dry_run or "liboliphauntNpmTarballs" not in release_product_dry_run or "liboliphaunt-native-maven-dry-run" not in release_product_dry_run or "BROKER_PRODUCT," not in release_product_dry_run @@ -1083,7 +1088,7 @@ def validate_publish_target_coverage() -> None: or "prepareOliphauntWasixReleaseSource" not in release_sdk_product_dry_run or 'spawnSync("tools/release/release.py", argv' not in release_publish ): - fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper and exact-extension GitHub asset publish steps must run in Bun, liboliphaunt-native and exact-extension Maven publication must run in Bun, liboliphaunt-native, broker, Node direct, and React Native npm publication must run in Bun, Broker and WASIX Cargo artifact publication must run in Bun, and React Native SDK tasks must not track release.py directly") + fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper and exact-extension GitHub asset publish steps must run in Bun, liboliphaunt-native and exact-extension Maven publication must run in Bun, liboliphaunt-native, broker, Node direct, and React Native npm publication must run in Bun, native, Broker, and WASIX Cargo artifact publication must run in Bun, and React Native SDK tasks must not track release.py directly") 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 diff --git a/tools/release/release-product-dry-run.mjs b/tools/release/release-product-dry-run.mjs index f1b8decf..642d3280 100644 --- a/tools/release/release-product-dry-run.mjs +++ b/tools/release/release-product-dry-run.mjs @@ -1055,9 +1055,20 @@ function validateNativeCargoArtifacts(outputDir) { ...nativeCargoArtifactTargets(LIBOLIPHAUNT_NATIVE_TOOLS_KIND) .map((target) => `${LIBOLIPHAUNT_NATIVE_TOOLS_PRODUCT}-${target.target}`), ]); + const expectedRegistryCrates = new Set([...expectedAggregators, LIBOLIPHAUNT_NATIVE_TOOLS_PRODUCT]); + const configuredCrates = new Set( + registryPackageRows({ product: LIBOLIPHAUNT_NATIVE_PRODUCT, packageKind: "crates" }, TOOL) + .map((row) => row.packageName), + ); + assertSameStringSet( + `${LIBOLIPHAUNT_NATIVE_PRODUCT} crates.io packages must match native runtime/tool artifact packages`, + configuredCrates, + expectedRegistryCrates, + ); const aggregators = new Set(); const facades = new Set(); const expectedCratePaths = new Set(); + const packages = []; for (const item of data.packages) { if (item === null || Array.isArray(item) || typeof item !== "object") { @@ -1084,6 +1095,7 @@ function validateNativeCargoArtifacts(outputDir) { fail(`missing generated ${LIBOLIPHAUNT_NATIVE_PRODUCT} Cargo part crate for ${name}: ${rawCrate}`); } expectedCratePaths.add(path.resolve(cratePath)); + packages.push({ name, cratePath, manifestPath: sourceManifest, role }); continue; } if (role === "aggregator") { @@ -1094,6 +1106,7 @@ function validateNativeCargoArtifacts(outputDir) { fail(`generated ${LIBOLIPHAUNT_NATIVE_PRODUCT} aggregator crate ${name} must be source-only`); } aggregators.add(name); + packages.push({ name, cratePath: null, manifestPath: sourceManifest, role }); continue; } if (role === "facade") { @@ -1104,6 +1117,7 @@ function validateNativeCargoArtifacts(outputDir) { fail(`generated ${LIBOLIPHAUNT_NATIVE_PRODUCT} facade crate ${name} must be source-only`); } facades.add(name); + packages.push({ name, cratePath: null, manifestPath: sourceManifest, role }); continue; } fail(`${rel(manifestPath)} has unsupported Cargo artifact role ${JSON.stringify(role)}`); @@ -1127,10 +1141,18 @@ function validateNativeCargoArtifacts(outputDir) { if (unexpected.length > 0) { fail(`unexpected ${LIBOLIPHAUNT_NATIVE_PRODUCT} Cargo artifact crate(s): ${unexpected.join(", ")}`); } + const roleOrder = new Map([ + ["part", 0], + ["aggregator", 1], + ["facade", 2], + ]); + return packages.sort((left, right) => + (roleOrder.get(left.role) ?? 99) - (roleOrder.get(right.role) ?? 99) || + compareText(left.name, right.name), + ); } -function runLiboliphauntNativeDryRun() { - const version = currentProductVersionSync(LIBOLIPHAUNT_NATIVE_PRODUCT, TOOL); +export function liboliphauntNativeCargoArtifactPackages(version = currentProductVersionSync(LIBOLIPHAUNT_NATIVE_PRODUCT, TOOL)) { const outputDir = path.join(ROOT, "target/liboliphaunt/cargo-artifacts"); ensureLiboliphauntReleaseAssets(); run(TOOL, [ @@ -1141,7 +1163,12 @@ function runLiboliphauntNativeDryRun() { "--output-dir", rel(outputDir), ]); - validateNativeCargoArtifacts(outputDir); + return validateNativeCargoArtifacts(outputDir); +} + +function runLiboliphauntNativeDryRun() { + const version = currentProductVersionSync(LIBOLIPHAUNT_NATIVE_PRODUCT, TOOL); + liboliphauntNativeCargoArtifactPackages(version); liboliphauntNpmTarballs(version); const manifest = buildMavenArtifactManifest("liboliphaunt-native-runtime", { runtime: true }); runMavenArtifactPublisher( diff --git a/tools/release/release-publish.mjs b/tools/release/release-publish.mjs index 931283b2..bffd8e3b 100755 --- a/tools/release/release-publish.mjs +++ b/tools/release/release-publish.mjs @@ -13,6 +13,7 @@ import { ensureNodeDirectReleaseAssets, ensureWasixReleaseAssets, extensionAssetPaths, + liboliphauntNativeCargoArtifactPackages, liboliphauntNpmTarballs, liboliphauntWasixCargoArtifactPackages, nodeDirectOptionalNpmTarballs, @@ -494,6 +495,16 @@ async function publishLiboliphauntWasixCargoArtifacts(headRef) { requireProductRegistryPublished(product, "crates"); } +async function publishLiboliphauntNativeCargoArtifacts(headRef) { + const product = "liboliphaunt-native"; + verifyReleaseTag(product, headRef); + const version = currentProductVersionSync(product, TOOL); + for (const { name, manifestPath } of liboliphauntNativeCargoArtifactPackages(version)) { + await cargoPublishManifest(name, version, manifestPath); + } + requireProductRegistryPublished(product, "crates"); +} + function publishLiboliphauntNpmPackages(headRef) { const product = "liboliphaunt-native"; verifyReleaseTag(product, headRef); @@ -669,6 +680,11 @@ if (publishProductStep?.product === "liboliphaunt-native" && publishProductStep. process.exit(0); } +if (publishProductStep?.product === "liboliphaunt-native" && publishProductStep.step === "crates-io") { + await publishLiboliphauntNativeCargoArtifacts(publishProductStep.headRef); + process.exit(0); +} + if (publishProductStep?.product === "liboliphaunt-wasix" && publishProductStep.step === "crates-io") { await publishLiboliphauntWasixCargoArtifacts(publishProductStep.headRef); process.exit(0); From 630ca9e0645695f2757be2add38569143f901405 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 09:36:21 +0000 Subject: [PATCH 294/308] fix: publish rust sdk crates in bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 21 ++++ tools/policy/check-tooling-stack.sh | 12 +++ tools/release/check_release_metadata.py | 8 +- tools/release/release-publish.mjs | 99 ++++++++++++++++++- tools/release/release-sdk-product-dry-run.mjs | 2 +- 5 files changed, 139 insertions(+), 3 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 3ecec680..55c09cfb 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -3083,3 +3083,24 @@ until the current-state gates here are checked with fresh local evidence. `tools/dev/bun.sh tools/release/check-release-metadata.mjs`, `tools/dev/bun.sh tools/release/check_artifact_targets.mjs`, and `tools/dev/bun.sh tools/release/check-consumer-shape.mjs`. +- On 2026-06-28, the protected `oliphaunt-rust` crates.io publish step moved + onto the Bun `release-publish.mjs` surface. The route preserves the Python + dependency order and idempotency: if the release tag already points at the + head and all configured registries are published, it skips; otherwise it + verifies the product tag, validates staged SDK Cargo artifacts, requires the + matching `liboliphaunt-native` and `oliphaunt-broker` Cargo artifact packages + to be published, publishes `oliphaunt-build` first with `cargo publish -p + oliphaunt-build --locked`, generates the crates.io `oliphaunt` manifest + through `prepare-rust-release-source.mjs`, publishes that manifest with + `cargo publish --manifest-path`, waits for crates.io visibility, and verifies + product registry publication through the shared registry checker. Fresh local + evidence passed for `node --check tools/release/release-publish.mjs`, + `node --check tools/release/release-sdk-product-dry-run.mjs`, + `PYTHONPYCACHEPREFIX=target/python-pycache python3 -m py_compile tools/release/check_release_metadata.py`, + `tools/dev/bun.sh tools/release/release-publish.mjs publish --product oliphaunt-rust --step crates-io --head-ref oliphaunt-not-a-ref` + failing at Bun tag verification, + `tools/dev/bun.sh tools/release/release-sdk-product-dry-run.mjs --product oliphaunt-rust --allow-dirty`, + `bash tools/policy/check-tooling-stack.sh`, + `tools/dev/bun.sh tools/release/check-release-metadata.mjs`, + `tools/dev/bun.sh tools/release/check_artifact_targets.mjs`, and + `tools/dev/bun.sh tools/release/check-consumer-shape.mjs`. diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 847b5257..9c31b09c 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -508,6 +508,18 @@ grep -Fq 'stagedSdkNpmPackageTarball(product)' tools/release/release-publish.mjs fail "release-publish must validate the staged React Native npm tarball before publish" grep -Fq 'uploadGithubReleaseAssets(product, [])' tools/release/release-publish.mjs || fail "release-publish must preserve React Native no-asset GitHub release publication in Bun" +grep -Fq 'publishRustCratesIo' tools/release/release-publish.mjs || + fail "release-publish must own oliphaunt-rust crates.io publication in Bun" +grep -Fq 'verifyStagedCargoProductCrates(product)' tools/release/release-publish.mjs || + fail "release-publish must validate staged Rust SDK Cargo crates before publish" +grep -Fq 'requireProductRegistryVersionPublished("liboliphaunt-native", "crates", nativeVersion)' tools/release/release-publish.mjs || + fail "release-publish must require native Cargo artifact publication before oliphaunt-rust" +grep -Fq 'requireProductRegistryVersionPublished("oliphaunt-broker", "crates", brokerVersion)' tools/release/release-publish.mjs || + fail "release-publish must require broker Cargo artifact publication before oliphaunt-rust" +grep -Fq 'await cargoPublishWorkspacePackage("oliphaunt-build", version)' tools/release/release-publish.mjs || + fail "release-publish must publish oliphaunt-build before the oliphaunt crate" +grep -Fq 'await cargoPublishManifest("oliphaunt", version, prepareRustSdkReleaseManifest())' tools/release/release-publish.mjs || + fail "release-publish must publish the generated oliphaunt release manifest through Bun" grep -Fq 'exactExtensionProducts(TOOL)' tools/release/release-publish.mjs || fail "release-publish must derive exact-extension publish routing from the canonical extension product set" for github_asset_product in liboliphaunt-native liboliphaunt-wasix oliphaunt-broker oliphaunt-node-direct; do diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 3c3ebad4..1f62a45b 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1044,6 +1044,12 @@ def validate_publish_target_coverage() -> None: or "publishReactNativeNpm" not in release_publish or "stagedSdkNpmPackageTarball(product)" not in release_publish or "uploadGithubReleaseAssets(product, [])" not in release_publish + or "publishRustCratesIo" not in release_publish + or "verifyStagedCargoProductCrates(product)" not in release_publish + or 'requireProductRegistryVersionPublished("liboliphaunt-native", "crates", nativeVersion)' not in release_publish + or 'requireProductRegistryVersionPublished("oliphaunt-broker", "crates", brokerVersion)' not in release_publish + or 'await cargoPublishWorkspacePackage("oliphaunt-build", version)' not in release_publish + or 'await cargoPublishManifest("oliphaunt", version, prepareRustSdkReleaseManifest())' not in release_publish or "exactExtensionProducts(TOOL)" not in release_publish or '"liboliphaunt-native"' not in release_publish or '"liboliphaunt-wasix"' not in release_publish @@ -1088,7 +1094,7 @@ def validate_publish_target_coverage() -> None: or "prepareOliphauntWasixReleaseSource" not in release_sdk_product_dry_run or 'spawnSync("tools/release/release.py", argv' not in release_publish ): - fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper and exact-extension GitHub asset publish steps must run in Bun, liboliphaunt-native and exact-extension Maven publication must run in Bun, liboliphaunt-native, broker, Node direct, and React Native npm publication must run in Bun, native, Broker, and WASIX Cargo artifact publication must run in Bun, and React Native SDK tasks must not track release.py directly") + fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper and exact-extension GitHub asset publish steps must run in Bun, liboliphaunt-native and exact-extension Maven publication must run in Bun, liboliphaunt-native, broker, Node direct, and React Native npm publication must run in Bun, native, Broker, WASIX, and Rust SDK Cargo artifact publication must run in Bun, and React Native SDK tasks must not track release.py directly") 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 diff --git a/tools/release/release-publish.mjs b/tools/release/release-publish.mjs index bffd8e3b..fd8cea52 100755 --- a/tools/release/release-publish.mjs +++ b/tools/release/release-publish.mjs @@ -20,7 +20,10 @@ import { runBunProductDryRun, runMavenArtifactPublisher, } from "./release-product-dry-run.mjs"; -import { stagedSdkNpmPackageTarball } from "./release-sdk-product-dry-run.mjs"; +import { + stagedSdkNpmPackageTarball, + verifyStagedCargoProductCrates, +} from "./release-sdk-product-dry-run.mjs"; import { artifactTargets, compareText, @@ -290,6 +293,26 @@ function registryPublicationCheckSucceeds(args) { return result.status === 0; } +function gitCommit(ref) { + const result = spawnSync("git", ["rev-parse", `${ref}^{commit}`], { + cwd: ROOT, + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); + return result.status === 0 ? result.stdout.trim() : null; +} + +function productTagPointsAt(product, headRef) { + const version = currentProductVersionSync(product, TOOL); + const tagCommit = gitCommit(`${product}-v${version}`); + const headCommit = gitCommit(headRef); + return tagCommit !== null && headCommit !== null && tagCommit === headCommit; +} + +function publishedRerun(product, headRef) { + return productTagPointsAt(product, headRef) && productRegistryPublished(product, null); +} + function extensionMavenArtifactsPublished(products) { return registryPublicationCheckSucceeds([ "--products-json", @@ -340,6 +363,18 @@ function requireProductRegistryPublished(product, registryKind) { registryPublicationCheck(args); } +function requireProductRegistryVersionPublished(product, registryKind, version) { + registryPublicationCheck([ + "--product", + product, + "--registry-kind", + registryKind, + "--require-published", + "--version", + version, + ]); +} + function npmPackagePublished(packageName, version) { const result = spawnSync("npm", ["view", `${packageName}@${version}`, "version"], { cwd: ROOT, @@ -393,6 +428,44 @@ async function cargoPublishManifest(crateName, version, manifestPath) { await waitForCratesioCrate(crateName, version); } +async function cargoPublishWorkspacePackage(crateName, version) { + if (cratesioCrateVersionPublished(crateName, version)) { + console.log(`${crateName} ${version} is already published on crates.io; skipping cargo publish.`); + return; + } + run(TOOL, ["cargo", "publish", "-p", crateName, "--locked"]); + await waitForCratesioCrate(crateName, version); +} + +function commandOutput(args, { cwd = ROOT } = {}) { + const result = spawnSync(args[0], args.slice(1), { + cwd, + encoding: "utf8", + maxBuffer: 100 * 1024 * 1024, + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.error !== undefined) { + fail(`${args[0]} failed to start: ${result.error.message}`); + } + if (result.status !== 0) { + fail(`${args.join(" ")} failed${result.stderr ? `: ${result.stderr.trim()}` : ""}`); + } + return result.stdout; +} + +function prepareRustSdkReleaseManifest() { + const output = commandOutput(["tools/dev/bun.sh", "tools/release/prepare-rust-release-source.mjs"]); + const manifest = output.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).at(-1); + if (typeof manifest !== "string" || !manifest.endsWith("Cargo.toml")) { + fail(`prepare-rust-release-source.mjs did not print a generated Cargo.toml path: ${JSON.stringify(output)}`); + } + const manifestPath = path.isAbsolute(manifest) ? manifest : path.join(ROOT, manifest); + if (!isFile(manifestPath)) { + fail(`generated Rust SDK release manifest does not exist: ${rel(manifestPath)}`); + } + return manifestPath; +} + function npmPublishTarball(packageName, tarball, version) { if (npmPackagePublished(packageName, version)) { console.log(`${packageName} ${version} is already published on npm; skipping npm publish.`); @@ -530,6 +603,25 @@ function publishReactNativeNpm(headRef) { uploadGithubReleaseAssets(product, []); } +async function publishRustCratesIo(headRef) { + const product = "oliphaunt-rust"; + if (publishedRerun(product, headRef)) { + console.log("oliphaunt-rust is already published at this commit; skipping crates.io publish."); + return; + } + verifyReleaseTag(product, headRef); + const version = currentProductVersionSync(product, TOOL); + run(TOOL, ["tools/dev/bun.sh", "tools/release/check-staged-artifacts.mjs", "--require-sdk-product", product]); + verifyStagedCargoProductCrates(product); + const nativeVersion = currentProductVersionSync("liboliphaunt-native", TOOL); + const brokerVersion = currentProductVersionSync("oliphaunt-broker", TOOL); + requireProductRegistryVersionPublished("liboliphaunt-native", "crates", nativeVersion); + requireProductRegistryVersionPublished("oliphaunt-broker", "crates", brokerVersion); + await cargoPublishWorkspacePackage("oliphaunt-build", version); + await cargoPublishManifest("oliphaunt", version, prepareRustSdkReleaseManifest()); + requireProductRegistryPublished(product, null); +} + function publishLiboliphauntRuntimeMaven(headRef) { const product = "liboliphaunt-native"; verifyReleaseTag(product, headRef); @@ -710,6 +802,11 @@ if (publishProductStep?.product === "oliphaunt-react-native" && publishProductSt process.exit(0); } +if (publishProductStep?.product === "oliphaunt-rust" && publishProductStep.step === "crates-io") { + await publishRustCratesIo(publishProductStep.headRef); + process.exit(0); +} + if (publishProductStep?.step === "maven-central" && EXTENSION_PRODUCTS.has(publishProductStep.product)) { publishSelectedExtensionMaven([publishProductStep.product], publishProductStep.headRef); process.exit(0); diff --git a/tools/release/release-sdk-product-dry-run.mjs b/tools/release/release-sdk-product-dry-run.mjs index cf79292e..74a53e2e 100644 --- a/tools/release/release-sdk-product-dry-run.mjs +++ b/tools/release/release-sdk-product-dry-run.mjs @@ -312,7 +312,7 @@ function cratesioProductCrates(product) { return crates; } -function verifyStagedCargoProductCrates(product) { +export function verifyStagedCargoProductCrates(product) { const version = currentProductVersionSync(product, TOOL); const stagedNames = stagedCargoCrates(product).map((file) => path.basename(file)).sort(); const expectedNames = cratesioProductCrates(product) From d55d4c67b31a4e1a5aaeb9637a48e946c13f8cb5 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 09:46:26 +0000 Subject: [PATCH 295/308] refactor: remove rust sdk publish fallback from release py --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 16 ++ tools/policy/check-tooling-stack.sh | 12 + tools/release/check_consumer_shape.py | 4 +- tools/release/check_release_metadata.py | 13 +- tools/release/release.py | 225 ------------------ 5 files changed, 41 insertions(+), 229 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index 55c09cfb..aee0b5f6 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -3021,6 +3021,22 @@ until the current-state gates here are checked with fresh local evidence. `tools/dev/bun.sh tools/release/check-release-metadata.mjs`, `tools/dev/bun.sh tools/release/check_artifact_targets.mjs`, and `tools/dev/bun.sh tools/release/check-consumer-shape.mjs`. +- On 2026-06-28, after the Rust SDK crates.io route moved to Bun, the duplicate + `oliphaunt-rust` publish and publish-dry-run implementation was removed from + `tools/release/release.py`. The remaining protected Python fallback no longer + contains `oliphaunt-rust`, `prepare_oliphaunt_release_source`, + `run_rust_sdk_dry_run`, `publish_rust_crates_io`, + `render_oliphaunt_release_cargo_toml`, or + `validate_generated_oliphaunt_release_artifact_coverage`; the Rust SDK + generated publish-source check now points at + `tools/release/prepare-rust-release-source.mjs`, and release metadata coverage + checks the Bun `publishProductStep?.product === "oliphaunt-rust"` dispatcher + for `crates-io`. Fresh local evidence passed for + `PYTHONPYCACHEPREFIX=target/python-pycache python3 -m py_compile tools/release/release.py tools/release/check_consumer_shape.py tools/release/check_release_metadata.py`, + an `rg` absence scan over `tools/release/release.py`, + `bash tools/policy/check-tooling-stack.sh`, + `tools/dev/bun.sh tools/release/check-consumer-shape.mjs`, and + `tools/dev/bun.sh tools/release/check-release-metadata.mjs`. - On 2026-06-28, the protected Broker Cargo artifact publish step moved onto the Bun `release-publish.mjs` surface. The route preserves the Python behavior: verify the Broker product tag, regenerate the four diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 9c31b09c..389658ec 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -377,6 +377,18 @@ grep -Fq 'tools/dev/bun.sh tools/release/prepare-rust-release-source.mjs' src/sd if grep -Fq '"prepare-rust-release-source"' tools/release/release.py; then fail "release.py must not retain the Rust SDK prepare-rust-release-source command surface after it moved to Bun" fi +for retired_rust_sdk_release_py in \ + 'def render_oliphaunt_release_cargo_toml(' \ + 'def validate_generated_oliphaunt_release_artifact_coverage(' \ + 'def prepare_oliphaunt_release_source(' \ + 'def run_rust_sdk_dry_run(' \ + 'def publish_rust_crates_io(' \ + 'product == "oliphaunt-rust"' +do + if grep -Fq "$retired_rust_sdk_release_py" tools/release/release.py; then + fail "release.py must not retain Rust SDK dry-run or publish logic after it moved to Bun: $retired_rust_sdk_release_py" + fi +done for retired_release_command in \ 'def command_check(' \ 'def command_check_registries(' \ diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index acc8822d..de43708b 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -1284,9 +1284,9 @@ def check_rust(findings: list[Finding]) -> None: product, "publish-only-broker-dependencies", "oliphaunt-broker-linux-x64-gnu" not in sdk_manifest_text - and "prepare_oliphaunt_release_source" in read_text("tools/release/release.py"), + and "renderReleaseCargoToml(" in read_text("tools/release/prepare-rust-release-source.mjs"), "Rust SDK source manifest must stay local-check friendly; broker artifact dependencies are injected into the generated publish source.", - "src/sdks/rust/Cargo.toml and tools/release/release.py", + "src/sdks/rust/Cargo.toml and tools/release/prepare-rust-release-source.mjs", severity="P0", ) require_absent_text( diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 1f62a45b..24c103d0 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -766,6 +766,12 @@ def validate_graph_files() -> None: if ( "tools/dev/bun.sh tools/release/prepare-rust-release-source.mjs" not in rust_sdk_check or '"prepare-rust-release-source"' in release_source + or "def render_oliphaunt_release_cargo_toml(" in release_source + or "def validate_generated_oliphaunt_release_artifact_coverage(" in release_source + or "def prepare_oliphaunt_release_source(" in release_source + or "def run_rust_sdk_dry_run(" in release_source + or "def publish_rust_crates_io(" in release_source + or 'product == "oliphaunt-rust"' in release_source or "renderReleaseCargoToml(" not in prepare_rust_release_source or "currentProductVersionSync(RUST_PRODUCT" not in prepare_rust_release_source or "allArtifactTargets({ product, kind, surface, publishedOnly: true }" not in prepare_rust_release_source @@ -1103,7 +1109,7 @@ def validate_publish_target_coverage() -> None: supported = supported_publish_targets(product) if declared != supported: fail( - f"{product}.publish_targets must match release.py publish handler coverage: " + f"{product}.publish_targets must match publish handler coverage: " f"declared={sorted(declared)}, supported={sorted(supported)}" ) step_coverage = publish_step_target_coverage(product) @@ -1111,7 +1117,10 @@ def validate_publish_target_coverage() -> None: saw_extension = True continue for step in step_coverage: - if f'product == "{product}" and step == "{step}"' not in release_source: + if product == "oliphaunt-rust" and step == "crates-io": + if f'publishProductStep?.product === "{product}" && publishProductStep.step === "{step}"' not in release_publish: + fail(f"Bun publish implementation must dispatch publish step {product}:{step}") + elif 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}") diff --git a/tools/release/release.py b/tools/release/release.py index bc22566b..fe5abda9 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -1109,120 +1109,6 @@ def maven_pom_url(coordinate: str, version: str) -> str: ) -def rust_artifact_cargo_target_cfg(target: 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 render_oliphaunt_release_cargo_toml(source: str, native_version: str, broker_version: str) -> str: - text = source.replace( - "repository.workspace = true", - 'repository = "https://github.com/f0rr0/oliphaunt"', - ).replace( - "homepage.workspace = true", - 'homepage = "https://oliphaunt.dev"', - ) - if "[workspace]" not in text: - text = text.rstrip() + "\n\n[workspace]\n" - lines = [ - "", - "# Generated for crates.io publishing. Source checkouts keep native runtime", - "# and broker artifact crates out of the local dependency graph until those", - "# artifacts are published and indexed.", - ] - target_dependencies: dict[str, list[str]] = {} - for target in artifact_targets( - product="liboliphaunt-native", - kind="native-runtime", - surface="rust-native-direct", - published_only=True, - ): - crate = liboliphaunt_cargo_package_name(target.target) - tools_facade = LIBOLIPHAUNT_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_facade} = {{ version = "={native_version}" }}') - for target in artifact_targets( - product="oliphaunt-broker", - kind="broker-helper", - surface="rust-broker", - published_only=True, - ): - 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): - lines.extend( - [ - "", - f"[target.'cfg({cfg})'.dependencies]", - *sorted(target_dependencies[cfg]), - ] - ) - return text.rstrip() + "\n" + "\n".join(lines) + "\n" - - -def validate_generated_oliphaunt_release_artifact_coverage(manifest_path: Path) -> None: - manifest = manifest_path.read_text(encoding="utf-8") - broker_crates = cargo_registry_packages("oliphaunt-broker") - missing_broker = [crate for crate in broker_crates if f"{crate} = " not in manifest] - if missing_broker: - fail( - "generated oliphaunt release source is missing broker Cargo artifact dependencies: " - + ", ".join(missing_broker) - ) - - native_version = current_product_version("liboliphaunt-native") - native_targets = artifact_targets( - product="liboliphaunt-native", - kind="native-runtime", - surface="rust-native-direct", - published_only=True, - ) - native_runtime_crates = { - liboliphaunt_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( - "oliphaunt-rust cannot publish a working native Cargo consumer path: " - "oliphaunt-build requires Cargo-resolved liboliphaunt-native native-runtime " - f"artifacts for {target_names}, but liboliphaunt-native declares no crates.io " - "artifact packages. Split/size native runtime artifacts into crates.io-sized " - "packages before publishing oliphaunt-rust." - ) - tools_facade = LIBOLIPHAUNT_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: text = source.replace( "repository.workspace = true", @@ -1284,52 +1170,6 @@ def prepare_oliphaunt_wasix_release_source(version: str) -> Path: 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") - source_dir = ROOT / "src" / "sdks" / "rust" - stage_dir = ROOT / "target" / "release" / "cargo-package-sources" / "oliphaunt" - shutil.rmtree(stage_dir, ignore_errors=True) - shutil.copytree( - source_dir, - stage_dir, - ignore=shutil.ignore_patterns("target"), - ) - shutil.rmtree(stage_dir / "crates" / "oliphaunt-build", ignore_errors=True) - cargo_toml = stage_dir / "Cargo.toml" - rendered = render_oliphaunt_release_cargo_toml( - cargo_toml.read_text(encoding="utf-8"), - native_version, - broker_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 release source must keep SDK version {version}") - for target in artifact_targets( - product="liboliphaunt-native", - kind="native-runtime", - surface="rust-native-direct", - published_only=True, - ): - crate = liboliphaunt_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_facade = LIBOLIPHAUNT_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( - product="oliphaunt-broker", - kind="broker-helper", - surface="rust-broker", - published_only=True, - ): - 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 - - def wasix_release_asset_dir() -> Path: return ROOT / "target/oliphaunt-wasix/release-assets" @@ -2091,16 +1931,6 @@ def validate_staged_sdk_package(product: str) -> None: run(["tools/dev/bun.sh", "tools/release/check-staged-artifacts.mjs", "--require-sdk-product", product]) -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") - verify_staged_cargo_product_crates("oliphaunt-rust", version, allow_dirty=allow_dirty) - release_manifest = prepare_oliphaunt_release_source(version) - validate_generated_oliphaunt_release_artifact_coverage(release_manifest) - print(f"validated generated Rust SDK release source: {release_manifest.relative_to(ROOT)}") - print("validated staged Rust SDK crates; skipping source cargo publish dry-run.") - - def run_broker_dry_run() -> None: version = current_product_version("oliphaunt-broker") ensure_broker_release_assets() @@ -2147,8 +1977,6 @@ def run_product_publish_dry_runs(products: list[str], *, allow_dirty: bool, head run_runtime_maven_artifact_dry_run() elif product == "liboliphaunt-wasix": run_wasix_runtime_release_dry_run(allow_dirty) - elif product == "oliphaunt-rust": - run_rust_sdk_dry_run(allow_dirty, head_ref) elif product == "oliphaunt-broker": run_broker_dry_run() elif product == "oliphaunt-node-direct": @@ -2307,57 +2135,6 @@ def publish_react_native_npm(head_ref: str) -> None: upload_github_release_assets("oliphaunt-react-native") -def publish_rust_crates_io(head_ref: str) -> None: - if published_rerun("oliphaunt-rust", head_ref): - print("oliphaunt-rust is already published at this commit; skipping crates.io publish.") - return - verify_release_tag("oliphaunt-rust", head_ref) - version = current_product_version("oliphaunt-rust") - verify_staged_cargo_product_crates("oliphaunt-rust", version, allow_dirty=False) - broker_version = current_product_version("oliphaunt-broker") - native_version = current_product_version("liboliphaunt-native") - run( - [ - *REGISTRY_PUBLICATION_CHECK, - "--product", - "liboliphaunt-native", - "--registry-kind", - "crates", - "--require-published", - "--version", - native_version, - ] - ) - run( - [ - *REGISTRY_PUBLICATION_CHECK, - "--product", - "oliphaunt-broker", - "--registry-kind", - "crates", - "--require-published", - "--version", - broker_version, - ] - ) - cargo_publish_package("oliphaunt-build", version) - release_manifest = prepare_oliphaunt_release_source(version) - validate_generated_oliphaunt_release_artifact_coverage(release_manifest) - cargo_publish_manifest("oliphaunt", version, release_manifest) - run( - [ - *REGISTRY_PUBLICATION_CHECK, - "--product", - "oliphaunt-rust", - "--require-published", - "--retries", - "12", - "--retry-delay", - "10", - ] - ) - - def publish_broker_release_assets(head_ref: str) -> None: verify_release_tag("oliphaunt-broker", head_ref) ensure_broker_release_assets() @@ -3567,8 +3344,6 @@ def command_publish_product_step(args: argparse.Namespace) -> None: publish_kotlin_maven(head_ref) elif product == "oliphaunt-react-native" and step == "npm": publish_react_native_npm(head_ref) - elif product == "oliphaunt-rust" and step == "crates-io": - publish_rust_crates_io(head_ref) elif product == "oliphaunt-broker" and step == "github-release-assets": publish_broker_release_assets(head_ref) elif product == "oliphaunt-broker" and step == "crates-io": From b1e6674e4245cb087160ff92350ccd2ea49b677d Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 10:05:06 +0000 Subject: [PATCH 296/308] refactor: publish wasix rust sdk crates in bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 14 ++ tools/policy/check-tooling-stack.sh | 21 +++ tools/release/check_artifact_targets.mjs | 22 ++- tools/release/check_release_metadata.py | 13 +- tools/release/release-publish.mjs | 27 +++- tools/release/release.py | 126 ------------------ 6 files changed, 88 insertions(+), 135 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index aee0b5f6..a50ab8c1 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -3120,3 +3120,17 @@ until the current-state gates here are checked with fresh local evidence. `tools/dev/bun.sh tools/release/check-release-metadata.mjs`, `tools/dev/bun.sh tools/release/check_artifact_targets.mjs`, and `tools/dev/bun.sh tools/release/check-consumer-shape.mjs`. +- On 2026-06-28, the protected `oliphaunt-wasix-rust` crates.io publish step + moved onto the Bun `release-publish.mjs` surface. The route preserves the + staged SDK package validation and generated release manifest path while + requiring the matching `liboliphaunt-wasix` Cargo artifact crates to be + published first. The old Python `--wasm` publish/dry-run implementation was + removed from `release.py`; the legacy `publish-dry-run --wasm` compatibility + shortcut remains in Bun and still maps to the `oliphaunt-wasix-rust` SDK + product dry-run. `check_artifact_targets.mjs` now asserts the Bun SDK + dry-run helper validates staged SDK artifacts instead of requiring stale + `release.py` handlers. Fresh local evidence passed for `node --check + tools/release/release-publish.mjs`, `PYTHONPYCACHEPREFIX=target/python-pycache + python3 -m py_compile tools/release/release.py + tools/release/check_release_metadata.py`, and no-match searches for the + retired Python WASIX Rust publish functions in `tools/release/release.py`. diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 389658ec..ed23f53b 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -389,6 +389,19 @@ do fail "release.py must not retain Rust SDK dry-run or publish logic after it moved to Bun: $retired_rust_sdk_release_py" fi done +for retired_wasix_rust_sdk_release_py in \ + 'def render_oliphaunt_wasix_release_cargo_toml(' \ + 'def validate_generated_oliphaunt_wasix_release_artifact_coverage(' \ + 'def prepare_oliphaunt_wasix_release_source(' \ + 'def run_wasm_release_dry_run(' \ + 'def publish_wasm_crates_io(' \ + 'product == "oliphaunt-wasix-rust"' \ + '--wasm' +do + if grep -Fq -- "$retired_wasix_rust_sdk_release_py" tools/release/release.py; then + fail "release.py must not retain WASIX Rust SDK dry-run or publish logic after it moved to Bun: $retired_wasix_rust_sdk_release_py" + fi +done for retired_release_command in \ 'def command_check(' \ 'def command_check_registries(' \ @@ -532,6 +545,14 @@ grep -Fq 'await cargoPublishWorkspacePackage("oliphaunt-build", version)' tools/ fail "release-publish must publish oliphaunt-build before the oliphaunt crate" grep -Fq 'await cargoPublishManifest("oliphaunt", version, prepareRustSdkReleaseManifest())' tools/release/release-publish.mjs || fail "release-publish must publish the generated oliphaunt release manifest through Bun" +grep -Fq 'publishWasixRustCratesIo' tools/release/release-publish.mjs || + fail "release-publish must own oliphaunt-wasix-rust crates.io publication in Bun" +grep -Fq 'prepareOliphauntWasixReleaseSource(version)' tools/release/release-publish.mjs || + fail "release-publish must generate the oliphaunt-wasix release manifest through the shared Bun helper" +grep -Fq 'requireProductRegistryVersionPublished("liboliphaunt-wasix", "crates", runtimeVersion)' tools/release/release-publish.mjs || + fail "release-publish must require WASIX Cargo artifact publication before oliphaunt-wasix-rust" +grep -Fq 'await cargoPublishManifest("oliphaunt-wasix", version, releaseManifest)' tools/release/release-publish.mjs || + fail "release-publish must publish the generated oliphaunt-wasix release manifest through Bun" grep -Fq 'exactExtensionProducts(TOOL)' tools/release/release-publish.mjs || fail "release-publish must derive exact-extension publish routing from the canonical extension product set" for github_asset_product in liboliphaunt-native liboliphaunt-wasix oliphaunt-broker oliphaunt-node-direct; do diff --git a/tools/release/check_artifact_targets.mjs b/tools/release/check_artifact_targets.mjs index a9ffde90..da88c566 100644 --- a/tools/release/check_artifact_targets.mjs +++ b/tools/release/check_artifact_targets.mjs @@ -929,17 +929,27 @@ function validateCiReleaseArtifacts() { "Swift SDK release must use the Package.swift.release produced by the SDK package builder", ); requireText( - "tools/release/release.py", - "def validate_staged_sdk_package", - "release dry-runs must validate staged SDK package artifacts before publish checks", + "tools/release/release-sdk-product-dry-run.mjs", + 'run(TOOL, ["tools/dev/bun.sh", "tools/release/check-staged-artifacts.mjs", "--require-sdk-product", product]);', + "SDK product dry-runs must validate staged SDK package artifacts before publish checks in Bun", ); for (const productId of sdkPackageProducts()) { requireText( - "tools/release/release.py", - `validate_staged_sdk_package("${productId}")`, - `${productId} release dry-run must validate the staged SDK package artifact`, + "tools/release/release-sdk-product-dry-run.mjs", + `"${productId}",`, + `${productId} release dry-run must be handled by the Bun SDK dry-run helper`, ); } + requireText( + "tools/release/release-sdk-product-dry-run.mjs", + 'verifyStagedCargoProductCrates("oliphaunt-rust")', + "oliphaunt-rust release dry-run must validate staged Cargo crate identities", + ); + requireText( + "tools/release/release-sdk-product-dry-run.mjs", + 'verifyStagedCargoProductCrates("oliphaunt-wasix-rust")', + "oliphaunt-wasix-rust release dry-run must validate staged Cargo crate identities", + ); requireText( ".github/scripts/run-planned-moon-job.sh", "OLIPHAUNT_MOON_UPSTREAM", diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 24c103d0..5f9bda66 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -772,6 +772,13 @@ def validate_graph_files() -> None: or "def run_rust_sdk_dry_run(" in release_source or "def publish_rust_crates_io(" in release_source or 'product == "oliphaunt-rust"' in release_source + or "def render_oliphaunt_wasix_release_cargo_toml(" in release_source + or "def validate_generated_oliphaunt_wasix_release_artifact_coverage(" in release_source + or "def prepare_oliphaunt_wasix_release_source(" in release_source + or "def run_wasm_release_dry_run(" in release_source + or "def publish_wasm_crates_io(" in release_source + or 'product == "oliphaunt-wasix-rust"' in release_source + or "--wasm" in release_source or "renderReleaseCargoToml(" not in prepare_rust_release_source or "currentProductVersionSync(RUST_PRODUCT" not in prepare_rust_release_source or "allArtifactTargets({ product, kind, surface, publishedOnly: true }" not in prepare_rust_release_source @@ -1056,6 +1063,10 @@ def validate_publish_target_coverage() -> None: or 'requireProductRegistryVersionPublished("oliphaunt-broker", "crates", brokerVersion)' not in release_publish or 'await cargoPublishWorkspacePackage("oliphaunt-build", version)' not in release_publish or 'await cargoPublishManifest("oliphaunt", version, prepareRustSdkReleaseManifest())' not in release_publish + or "publishWasixRustCratesIo" not in release_publish + or "prepareOliphauntWasixReleaseSource(version)" not in release_publish + or 'requireProductRegistryVersionPublished("liboliphaunt-wasix", "crates", runtimeVersion)' not in release_publish + or 'await cargoPublishManifest("oliphaunt-wasix", version, releaseManifest)' not in release_publish or "exactExtensionProducts(TOOL)" not in release_publish or '"liboliphaunt-native"' not in release_publish or '"liboliphaunt-wasix"' not in release_publish @@ -1117,7 +1128,7 @@ def validate_publish_target_coverage() -> None: saw_extension = True continue for step in step_coverage: - if product == "oliphaunt-rust" and step == "crates-io": + if product in {"oliphaunt-rust", "oliphaunt-wasix-rust"} and step == "crates-io": if f'publishProductStep?.product === "{product}" && publishProductStep.step === "{step}"' not in release_publish: fail(f"Bun publish implementation must dispatch publish step {product}:{step}") elif f'product == "{product}" and step == "{step}"' not in release_source: diff --git a/tools/release/release-publish.mjs b/tools/release/release-publish.mjs index fd8cea52..a4491763 100755 --- a/tools/release/release-publish.mjs +++ b/tools/release/release-publish.mjs @@ -24,6 +24,7 @@ import { stagedSdkNpmPackageTarball, verifyStagedCargoProductCrates, } from "./release-sdk-product-dry-run.mjs"; +import { prepareOliphauntWasixReleaseSource } from "./package_oliphaunt_wasix_sdk_crate.mjs"; import { artifactTargets, compareText, @@ -45,8 +46,8 @@ function usage() { Runs protected release publish and publish dry-run operations through the Bun release command surface. The public no-product publish dry-run and product dry-runs are handled in Bun, including the legacy --wasm shortcut for the WASIX -Rust SDK dry-run. Protected publish dispatch still delegates to release.py while -the protected implementation is ported. +Rust SDK dry-run. Protected publish steps that have not yet moved to Bun still +delegate to release.py while the remaining implementation is ported. `); } @@ -622,6 +623,23 @@ async function publishRustCratesIo(headRef) { requireProductRegistryPublished(product, null); } +async function publishWasixRustCratesIo(headRef) { + const product = "oliphaunt-wasix-rust"; + if (publishedRerun(product, headRef)) { + console.log("oliphaunt-wasix-rust is already published at this commit; skipping crates.io publish."); + return; + } + verifyReleaseTag(product, headRef); + const runtimeVersion = currentProductVersionSync("liboliphaunt-wasix", TOOL); + requireProductRegistryVersionPublished("liboliphaunt-wasix", "crates", runtimeVersion); + const version = currentProductVersionSync(product, TOOL); + run(TOOL, ["tools/dev/bun.sh", "tools/release/check-staged-artifacts.mjs", "--require-sdk-product", product]); + verifyStagedCargoProductCrates(product); + const releaseManifest = await prepareOliphauntWasixReleaseSource(version); + await cargoPublishManifest("oliphaunt-wasix", version, releaseManifest); + requireProductRegistryPublished(product, null); +} + function publishLiboliphauntRuntimeMaven(headRef) { const product = "liboliphaunt-native"; verifyReleaseTag(product, headRef); @@ -807,6 +825,11 @@ if (publishProductStep?.product === "oliphaunt-rust" && publishProductStep.step process.exit(0); } +if (publishProductStep?.product === "oliphaunt-wasix-rust" && publishProductStep.step === "crates-io") { + await publishWasixRustCratesIo(publishProductStep.headRef); + process.exit(0); +} + if (publishProductStep?.step === "maven-central" && EXTENSION_PRODUCTS.has(publishProductStep.product)) { publishSelectedExtensionMaven([publishProductStep.product], publishProductStep.headRef); process.exit(0); diff --git a/tools/release/release.py b/tools/release/release.py index fe5abda9..a803c0b0 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -1109,67 +1109,6 @@ def maven_pom_url(coordinate: str, version: str) -> str: ) -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 = set(wasix_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) - 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 = set(wasix_public_cargo_package_names()) - 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 wasix_release_asset_dir() -> Path: return ROOT / "target/oliphaunt-wasix/release-assets" @@ -1463,60 +1402,6 @@ 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 and generated publish manifest; " - "source publish runs after WASIX artifact crates are published." - ) - - -def publish_wasm_crates_io(head_ref: str) -> None: - if published_rerun("oliphaunt-wasix-rust", head_ref): - print("oliphaunt-wasix is already published at this commit; skipping crates.io publish.") - return - - verify_release_tag("oliphaunt-wasix-rust", head_ref) - run( - [ - *REGISTRY_PUBLICATION_CHECK, - "--product", - "liboliphaunt-wasix", - "--registry-kind", - "crates", - "--require-published", - "--retries", - "12", - "--retry-delay", - "10", - ] - ) - 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) - cargo_publish_manifest("oliphaunt-wasix", version, release_manifest) - run( - [ - *REGISTRY_PUBLICATION_CHECK, - "--product", - "oliphaunt-wasix-rust", - "--require-published", - "--retries", - "12", - "--retry-delay", - "10", - ] - ) - - def liboliphaunt_release_asset_dir() -> Path: return ROOT / "target" / "liboliphaunt" / "release-assets" @@ -1989,11 +1874,6 @@ def run_product_publish_dry_runs(products: list[str], *, allow_dirty: bool, head run_react_native_sdk_dry_run() elif product == "oliphaunt-js": run_typescript_sdk_dry_run(allow_dirty) - elif product == "oliphaunt-wasix-rust": - if published_rerun("oliphaunt-wasix-rust", head_ref): - print("oliphaunt-wasix is already published at this commit; skipping WASM publish dry-run.") - else: - run_wasm_release_dry_run(allow_dirty) elif is_extension_product(product): run_extension_artifact_dry_run(product) else: @@ -3356,8 +3236,6 @@ def command_publish_product_step(args: argparse.Namespace) -> None: publish_node_direct_npm_optional_packages(head_ref) elif product == "oliphaunt-js" and step == "npm-jsr": publish_typescript_npm_jsr(head_ref) - elif product == "oliphaunt-wasix-rust" and step == "crates-io": - publish_wasm_crates_io(head_ref) elif is_extension_product(product) and step == "github-release-assets": publish_extension_release_assets(product, head_ref) elif is_extension_product(product) and step == "maven-central": @@ -3377,8 +3255,6 @@ def command_publish_dry_run(args: argparse.Namespace, passthrough: list[str]) -> head_ref=passthrough_value(passthrough, "--head-ref") or "HEAD", ) return - if args.wasm: - run_wasm_release_dry_run(args.allow_dirty) if passthrough: run(["tools/dev/bun.sh", "tools/release/release-check-registries.mjs", *passthrough]) @@ -3405,11 +3281,9 @@ def main(argv: list[str]) -> int: subparsers = parser.add_subparsers(dest="command", required=True) dry_run = subparsers.add_parser("publish-dry-run") - dry_run.add_argument("--wasm", action="store_true") dry_run.add_argument("--allow-dirty", action="store_true") publish = subparsers.add_parser("publish") - publish.add_argument("--wasm", action="store_true") publish.add_argument("--allow-dirty", action="store_true") publish.add_argument("--product") publish.add_argument("--step") From 02c6dba1acf26a33b0a63c31fd96624a4c183fcc Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 10:23:23 +0000 Subject: [PATCH 297/308] refactor: publish typescript sdk packages in bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 21 ++++++ tools/policy/check-tooling-stack.sh | 8 ++ tools/release/check_artifact_targets.mjs | 26 +++---- tools/release/check_release_metadata.py | 12 ++- tools/release/release-publish.mjs | 30 ++++++++ tools/release/release-sdk-product-dry-run.mjs | 2 +- tools/release/release.py | 75 ------------------- 7 files changed, 84 insertions(+), 90 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index a50ab8c1..b8223970 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -3134,3 +3134,24 @@ until the current-state gates here are checked with fresh local evidence. python3 -m py_compile tools/release/release.py tools/release/check_release_metadata.py`, and no-match searches for the retired Python WASIX Rust publish functions in `tools/release/release.py`. +- On 2026-06-28, the protected `oliphaunt-js` npm/JSR publish step moved onto + the Bun `release-publish.mjs` surface. The route verifies the product tag and + release-version/registry state, publishes the staged CI npm tarball, publishes + JSR from the staged `target/sdk-artifacts/oliphaunt-js/jsr-source` tree when + JSR is not already visible, verifies npm plus JSR publication through the + shared registry checker, and preserves the empty GitHub release-asset publish. + The Python TypeScript JSR helper, product dry-run branch, and protected + `npm-jsr` publish branch were removed from `release.py`; policy checks now + require the staged JSR source and npm tarball validation through Bun. Fresh + local evidence passed for `node --check tools/release/release-publish.mjs`, + `node --check tools/release/release-sdk-product-dry-run.mjs`, + `PYTHONPYCACHEPREFIX=target/python-pycache python3 -m py_compile + tools/release/release.py tools/release/check_release_metadata.py`, + `tools/dev/bun.sh tools/release/release-publish.mjs publish --product + oliphaunt-js --step npm-jsr --head-ref oliphaunt-not-a-ref` failing at Bun + tag verification, `tools/dev/bun.sh + tools/release/release-sdk-product-dry-run.mjs --product oliphaunt-js + --allow-dirty`, `tools/dev/bun.sh tools/release/check_artifact_targets.mjs`, + `tools/dev/bun.sh tools/release/check-release-metadata.mjs`, `bash + tools/policy/check-tooling-stack.sh`, `bash tools/policy/check-docs.sh`, and + `tools/dev/bun.sh tools/release/check-consumer-shape.mjs`. diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index ed23f53b..4bca2ab1 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -533,6 +533,12 @@ grep -Fq 'stagedSdkNpmPackageTarball(product)' tools/release/release-publish.mjs fail "release-publish must validate the staged React Native npm tarball before publish" grep -Fq 'uploadGithubReleaseAssets(product, [])' tools/release/release-publish.mjs || fail "release-publish must preserve React Native no-asset GitHub release publication in Bun" +grep -Fq 'publishTypescriptNpmJsr' tools/release/release-publish.mjs || + fail "release-publish must own TypeScript npm/JSR publication in Bun" +grep -Fq 'stagedJsrSourceDir(product)' tools/release/release-publish.mjs || + fail "release-publish must publish JSR from the staged CI source artifact" +grep -Fq 'productRegistryPublished(product, "jsr")' tools/release/release-publish.mjs || + fail "release-publish must skip JSR publish when the TypeScript SDK is already visible" grep -Fq 'publishRustCratesIo' tools/release/release-publish.mjs || fail "release-publish must own oliphaunt-rust crates.io publication in Bun" grep -Fq 'verifyStagedCargoProductCrates(product)' tools/release/release-publish.mjs || @@ -655,6 +661,8 @@ grep -Fq '"oliphaunt-js",' tools/release/release-sdk-product-dry-run.mjs || fail "release SDK product dry-run helper must declare Bun-owned low-risk SDK product dry-runs" grep -Fq 'tools/release/check-staged-artifacts.mjs", "--require-sdk-product", product' tools/release/release-sdk-product-dry-run.mjs || fail "Bun product dry-runs must validate staged SDK artifacts through the Bun checker" +grep -Fq 'export function stagedJsrSourceDir(product)' tools/release/release-sdk-product-dry-run.mjs || + fail "Bun SDK product helpers must expose the staged JSR source directory for TypeScript publishing" grep -Fq 'prepareStagedSwiftReleaseManifest' tools/release/release-sdk-product-dry-run.mjs || fail "Bun SDK product dry-runs must preserve Swift staged release manifest validation" grep -Fq 'stagedKotlinMavenRepo' tools/release/release-sdk-product-dry-run.mjs || diff --git a/tools/release/check_artifact_targets.mjs b/tools/release/check_artifact_targets.mjs index da88c566..81bf50cc 100644 --- a/tools/release/check_artifact_targets.mjs +++ b/tools/release/check_artifact_targets.mjs @@ -1083,9 +1083,9 @@ function validateCiReleaseArtifacts() { "release CLI must fail closed when WASIX releases lack staged CI-built runtime artifacts", ); requireText( - "tools/release/release.py", + "tools/release/release-sdk-product-dry-run.mjs", "requires staged JSR source", - "release CLI must fail closed when TypeScript JSR release artifacts are not staged", + "Bun SDK release helper must fail closed when TypeScript JSR release artifacts are not staged", ); requireText( ".github/workflows/release.yml", @@ -1293,24 +1293,24 @@ function validateCiReleaseArtifacts() { "TypeScript SDK builder must stage source for JSR publishing in addition to the npm tarball", ); requireText( - "tools/release/release.py", - 'staged_jsr_source_dir("oliphaunt-js")', - "TypeScript SDK release must publish JSR from staged CI-built source artifacts", + "tools/release/release-publish.mjs", + "stagedJsrSourceDir(product)", + "TypeScript SDK release must publish JSR from staged CI-built source artifacts in Bun", ); requireText( - "tools/release/release.py", - "validate_staged_npm_package_tarball", - "npm SDK release steps must validate CI-built package tarballs before dry-run or publish", + "tools/release/release-sdk-product-dry-run.mjs", + "validateStagedNpmPackageTarball(product, matches[0])", + "npm SDK release steps must validate CI-built package tarballs before dry-run or publish in Bun", ); requireText( - "tools/release/release.py", + "tools/release/release-sdk-product-dry-run.mjs", "must not contain workspace: dependency specifiers", - "staged npm SDK package validation must reject unpublished workspace protocol specs", + "Bun staged npm SDK package validation must reject unpublished workspace protocol specs", ); requireText( - "tools/release/release.py", - "verify_staged_cargo_crate_identity", - "Cargo SDK release steps must verify staged CI-built .crate identity before dry-run or publish", + "tools/release/release-sdk-product-dry-run.mjs", + "verifyStagedCargoProductCrates", + "Bun Cargo SDK release steps must verify staged CI-built .crate identity before dry-run or publish", ); for (const forbidden of [ "tools/release/package-liboliphaunt-assets.sh", diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 5f9bda66..86fad01c 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -779,6 +779,10 @@ def validate_graph_files() -> None: or "def publish_wasm_crates_io(" in release_source or 'product == "oliphaunt-wasix-rust"' in release_source or "--wasm" in release_source + or "def staged_jsr_source_dir(" in release_source + or "def run_typescript_sdk_dry_run(" in release_source + or "def publish_typescript_npm_jsr(" in release_source + or 'product == "oliphaunt-js"' in release_source or "renderReleaseCargoToml(" not in prepare_rust_release_source or "currentProductVersionSync(RUST_PRODUCT" not in prepare_rust_release_source or "allArtifactTargets({ product, kind, surface, publishedOnly: true }" not in prepare_rust_release_source @@ -1057,6 +1061,9 @@ def validate_publish_target_coverage() -> None: or "publishReactNativeNpm" not in release_publish or "stagedSdkNpmPackageTarball(product)" not in release_publish or "uploadGithubReleaseAssets(product, [])" not in release_publish + or "publishTypescriptNpmJsr" not in release_publish + or "stagedJsrSourceDir(product)" not in release_publish + or 'productRegistryPublished(product, "jsr")' not in release_publish or "publishRustCratesIo" not in release_publish or "verifyStagedCargoProductCrates(product)" not in release_publish or 'requireProductRegistryVersionPublished("liboliphaunt-native", "crates", nativeVersion)' not in release_publish @@ -1128,7 +1135,10 @@ def validate_publish_target_coverage() -> None: saw_extension = True continue for step in step_coverage: - if product in {"oliphaunt-rust", "oliphaunt-wasix-rust"} and step == "crates-io": + if ( + (product in {"oliphaunt-rust", "oliphaunt-wasix-rust"} and step == "crates-io") + or (product == "oliphaunt-js" and step == "npm-jsr") + ): if f'publishProductStep?.product === "{product}" && publishProductStep.step === "{step}"' not in release_publish: fail(f"Bun publish implementation must dispatch publish step {product}:{step}") elif f'product == "{product}" and step == "{step}"' not in release_source: diff --git a/tools/release/release-publish.mjs b/tools/release/release-publish.mjs index a4491763..bc5dca7e 100755 --- a/tools/release/release-publish.mjs +++ b/tools/release/release-publish.mjs @@ -21,6 +21,7 @@ import { runMavenArtifactPublisher, } from "./release-product-dry-run.mjs"; import { + stagedJsrSourceDir, stagedSdkNpmPackageTarball, verifyStagedCargoProductCrates, } from "./release-sdk-product-dry-run.mjs"; @@ -604,6 +605,30 @@ function publishReactNativeNpm(headRef) { uploadGithubReleaseAssets(product, []); } +function publishTypescriptNpmJsr(headRef) { + const product = "oliphaunt-js"; + const packageName = "@oliphaunt/ts"; + verifyReleaseTag(product, headRef); + run(TOOL, [ + "tools/dev/bun.sh", + "tools/release/check_release_versions.mjs", + "--products-json", + JSON.stringify([product]), + "--head-ref", + headRef, + "--check-registries", + ]); + const version = currentProductVersionSync(product, TOOL); + npmPublishTarball(packageName, stagedSdkNpmPackageTarball(product), version); + if (productRegistryPublished(product, "jsr")) { + console.log(`jsr:${packageName} ${version} is already published; skipping jsr publish.`); + } else { + run(TOOL, ["pnpm", "exec", "jsr", "publish"], { cwd: stagedJsrSourceDir(product) }); + } + requireProductRegistryPublished(product, null); + uploadGithubReleaseAssets(product, []); +} + async function publishRustCratesIo(headRef) { const product = "oliphaunt-rust"; if (publishedRerun(product, headRef)) { @@ -820,6 +845,11 @@ if (publishProductStep?.product === "oliphaunt-react-native" && publishProductSt process.exit(0); } +if (publishProductStep?.product === "oliphaunt-js" && publishProductStep.step === "npm-jsr") { + publishTypescriptNpmJsr(publishProductStep.headRef); + process.exit(0); +} + if (publishProductStep?.product === "oliphaunt-rust" && publishProductStep.step === "crates-io") { await publishRustCratesIo(publishProductStep.headRef); process.exit(0); diff --git a/tools/release/release-sdk-product-dry-run.mjs b/tools/release/release-sdk-product-dry-run.mjs index 74a53e2e..d9f93514 100644 --- a/tools/release/release-sdk-product-dry-run.mjs +++ b/tools/release/release-sdk-product-dry-run.mjs @@ -89,7 +89,7 @@ function requireStagedSdkArtifact(product, description, suffixes) { return matches; } -function stagedJsrSourceDir(product) { +export function stagedJsrSourceDir(product) { const directory = path.join(sdkArtifactDir(product), "jsr-source"); requireDirectory( directory, diff --git a/tools/release/release.py b/tools/release/release.py index a803c0b0..6d0f7bd4 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -836,20 +836,6 @@ def validate_staged_npm_package_tarball(product: str, tarball: Path) -> None: fail(f"{tarball.relative_to(ROOT)} is not a valid staged npm package tarball: {error}") -def staged_jsr_source_dir(product: str) -> Path | None: - directory = sdk_artifact_dir(product) / "jsr-source" - if not directory.is_dir(): - fail( - f"{product} requires staged JSR source under {directory.relative_to(ROOT)}; " - "download the CI workflow SDK package artifacts before release validation or publishing" - ) - required = ["jsr.json", "package.json", "src"] - missing = [name for name in required if not (directory / name).exists()] - if missing: - fail(f"{product} staged JSR source is missing: {', '.join(missing)}") - return directory - - def npm_publish_pnpm_packed_package(package_dir: Path, *, product: str | None = None) -> None: tarball = staged_npm_package_tarball(product) if product is not None else None if tarball is None: @@ -1838,16 +1824,6 @@ def run_react_native_sdk_dry_run() -> None: require_staged_sdk_artifact("oliphaunt-react-native", "npm package", (".tgz",)) -def run_typescript_sdk_dry_run(allow_dirty: bool) -> None: - validate_staged_sdk_package("oliphaunt-js") - require_staged_sdk_artifact("oliphaunt-js", "npm package", (".tgz",)) - jsr_source = staged_jsr_source_dir("oliphaunt-js") - command = ["pnpm", "exec", "jsr", "publish", "--dry-run"] - if allow_dirty: - command.append("--allow-dirty") - run(command, cwd=jsr_source) - - def run_node_direct_dry_run() -> None: run(["src/runtimes/node-direct/tools/check-package.sh", "package-shape"]) ensure_node_direct_release_assets() @@ -1872,8 +1848,6 @@ def run_product_publish_dry_runs(products: list[str], *, allow_dirty: bool, head run_kotlin_sdk_dry_run() elif product == "oliphaunt-react-native": run_react_native_sdk_dry_run() - elif product == "oliphaunt-js": - run_typescript_sdk_dry_run(allow_dirty) elif is_extension_product(product): run_extension_artifact_dry_run(product) else: @@ -3074,53 +3048,6 @@ def publish_broker_npm_packages(head_ref: str) -> None: ) -def publish_typescript_npm_jsr(head_ref: str) -> None: - verify_release_tag("oliphaunt-js", head_ref) - run( - [ - "tools/dev/bun.sh", - "tools/release/check_release_versions.mjs", - "--products-json", - '["oliphaunt-js"]', - "--head-ref", - head_ref, - "--check-registries", - ] - ) - version = current_product_version("oliphaunt-js") - if npm_package_is_published("@oliphaunt/ts", version): - print(f"@oliphaunt/ts {version} is already published on npm; skipping npm publish.") - else: - npm_publish_pnpm_packed_package(ROOT / "src/sdks/js", product="oliphaunt-js") - if succeeds( - [ - *REGISTRY_PUBLICATION_CHECK, - "--product", - "oliphaunt-js", - "--registry-kind", - "jsr", - "--require-published", - ] - ): - print(f"jsr:@oliphaunt/ts {version} is already published; skipping jsr publish.") - else: - jsr_source = staged_jsr_source_dir("oliphaunt-js") or (ROOT / "src/sdks/js") - run(["pnpm", "exec", "jsr", "publish"], cwd=jsr_source) - run( - [ - *REGISTRY_PUBLICATION_CHECK, - "--product", - "oliphaunt-js", - "--require-published", - "--retries", - "12", - "--retry-delay", - "10", - ] - ) - upload_github_release_assets("oliphaunt-js", assets=[]) - - def publish_wasm_release_assets() -> None: validate_wasix_release_assets() asset_dir = wasix_release_asset_dir() @@ -3234,8 +3161,6 @@ def command_publish_product_step(args: argparse.Namespace) -> None: publish_node_direct_release_assets(head_ref) elif product == "oliphaunt-node-direct" and step == "npm": publish_node_direct_npm_optional_packages(head_ref) - elif product == "oliphaunt-js" and step == "npm-jsr": - publish_typescript_npm_jsr(head_ref) elif is_extension_product(product) and step == "github-release-assets": publish_extension_release_assets(product, head_ref) elif is_extension_product(product) and step == "maven-central": From 22c32e50ac4b4e0100a48006b2ef5fe304deb4e1 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 10:31:36 +0000 Subject: [PATCH 298/308] refactor: publish swift release in bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 23 +++++++ tools/policy/check-tooling-stack.sh | 14 ++++ tools/release/check_artifact_targets.mjs | 11 ++- tools/release/check_release_metadata.py | 66 +++++++++--------- tools/release/release-publish.mjs | 24 +++++++ tools/release/release-sdk-product-dry-run.mjs | 6 +- tools/release/release.py | 69 ------------------- 7 files changed, 108 insertions(+), 105 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index b8223970..c7e311ed 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -3155,3 +3155,26 @@ until the current-state gates here are checked with fresh local evidence. `tools/dev/bun.sh tools/release/check-release-metadata.mjs`, `bash tools/policy/check-tooling-stack.sh`, `bash tools/policy/check-docs.sh`, and `tools/dev/bun.sh tools/release/check-consumer-shape.mjs`. +- On 2026-06-28, the protected `oliphaunt-swift` GitHub release/source-tag + publish step moved onto the Bun `release-publish.mjs` surface. The route + verifies the product tag, reuses the exported Bun + `prepareStagedSwiftReleaseManifest()` helper to validate and stage + `Oliphaunt-source.zip`, `Package.swift.release`, and the generated SwiftPM + release tree, runs `publish_swiftpm_source_tag.mjs` with `--manifest`, + `--include-tree`, and `--push`, and preserves the empty GitHub release asset + upload. The retired Python Swift staging, dry-run, and publish helpers were + removed from `release.py`; policy checks now require Swift publish ownership + in Bun and reject the old Python symbols. Fresh local evidence passed for + `node --check tools/release/release-publish.mjs`, + `node --check tools/release/release-sdk-product-dry-run.mjs`, + `node --check tools/release/check_artifact_targets.mjs`, + `PYTHONPYCACHEPREFIX=target/python-pycache python3 -m py_compile + tools/release/release.py tools/release/check_release_metadata.py`, + no-match `rg` for the retired Swift Python symbols in `release.py`, + `tools/dev/bun.sh tools/release/release-publish.mjs publish --product + oliphaunt-swift --step github-release --head-ref oliphaunt-not-a-ref` + failing at Bun tag verification, `bash tools/policy/check-tooling-stack.sh`, + `tools/dev/bun.sh tools/release/check-release-metadata.mjs`, + `tools/dev/bun.sh tools/release/check_artifact_targets.mjs`, `bash + tools/policy/check-docs.sh`, `tools/dev/bun.sh + tools/release/check-consumer-shape.mjs`, and `git diff --check`. diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 4bca2ab1..c4784213 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -533,6 +533,12 @@ grep -Fq 'stagedSdkNpmPackageTarball(product)' tools/release/release-publish.mjs fail "release-publish must validate the staged React Native npm tarball before publish" grep -Fq 'uploadGithubReleaseAssets(product, [])' tools/release/release-publish.mjs || fail "release-publish must preserve React Native no-asset GitHub release publication in Bun" +grep -Fq 'publishSwiftGithubRelease' tools/release/release-publish.mjs || + fail "release-publish must own Swift GitHub release/source-tag publication in Bun" +grep -Fq 'prepareStagedSwiftReleaseManifest()' tools/release/release-publish.mjs || + fail "release-publish must validate and stage SwiftPM release artifacts through the Bun helper before tagging" +grep -Fq 'tools/release/publish_swiftpm_source_tag.mjs' tools/release/release-publish.mjs || + fail "release-publish must create/push the SwiftPM source tag through the Bun source-tag publisher" grep -Fq 'publishTypescriptNpmJsr' tools/release/release-publish.mjs || fail "release-publish must own TypeScript npm/JSR publication in Bun" grep -Fq 'stagedJsrSourceDir(product)' tools/release/release-publish.mjs || @@ -665,6 +671,14 @@ grep -Fq 'export function stagedJsrSourceDir(product)' tools/release/release-sdk fail "Bun SDK product helpers must expose the staged JSR source directory for TypeScript publishing" grep -Fq 'prepareStagedSwiftReleaseManifest' tools/release/release-sdk-product-dry-run.mjs || fail "Bun SDK product dry-runs must preserve Swift staged release manifest validation" +grep -Fq 'export function prepareStagedSwiftReleaseManifest()' tools/release/release-sdk-product-dry-run.mjs || + fail "Bun SDK product helper must export Swift staged release manifest preparation for publish" +if grep -Fq 'def publish_swift_release(' tools/release/release.py; then + fail "release.py must not own Swift GitHub release publishing after the route moved to Bun" +fi +if grep -Fq 'def staged_swift_release_artifacts(' tools/release/release.py; then + fail "release.py must not own Swift staged artifact validation after the route moved to Bun" +fi grep -Fq 'stagedKotlinMavenRepo' tools/release/release-sdk-product-dry-run.mjs || fail "Bun SDK product dry-runs must preserve Kotlin staged Maven repository validation" grep -Fq 'stagedSdkNpmPackageTarball(product)' tools/release/release-sdk-product-dry-run.mjs || diff --git a/tools/release/check_artifact_targets.mjs b/tools/release/check_artifact_targets.mjs index 81bf50cc..f861bf19 100644 --- a/tools/release/check_artifact_targets.mjs +++ b/tools/release/check_artifact_targets.mjs @@ -924,9 +924,14 @@ function validateCiReleaseArtifacts() { "release workflow must still download aggregate liboliphaunt assets for liboliphaunt-native releases", ); requireText( - "tools/release/release.py", - "prepare_staged_swift_release_manifest", - "Swift SDK release must use the Package.swift.release produced by the SDK package builder", + "tools/release/release-sdk-product-dry-run.mjs", + "export function prepareStagedSwiftReleaseManifest()", + "Swift SDK release must use the Package.swift.release produced by the SDK package builder through the Bun helper", + ); + requireText( + "tools/release/release-publish.mjs", + "publishSwiftGithubRelease", + "Swift SDK GitHub release/source-tag publish must run through the Bun release-publish entrypoint", ); requireText( "tools/release/release-sdk-product-dry-run.mjs", diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index 86fad01c..a7e32b4c 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1061,6 +1061,10 @@ def validate_publish_target_coverage() -> None: or "publishReactNativeNpm" not in release_publish or "stagedSdkNpmPackageTarball(product)" not in release_publish or "uploadGithubReleaseAssets(product, [])" not in release_publish + or "publishSwiftGithubRelease" not in release_publish + or "prepareStagedSwiftReleaseManifest()" not in release_publish + or "tools/release/publish_swiftpm_source_tag.mjs" not in release_publish + or 'publishProductStep?.product === "oliphaunt-swift" && publishProductStep.step === "github-release"' not in release_publish or "publishTypescriptNpmJsr" not in release_publish or "stagedJsrSourceDir(product)" not in release_publish or 'productRegistryPublished(product, "jsr")' not in release_publish @@ -1111,14 +1115,17 @@ def validate_publish_target_coverage() -> None: or '"oliphaunt-swift",' not in release_sdk_product_dry_run or 'tools/release/check-staged-artifacts.mjs", "--require-sdk-product", product' not in release_sdk_product_dry_run or "prepareStagedSwiftReleaseManifest" not in release_sdk_product_dry_run + or "export function prepareStagedSwiftReleaseManifest()" not in release_sdk_product_dry_run or "stagedKotlinMavenRepo" not in release_sdk_product_dry_run or "stagedSdkNpmPackageTarball(product)" not in release_sdk_product_dry_run or 'verifyStagedCargoProductCrates("oliphaunt-rust")' not in release_sdk_product_dry_run or "tools/release/prepare-rust-release-source.mjs" not in release_sdk_product_dry_run or "prepareOliphauntWasixReleaseSource" not in release_sdk_product_dry_run + or "def publish_swift_release(" in release_source + or "def staged_swift_release_artifacts(" in release_source or 'spawnSync("tools/release/release.py", argv' not in release_publish ): - fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper and exact-extension GitHub asset publish steps must run in Bun, liboliphaunt-native and exact-extension Maven publication must run in Bun, liboliphaunt-native, broker, Node direct, and React Native npm publication must run in Bun, native, Broker, WASIX, and Rust SDK Cargo artifact publication must run in Bun, and React Native SDK tasks must not track release.py directly") + fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper and exact-extension GitHub asset publish steps must run in Bun, liboliphaunt-native and exact-extension Maven publication must run in Bun, liboliphaunt-native, broker, Node direct, Swift, and React Native npm/publication paths must run in Bun, native, Broker, WASIX, and Rust SDK Cargo artifact publication must run in Bun, and React Native SDK tasks must not track release.py directly") 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 @@ -1138,6 +1145,7 @@ def validate_publish_target_coverage() -> None: if ( (product in {"oliphaunt-rust", "oliphaunt-wasix-rust"} and step == "crates-io") or (product == "oliphaunt-js" and step == "npm-jsr") + or (product == "oliphaunt-swift" and step == "github-release") ): if f'publishProductStep?.product === "{product}" && publishProductStep.step === "{step}"' not in release_publish: fail(f"Bun publish implementation must dispatch publish step {product}:{step}") @@ -1454,41 +1462,37 @@ def validate_swift(swift_version: str, liboliphaunt_version: str) -> None: "--include-tree", "SwiftPM source-tag publisher must be able to include generated release-tree files", ) - require_text( - "tools/release/release.py", - "staged_swift_release_artifacts", - "release CLI must validate staged Swift source and SwiftPM manifest artifacts before dry-run or tagging", - ) - require_text( - "tools/release/release.py", + release_py = read_text("tools/release/release.py") + for retired in ( + "def staged_swift_release_artifacts(", + "def prepare_staged_swift_release_manifest(", + "def run_swift_sdk_dry_run(", + "def publish_swift_release(", + 'product == "oliphaunt-swift" and step == "github-release"', + ): + if retired in release_py: + fail(f"Swift release staging/publishing must run in Bun, not release.py: {retired}") + release_sdk_product_dry_run = read_text("tools/release/release-sdk-product-dry-run.mjs") + for required in ( + "export function prepareStagedSwiftReleaseManifest()", "Oliphaunt-source.zip", - "release CLI must require the staged Swift source archive", - ) - require_text( - "tools/release/release.py", "Package.swift.release", - "release CLI must require the staged SwiftPM release manifest", - ) - require_text( - "tools/release/release.py", "apple-spm-xcframework.zip", - "release CLI must validate that the staged SwiftPM manifest points at the Apple liboliphaunt binary artifact", - ) - require_text( - "tools/release/release.py", + 'path.join(outputDir, "Package.swift.release")', + ): + if required not in release_sdk_product_dry_run: + fail(f"Bun SDK dry-run helper must preserve Swift staged release artifact validation: {required}") + release_publish = read_text("tools/release/release-publish.mjs") + for required in ( + "publishSwiftGithubRelease", + "prepareStagedSwiftReleaseManifest()", + "tools/release/publish_swiftpm_source_tag.mjs", "--manifest", - "release CLI must pass a SwiftPM manifest to the source-tag publisher", - ) - require_text( - "tools/release/release.py", "--include-tree", - "release CLI must pass the SwiftPM release-tree root to the source-tag publisher", - ) - require_text( - "tools/release/release.py", - 'output_manifest = output_dir / "Package.swift.release"', - "release CLI must stage the SwiftPM binary manifest before tagging", - ) + "target/oliphaunt-swift/release-tree", + ): + if required not in release_publish: + fail(f"Bun release-publish must own Swift GitHub release/source-tag publishing: {required}") require_text( "src/sdks/swift/README.md", "Normal iOS and macOS app consumers do not install Rust", diff --git a/tools/release/release-publish.mjs b/tools/release/release-publish.mjs index bc5dca7e..7543b27f 100755 --- a/tools/release/release-publish.mjs +++ b/tools/release/release-publish.mjs @@ -21,6 +21,7 @@ import { runMavenArtifactPublisher, } from "./release-product-dry-run.mjs"; import { + prepareStagedSwiftReleaseManifest, stagedJsrSourceDir, stagedSdkNpmPackageTarball, verifyStagedCargoProductCrates, @@ -605,6 +606,24 @@ function publishReactNativeNpm(headRef) { uploadGithubReleaseAssets(product, []); } +function publishSwiftGithubRelease(headRef) { + const product = "oliphaunt-swift"; + verifyReleaseTag(product, headRef); + const manifest = prepareStagedSwiftReleaseManifest(); + run(TOOL, [ + "tools/dev/bun.sh", + "tools/release/publish_swiftpm_source_tag.mjs", + "--target", + headRef, + "--manifest", + rel(manifest), + "--include-tree", + "target/oliphaunt-swift/release-tree", + "--push", + ]); + uploadGithubReleaseAssets(product, []); +} + function publishTypescriptNpmJsr(headRef) { const product = "oliphaunt-js"; const packageName = "@oliphaunt/ts"; @@ -845,6 +864,11 @@ if (publishProductStep?.product === "oliphaunt-react-native" && publishProductSt process.exit(0); } +if (publishProductStep?.product === "oliphaunt-swift" && publishProductStep.step === "github-release") { + publishSwiftGithubRelease(publishProductStep.headRef); + process.exit(0); +} + if (publishProductStep?.product === "oliphaunt-js" && publishProductStep.step === "npm-jsr") { publishTypescriptNpmJsr(publishProductStep.headRef); process.exit(0); diff --git a/tools/release/release-sdk-product-dry-run.mjs b/tools/release/release-sdk-product-dry-run.mjs index d9f93514..c5484b76 100644 --- a/tools/release/release-sdk-product-dry-run.mjs +++ b/tools/release/release-sdk-product-dry-run.mjs @@ -129,14 +129,16 @@ function stagedSwiftReleaseArtifacts() { return { manifest: manifests[0], releaseTree }; } -function prepareStagedSwiftReleaseManifest() { +export function prepareStagedSwiftReleaseManifest() { const { manifest, releaseTree: stagedReleaseTree } = stagedSwiftReleaseArtifacts(); const outputDir = path.join(ROOT, "target", "oliphaunt-swift"); const releaseTree = path.join(outputDir, "release-tree"); rmSync(releaseTree, { force: true, recursive: true }); mkdirSync(outputDir, { recursive: true }); cpSync(stagedReleaseTree, releaseTree, { recursive: true }); - cpSync(manifest, path.join(outputDir, "Package.swift.release")); + const outputManifest = path.join(outputDir, "Package.swift.release"); + cpSync(manifest, outputManifest); + return outputManifest; } function walkFiles(root) { diff --git a/tools/release/release.py b/tools/release/release.py index 6d0f7bd4..cca9c0e2 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -643,47 +643,6 @@ def require_staged_sdk_artifact(product: str, description: str, suffixes: tuple[ return matches -def staged_swift_release_artifacts() -> tuple[Path, Path, Path]: - matches = require_staged_sdk_artifact("oliphaunt-swift", "Swift package", (".zip", ".release")) - source_archives = [path for path in matches if path.name == "Oliphaunt-source.zip"] - manifests = [path for path in matches if path.name == "Package.swift.release"] - release_tree = sdk_artifact_dir("oliphaunt-swift") / "release-tree" - if len(source_archives) != 1 or len(manifests) != 1: - fail( - "oliphaunt-swift release requires exactly one staged Oliphaunt-source.zip " - "and one staged Package.swift.release under target/sdk-artifacts/oliphaunt-swift" - ) - if not (release_tree / "generated/swiftpm/OliphauntICU/OliphauntICU.swift").is_file(): - fail( - "oliphaunt-swift release requires staged SwiftPM release-tree files, including " - "generated/swiftpm/OliphauntICU/OliphauntICU.swift" - ) - manifest_text = manifests[0].read_text(encoding="utf-8") - required_fragments = [ - "binaryTarget(", - "liboliphaunt-native-v", - "liboliphaunt-", - "apple-spm-xcframework.zip", - "checksum:", - ] - for fragment in required_fragments: - if fragment not in manifest_text: - fail(f"oliphaunt-swift staged Package.swift.release is missing {fragment!r}") - return source_archives[0], manifests[0], release_tree - - -def prepare_staged_swift_release_manifest() -> Path: - _source_archive, staged_manifest, staged_release_tree = staged_swift_release_artifacts() - output_dir = ROOT / "target" / "oliphaunt-swift" - release_tree = output_dir / "release-tree" - shutil.rmtree(release_tree, ignore_errors=True) - output_dir.mkdir(parents=True, exist_ok=True) - shutil.copytree(staged_release_tree, release_tree) - output_manifest = output_dir / "Package.swift.release" - shutil.copy2(staged_manifest, output_manifest) - return output_manifest - - def sha256_file(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as handle: @@ -1809,11 +1768,6 @@ def run_broker_dry_run() -> None: broker_cargo_artifact_crates(version) -def run_swift_sdk_dry_run() -> None: - validate_staged_sdk_package("oliphaunt-swift") - prepare_staged_swift_release_manifest() - - def run_kotlin_sdk_dry_run() -> None: validate_staged_sdk_package("oliphaunt-kotlin") staged_kotlin_maven_repo() @@ -1842,8 +1796,6 @@ def run_product_publish_dry_runs(products: list[str], *, allow_dirty: bool, head run_broker_dry_run() elif product == "oliphaunt-node-direct": run_node_direct_dry_run() - elif product == "oliphaunt-swift": - run_swift_sdk_dry_run() elif product == "oliphaunt-kotlin": run_kotlin_sdk_dry_run() elif product == "oliphaunt-react-native": @@ -1864,25 +1816,6 @@ def publish_liboliphaunt_github_assets(head_ref: str) -> None: upload_github_release_assets("liboliphaunt-native", assets=assets) -def publish_swift_release(head_ref: str) -> None: - verify_release_tag("oliphaunt-swift", head_ref) - manifest = prepare_staged_swift_release_manifest() - run( - [ - "tools/dev/bun.sh", - "tools/release/publish_swiftpm_source_tag.mjs", - "--target", - head_ref, - "--manifest", - str(manifest.relative_to(ROOT)), - "--include-tree", - "target/oliphaunt-swift/release-tree", - "--push", - ] - ) - upload_github_release_assets("oliphaunt-swift") - - def kotlin_artifacts_published(version: str) -> bool: return all( url_exists(maven_pom_url(coordinate, version)) @@ -3145,8 +3078,6 @@ def command_publish_product_step(args: argparse.Namespace) -> None: publish_wasm_release_assets() elif product == "liboliphaunt-wasix" and step == "crates-io": publish_liboliphaunt_wasix_cargo_artifacts(head_ref) - elif product == "oliphaunt-swift" and step == "github-release": - publish_swift_release(head_ref) elif product == "oliphaunt-kotlin" and step == "maven-central": publish_kotlin_maven(head_ref) elif product == "oliphaunt-react-native" and step == "npm": From 625ce96f2f58c4ae01ba756b2c4a8f2208e01ae3 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 10:40:58 +0000 Subject: [PATCH 299/308] refactor: publish kotlin maven artifacts in bun --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 13 ++ tools/policy/check-tooling-stack.sh | 26 ++++ tools/release/check_release_metadata.py | 55 ++++++++- tools/release/release-publish.mjs | 31 +++++ tools/release/release-sdk-product-dry-run.mjs | 3 +- tools/release/release.py | 112 ------------------ 6 files changed, 123 insertions(+), 117 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index c7e311ed..fa6f4af0 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -3178,3 +3178,16 @@ until the current-state gates here are checked with fresh local evidence. `tools/dev/bun.sh tools/release/check_artifact_targets.mjs`, `bash tools/policy/check-docs.sh`, `tools/dev/bun.sh tools/release/check-consumer-shape.mjs`, and `git diff --check`. +- On 2026-06-28, the protected `oliphaunt-kotlin` Maven Central publish step + moved onto the Bun `release-publish.mjs` surface. The route verifies the + product tag, reuses the exported Bun `stagedKotlinMavenRepo()` helper to + validate CI-staged Maven repository artifacts, skips publication when the + shared registry checker already sees the configured Maven packages, runs the + Kotlin SDK and Android Gradle plugin `publishAndReleaseToMavenCentral` tasks, + verifies Maven Central visibility through the shared registry checker, and + preserves the empty GitHub release asset upload. The retired Python Kotlin + staged Maven repository, dry-run, idempotency, and publish helpers were + removed from `release.py`; policy checks now require Kotlin Maven publish + ownership in Bun and reject the old Python symbols. Fresh local evidence was + collected with syntax, metadata, policy, route, docs, and consumer-shape + checks in the working tree before committing this change. diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index c4784213..1d865b2f 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -539,6 +539,18 @@ grep -Fq 'prepareStagedSwiftReleaseManifest()' tools/release/release-publish.mjs fail "release-publish must validate and stage SwiftPM release artifacts through the Bun helper before tagging" grep -Fq 'tools/release/publish_swiftpm_source_tag.mjs' tools/release/release-publish.mjs || fail "release-publish must create/push the SwiftPM source tag through the Bun source-tag publisher" +grep -Fq 'publishKotlinMaven' tools/release/release-publish.mjs || + fail "release-publish must own Kotlin Maven Central publication in Bun" +grep -Fq 'stagedKotlinMavenRepo()' tools/release/release-publish.mjs || + fail "release-publish must validate staged Kotlin Maven artifacts before publication" +grep -Fq ':oliphaunt:publishAndReleaseToMavenCentral' tools/release/release-publish.mjs || + fail "release-publish must publish the Kotlin SDK Maven artifact through Gradle" +grep -Fq ':oliphaunt-android-gradle-plugin:publishAndReleaseToMavenCentral' tools/release/release-publish.mjs || + fail "release-publish must publish the Kotlin Android Gradle plugin Maven artifact through Gradle" +grep -Fq 'productRegistryPublished(product, "maven")' tools/release/release-publish.mjs || + fail "release-publish must skip Kotlin Maven publication when the registry checker already sees it" +grep -Fq 'publishProductStep?.product === "oliphaunt-kotlin" && publishProductStep.step === "maven-central"' tools/release/release-publish.mjs || + fail "release-publish must dispatch the Kotlin Maven Central publish step in Bun" grep -Fq 'publishTypescriptNpmJsr' tools/release/release-publish.mjs || fail "release-publish must own TypeScript npm/JSR publication in Bun" grep -Fq 'stagedJsrSourceDir(product)' tools/release/release-publish.mjs || @@ -681,6 +693,20 @@ if grep -Fq 'def staged_swift_release_artifacts(' tools/release/release.py; then fi grep -Fq 'stagedKotlinMavenRepo' tools/release/release-sdk-product-dry-run.mjs || fail "Bun SDK product dry-runs must preserve Kotlin staged Maven repository validation" +grep -Fq 'export function stagedKotlinMavenRepo()' tools/release/release-sdk-product-dry-run.mjs || + fail "Bun SDK product helper must export Kotlin staged Maven repository validation for publish" +if grep -Fq 'def publish_kotlin_maven(' tools/release/release.py; then + fail "release.py must not own Kotlin Maven publishing after the route moved to Bun" +fi +if grep -Fq 'def run_kotlin_sdk_dry_run(' tools/release/release.py; then + fail "release.py must not own Kotlin SDK product dry-runs after the route moved to Bun" +fi +if grep -Fq 'def kotlin_artifacts_published(' tools/release/release.py; then + fail "release.py must not retain Kotlin Maven idempotency probes after the route moved to Bun" +fi +if grep -Fq 'def staged_kotlin_maven_repo(' tools/release/release.py; then + fail "release.py must not own Kotlin staged Maven repository validation after the route moved to Bun" +fi grep -Fq 'stagedSdkNpmPackageTarball(product)' tools/release/release-sdk-product-dry-run.mjs || fail "Bun SDK product dry-runs must validate staged npm tarball identity and built output" grep -Fq 'verifyStagedCargoProductCrates("oliphaunt-rust")' tools/release/release-sdk-product-dry-run.mjs || diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index a7e32b4c..f5cb40b2 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1065,6 +1065,12 @@ def validate_publish_target_coverage() -> None: or "prepareStagedSwiftReleaseManifest()" not in release_publish or "tools/release/publish_swiftpm_source_tag.mjs" not in release_publish or 'publishProductStep?.product === "oliphaunt-swift" && publishProductStep.step === "github-release"' not in release_publish + or "publishKotlinMaven" not in release_publish + or "stagedKotlinMavenRepo()" not in release_publish + or ":oliphaunt:publishAndReleaseToMavenCentral" not in release_publish + or ":oliphaunt-android-gradle-plugin:publishAndReleaseToMavenCentral" not in release_publish + or 'productRegistryPublished(product, "maven")' not in release_publish + or 'publishProductStep?.product === "oliphaunt-kotlin" && publishProductStep.step === "maven-central"' not in release_publish or "publishTypescriptNpmJsr" not in release_publish or "stagedJsrSourceDir(product)" not in release_publish or 'productRegistryPublished(product, "jsr")' not in release_publish @@ -1117,15 +1123,20 @@ def validate_publish_target_coverage() -> None: or "prepareStagedSwiftReleaseManifest" not in release_sdk_product_dry_run or "export function prepareStagedSwiftReleaseManifest()" not in release_sdk_product_dry_run or "stagedKotlinMavenRepo" not in release_sdk_product_dry_run + or "export function stagedKotlinMavenRepo()" not in release_sdk_product_dry_run or "stagedSdkNpmPackageTarball(product)" not in release_sdk_product_dry_run or 'verifyStagedCargoProductCrates("oliphaunt-rust")' not in release_sdk_product_dry_run or "tools/release/prepare-rust-release-source.mjs" not in release_sdk_product_dry_run or "prepareOliphauntWasixReleaseSource" not in release_sdk_product_dry_run or "def publish_swift_release(" in release_source or "def staged_swift_release_artifacts(" in release_source + or "def publish_kotlin_maven(" in release_source + or "def run_kotlin_sdk_dry_run(" in release_source + or "def kotlin_artifacts_published(" in release_source + or "def staged_kotlin_maven_repo(" in release_source or 'spawnSync("tools/release/release.py", argv' not in release_publish ): - fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper and exact-extension GitHub asset publish steps must run in Bun, liboliphaunt-native and exact-extension Maven publication must run in Bun, liboliphaunt-native, broker, Node direct, Swift, and React Native npm/publication paths must run in Bun, native, Broker, WASIX, and Rust SDK Cargo artifact publication must run in Bun, and React Native SDK tasks must not track release.py directly") + fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper and exact-extension GitHub asset publish steps must run in Bun, liboliphaunt-native, exact-extension, and Kotlin Maven publication must run in Bun, liboliphaunt-native, broker, Node direct, Swift, Kotlin, TypeScript, and React Native npm/publication paths must run in Bun, native, Broker, WASIX, and Rust SDK Cargo artifact publication must run in Bun, and React Native SDK tasks must not track release.py directly") 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 @@ -1146,6 +1157,7 @@ def validate_publish_target_coverage() -> None: (product in {"oliphaunt-rust", "oliphaunt-wasix-rust"} and step == "crates-io") or (product == "oliphaunt-js" and step == "npm-jsr") or (product == "oliphaunt-swift" and step == "github-release") + or (product == "oliphaunt-kotlin" and step == "maven-central") ): if f'publishProductStep?.product === "{product}" && publishProductStep.step === "{step}"' not in release_publish: fail(f"Bun publish implementation must dispatch publish step {product}:{step}") @@ -1649,15 +1661,50 @@ def validate_kotlin(kotlin_version: str, liboliphaunt_version: str) -> None: "Kotlin Android Gradle packaging must keep backward-compatible capitalized Oliphaunt property lookup", ) require_text( - "tools/release/release.py", - 'registry_package_names("oliphaunt-kotlin", "maven")', - "Kotlin Maven release idempotency probes must derive package coordinates from release metadata", + "tools/release/release-publish.mjs", + "publishKotlinMaven", + "Kotlin Maven release publishing must run through the Bun release-publish entrypoint", + ) + require_text( + "tools/release/release-publish.mjs", + "stagedKotlinMavenRepo()", + "Kotlin Maven release publishing must validate the CI-staged Maven repository before publishing", + ) + require_text( + "tools/release/release-publish.mjs", + ":oliphaunt:publishAndReleaseToMavenCentral", + "Kotlin Maven release publishing must publish the SDK artifact through Gradle", + ) + require_text( + "tools/release/release-publish.mjs", + ":oliphaunt-android-gradle-plugin:publishAndReleaseToMavenCentral", + "Kotlin Maven release publishing must publish the Android Gradle plugin artifact through Gradle", + ) + require_text( + "tools/release/release-publish.mjs", + 'productRegistryPublished(product, "maven")', + "Kotlin Maven release idempotency probes must derive package coordinates from release metadata through the registry checker", + ) + require_text( + "tools/release/release-publish.mjs", + 'requireProductRegistryPublished(product, "maven")', + "Kotlin Maven release publishing must verify Maven Central visibility through the registry checker", + ) + require_text( + "tools/release/release-sdk-product-dry-run.mjs", + "export function stagedKotlinMavenRepo()", + "Kotlin staged Maven repository validation must be exported for dry-run and publish reuse", ) 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", ) + reject_text( + "tools/release/release-publish.mjs", + "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.mjs", 'registryPackageNames("liboliphaunt-native", "maven")', diff --git a/tools/release/release-publish.mjs b/tools/release/release-publish.mjs index 7543b27f..00c3f4ed 100755 --- a/tools/release/release-publish.mjs +++ b/tools/release/release-publish.mjs @@ -22,6 +22,7 @@ import { } from "./release-product-dry-run.mjs"; import { prepareStagedSwiftReleaseManifest, + stagedKotlinMavenRepo, stagedJsrSourceDir, stagedSdkNpmPackageTarball, verifyStagedCargoProductCrates, @@ -624,6 +625,31 @@ function publishSwiftGithubRelease(headRef) { uploadGithubReleaseAssets(product, []); } +function publishKotlinMaven(headRef) { + const product = "oliphaunt-kotlin"; + verifyReleaseTag(product, headRef); + stagedKotlinMavenRepo(); + const version = currentProductVersionSync(product, TOOL); + if (productRegistryPublished(product, "maven")) { + console.log(`dev.oliphaunt Android artifacts ${version} are already published on Maven Central; skipping publishAndReleaseToMavenCentral.`); + } else { + run(TOOL, [ + "src/sdks/kotlin/gradlew", + "-p", + "src/sdks/kotlin", + ":oliphaunt:publishAndReleaseToMavenCentral", + ":oliphaunt-android-gradle-plugin:publishAndReleaseToMavenCentral", + `-PoliphauntBuildRoot=${path.join(ROOT, "target/liboliphaunt-sdk-check/gradle/oliphaunt-kotlin-release")}`, + `-PoliphauntCxxBuildRoot=${path.join(ROOT, "target/liboliphaunt-sdk-check/cxx/oliphaunt-kotlin-release")}`, + "--project-cache-dir", + path.join(ROOT, "target/liboliphaunt-sdk-check/gradle-cache/oliphaunt-kotlin-release"), + "--configuration-cache", + ]); + } + requireProductRegistryPublished(product, "maven"); + uploadGithubReleaseAssets(product, []); +} + function publishTypescriptNpmJsr(headRef) { const product = "oliphaunt-js"; const packageName = "@oliphaunt/ts"; @@ -869,6 +895,11 @@ if (publishProductStep?.product === "oliphaunt-swift" && publishProductStep.step process.exit(0); } +if (publishProductStep?.product === "oliphaunt-kotlin" && publishProductStep.step === "maven-central") { + publishKotlinMaven(publishProductStep.headRef); + process.exit(0); +} + if (publishProductStep?.product === "oliphaunt-js" && publishProductStep.step === "npm-jsr") { publishTypescriptNpmJsr(publishProductStep.headRef); process.exit(0); diff --git a/tools/release/release-sdk-product-dry-run.mjs b/tools/release/release-sdk-product-dry-run.mjs index c5484b76..abafaa0f 100644 --- a/tools/release/release-sdk-product-dry-run.mjs +++ b/tools/release/release-sdk-product-dry-run.mjs @@ -154,7 +154,7 @@ function walkFiles(root) { return files; } -function stagedKotlinMavenRepo() { +export function stagedKotlinMavenRepo() { const root = path.join(sdkArtifactDir("oliphaunt-kotlin"), "maven"); requireDirectory( root, @@ -185,6 +185,7 @@ function stagedKotlinMavenRepo() { } } console.log(`validated staged Kotlin Maven repository: ${rel(root)}`); + return root; } function safeNpmPackageFilenamePrefix(packageName) { diff --git a/tools/release/release.py b/tools/release/release.py index cca9c0e2..a3990db5 100755 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -698,51 +698,6 @@ def staged_npm_package_tarball(product: str) -> Path | None: return matches[0] -def staged_kotlin_maven_repo() -> Path: - root = sdk_artifact_dir("oliphaunt-kotlin") / "maven" - if not root.is_dir(): - fail( - "oliphaunt-kotlin requires staged Maven repository artifacts under " - f"{root.relative_to(ROOT)}; download the CI workflow Kotlin SDK package artifacts " - "before release validation or publishing" - ) - version = current_product_version("oliphaunt-kotlin") - required = [ - root / f"dev/oliphaunt/oliphaunt-android/{version}/oliphaunt-android-{version}.aar", - root / f"dev/oliphaunt/oliphaunt-android/{version}/oliphaunt-android-{version}.pom", - root / f"dev/oliphaunt/oliphaunt-android/{version}/oliphaunt-android-{version}.module", - root / ( - f"dev/oliphaunt/oliphaunt-android-gradle-plugin/{version}/" - f"oliphaunt-android-gradle-plugin-{version}.jar" - ), - root / ( - f"dev/oliphaunt/oliphaunt-android-gradle-plugin/{version}/" - f"oliphaunt-android-gradle-plugin-{version}.pom" - ), - root / ( - f"dev/oliphaunt/oliphaunt-android-gradle-plugin/{version}/" - f"oliphaunt-android-gradle-plugin-{version}.module" - ), - root / ( - f"dev/oliphaunt/android/dev.oliphaunt.android.gradle.plugin/{version}/" - f"dev.oliphaunt.android.gradle.plugin-{version}.pom" - ), - ] - missing = [path.relative_to(ROOT) for path in required if not path.is_file()] - if missing: - fail("oliphaunt-kotlin staged Maven repository is missing: " + ", ".join(str(path) for path in missing)) - for path in root.rglob("*"): - if not path.is_file(): - continue - relative = path.relative_to(root) - if relative.parts[:2] != ("dev", "oliphaunt"): - fail(f"oliphaunt-kotlin staged Maven repository contains unexpected path {path.relative_to(ROOT)}") - if path.suffix in {".lastUpdated", ".lock"}: - fail(f"oliphaunt-kotlin staged Maven repository contains local resolver state {path.relative_to(ROOT)}") - print(f"validated staged Kotlin Maven repository: {root.relative_to(ROOT)}") - return root - - def json_contains_workspace_protocol(value: object) -> bool: if isinstance(value, str): return value.startswith("workspace:") @@ -944,10 +899,6 @@ def validate_no_consumer_install_scripts(package: dict, label: str) -> None: fail(f"{label} must not declare consumer install lifecycle scripts: {', '.join(forbidden)}") -def url_exists(url: str) -> bool: - return succeeds(["curl", "-fsIL", "--retry", "3", "--connect-timeout", "10", url]) - - def git_commit(ref: str) -> str | None: result = subprocess.run( ["git", "rev-list", "-n", "1", ref], @@ -1043,17 +994,6 @@ def cargo_registry_packages(product: str) -> list[str]: return sorted(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" - ) - - def wasix_release_asset_dir() -> Path: return ROOT / "target/oliphaunt-wasix/release-assets" @@ -1768,11 +1708,6 @@ def run_broker_dry_run() -> None: broker_cargo_artifact_crates(version) -def run_kotlin_sdk_dry_run() -> None: - validate_staged_sdk_package("oliphaunt-kotlin") - staged_kotlin_maven_repo() - - def run_react_native_sdk_dry_run() -> None: validate_staged_sdk_package("oliphaunt-react-native") require_staged_sdk_artifact("oliphaunt-react-native", "npm package", (".tgz",)) @@ -1796,8 +1731,6 @@ def run_product_publish_dry_runs(products: list[str], *, allow_dirty: bool, head run_broker_dry_run() elif product == "oliphaunt-node-direct": run_node_direct_dry_run() - elif product == "oliphaunt-kotlin": - run_kotlin_sdk_dry_run() elif product == "oliphaunt-react-native": run_react_native_sdk_dry_run() elif is_extension_product(product): @@ -1816,49 +1749,6 @@ def publish_liboliphaunt_github_assets(head_ref: str) -> None: upload_github_release_assets("liboliphaunt-native", assets=assets) -def kotlin_artifacts_published(version: str) -> bool: - return all( - url_exists(maven_pom_url(coordinate, version)) - for coordinate in registry_package_names("oliphaunt-kotlin", "maven") - ) - - -def publish_kotlin_maven(head_ref: str) -> None: - verify_release_tag("oliphaunt-kotlin", head_ref) - staged_kotlin_maven_repo() - version = current_product_version("oliphaunt-kotlin") - if kotlin_artifacts_published(version): - print(f"dev.oliphaunt Android artifacts {version} are already published on Maven Central; skipping publishAndReleaseToMavenCentral.") - else: - run( - [ - "src/sdks/kotlin/gradlew", - "-p", - "src/sdks/kotlin", - ":oliphaunt:publishAndReleaseToMavenCentral", - ":oliphaunt-android-gradle-plugin:publishAndReleaseToMavenCentral", - f"-PoliphauntBuildRoot={ROOT / 'target/liboliphaunt-sdk-check/gradle/oliphaunt-kotlin-release'}", - f"-PoliphauntCxxBuildRoot={ROOT / 'target/liboliphaunt-sdk-check/cxx/oliphaunt-kotlin-release'}", - "--project-cache-dir", - str(ROOT / "target/liboliphaunt-sdk-check/gradle-cache/oliphaunt-kotlin-release"), - "--configuration-cache", - ] - ) - run( - [ - *REGISTRY_PUBLICATION_CHECK, - "--product", - "oliphaunt-kotlin", - "--require-published", - "--retries", - "12", - "--retry-delay", - "10", - ] - ) - upload_github_release_assets("oliphaunt-kotlin") - - def publish_liboliphaunt_runtime_maven(head_ref: str) -> None: verify_release_tag("liboliphaunt-native", head_ref) ensure_liboliphaunt_release_assets() @@ -3078,8 +2968,6 @@ def command_publish_product_step(args: argparse.Namespace) -> None: publish_wasm_release_assets() elif product == "liboliphaunt-wasix" and step == "crates-io": publish_liboliphaunt_wasix_cargo_artifacts(head_ref) - elif product == "oliphaunt-kotlin" and step == "maven-central": - publish_kotlin_maven(head_ref) elif product == "oliphaunt-react-native" and step == "npm": publish_react_native_npm(head_ref) elif product == "oliphaunt-broker" and step == "github-release-assets": From 22e6fd98b97e64f79be63fd9dfacceab9e184cf1 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 10:52:02 +0000 Subject: [PATCH 300/308] refactor: remove release publish python fallback --- .../EXAMPLE_RELEASE_VALIDATION_TASKS.md | 20 ++++++++ tools/policy/check-release-policy.mjs | 20 ++++++-- tools/policy/check-tooling-stack.sh | 7 +++ tools/release/check_release_metadata.py | 7 +-- tools/release/release-publish.mjs | 50 +++++++++++++------ 5 files changed, 80 insertions(+), 24 deletions(-) diff --git a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md index fa6f4af0..1456e382 100644 --- a/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md +++ b/docs/internal/EXAMPLE_RELEASE_VALIDATION_TASKS.md @@ -3191,3 +3191,23 @@ until the current-state gates here are checked with fresh local evidence. ownership in Bun and reject the old Python symbols. Fresh local evidence was collected with syntax, metadata, policy, route, docs, and consumer-shape checks in the working tree before committing this change. +- On 2026-06-28, the public `release-publish.mjs publish` wrapper stopped + using a generic `release.py` fallback. No-product publish validation now runs + in Bun: selected products validate publish credentials through + `check_publish_environment.mjs`, run the shared release/registry checks, and + execute the same Bun product dry-run plan used by `publish-dry-run`; an empty + selection runs release checks and skips package publishing explicitly. Policy + and release metadata checks now reject reintroducing + `spawnSync("tools/release/release.py", argv)` in the public wrapper. Fresh + local evidence passed for `node --check tools/release/release-publish.mjs`, + `node --check tools/policy/check-release-policy.mjs`, + `PYTHONPYCACHEPREFIX=target/python-pycache python3 -m py_compile + tools/release/check_release_metadata.py`, no-match fallback searches in + `release-publish.mjs`, `tools/dev/bun.sh tools/release/release-publish.mjs + publish --product oliphaunt-kotlin` failing inside the Bun wrapper as an + unsupported publish shape, `tools/dev/bun.sh + tools/release/release-publish.mjs publish --products-json '["oliphaunt-js"]' + --head-ref HEAD` failing at the Bun publish-environment check on the local + non-OIDC environment, and `tools/dev/bun.sh + tools/release/release-publish.mjs publish` passing the full release-check + stack before reporting that no release products were selected. diff --git a/tools/policy/check-release-policy.mjs b/tools/policy/check-release-policy.mjs index c6cc393c..e9b5f150 100644 --- a/tools/policy/check-release-policy.mjs +++ b/tools/policy/check-release-policy.mjs @@ -1231,6 +1231,8 @@ function checkReleaseWorkflowPolicy() { } const releaseScript = readText("tools/release/release.py"); + const releasePublishScript = readText("tools/release/release-publish.mjs"); + const releaseSdkProductDryRunScript = readText("tools/release/release-sdk-product-dry-run.mjs"); assertDirectReleasePythonToolsAreExecutable(releaseScript); for (const forbidden of [ "validate_wasix_runtime_inputs", @@ -1272,12 +1274,20 @@ function checkReleaseWorkflowPolicy() { } for (const snippet of [ '["pnpm", "exec", "jsr", "publish", "--dry-run"]', - 'command.append("--allow-dirty")', - "run(command, cwd=jsr_source)", - '"--product",\n "oliphaunt-node-direct",\n "--require-published"', + 'command.push("--allow-dirty")', + "run(TOOL, command, { cwd: stagedJsrSourceDir(product) });", ]) { - if (!releaseScript.includes(snippet)) { - fail(`release dry-runs and package publishes must cover registry-native checks: missing ${JSON.stringify(snippet)}`); + if (!releaseSdkProductDryRunScript.includes(snippet)) { + fail(`release dry-runs must cover TypeScript JSR registry-native checks in Bun: missing ${JSON.stringify(snippet)}`); + } + } + for (const snippet of [ + "publishNodeDirectNpmOptionalPackages", + "nodeDirectOptionalNpmTarballs(version)", + "requireProductRegistryPublished(product, null)", + ]) { + if (!releasePublishScript.includes(snippet)) { + fail(`release package publishes must cover Node direct registry-native checks in Bun: missing ${JSON.stringify(snippet)}`); } } diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index 1d865b2f..a6fa273f 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -453,6 +453,10 @@ grep -Fq 'run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check-registries fail "release publish dry-run wrapper must keep no-product passthrough registry checks in Bun" grep -Fq 'SUPPORTED_BUN_PRODUCT_DRY_RUNS' tools/release/release-publish.mjs || fail "release publish dry-run wrapper must import the Bun product dry-run support set" +grep -Fq 'async function publishNoProduct(' tools/release/release-publish.mjs || + fail "release publish wrapper must own the no-product publish validation path in Bun" +grep -Fq 'run(TOOL, ["tools/release/check_publish_environment.mjs", "--products-json", productsJson]);' tools/release/release-publish.mjs || + fail "release publish wrapper must validate publish credentials through the Bun publish-environment helper" grep -Fq 'await runBunProductDryRun(product, { allowDirty: productDryRunPlan.allowDirty });' tools/release/release-publish.mjs || fail "release publish dry-run wrapper must execute supported product dry-runs in Bun" grep -Fq 'function legacyWasmPublishDryRunPlan(' tools/release/release-publish.mjs || @@ -469,6 +473,9 @@ if grep -Fq 'Other product dry-runs' tools/release/release-publish.mjs; then fi grep -Fq 'publish-dry-run is Bun-owned' tools/release/release-publish.mjs || fail "release-publish must fail closed before release.py fallback for unsupported publish-dry-run arguments" +if grep -Fq 'spawnSync("tools/release/release.py", argv' tools/release/release-publish.mjs; then + fail "release-publish must not retain a generic release.py publish fallback after all publish routes moved to Bun" +fi grep -Fq 'GITHUB_RELEASE_ASSET_PUBLISHERS' tools/release/release-publish.mjs || fail "release-publish must route staged runtime/helper GitHub asset publish steps in Bun" grep -Fq 'publishGithubReleaseAssets' tools/release/release-publish.mjs || diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index f5cb40b2..f3524468 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -1022,6 +1022,9 @@ def validate_publish_target_coverage() -> None: or 'run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check.mjs"]);' not in release_publish or 'run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check-registries.mjs", ...passthrough]);' not in release_publish or "SUPPORTED_BUN_PRODUCT_DRY_RUNS" not in release_publish + or "async function publishNoProduct(" not in release_publish + or 'run(TOOL, ["tools/release/check_publish_environment.mjs", "--products-json", productsJson]);' not in release_publish + or "publish environment and dry-run checks passed" not in release_publish or 'await runBunProductDryRun(product, { allowDirty: productDryRunPlan.allowDirty });' not in release_publish or "function legacyWasmPublishDryRunPlan(" not in release_publish or 'LEGACY_WASM_DRY_RUN_PRODUCT = "oliphaunt-wasix-rust"' not in release_publish @@ -1134,11 +1137,9 @@ def validate_publish_target_coverage() -> None: or "def run_kotlin_sdk_dry_run(" in release_source or "def kotlin_artifacts_published(" in release_source or "def staged_kotlin_maven_repo(" in release_source - or 'spawnSync("tools/release/release.py", argv' not in release_publish + or 'spawnSync("tools/release/release.py", argv' in release_publish ): fail("Release workflow publish commands must use the Bun release-publish entrypoint, no-product, product, and legacy --wasm publish dry-runs must run through Bun without launching release.py, staged runtime/helper and exact-extension GitHub asset publish steps must run in Bun, liboliphaunt-native, exact-extension, and Kotlin Maven publication must run in Bun, liboliphaunt-native, broker, Node direct, Swift, Kotlin, TypeScript, and React Native npm/publication paths must run in Bun, native, Broker, WASIX, and Rust SDK Cargo artifact publication must run in Bun, and React Native SDK tasks must not track release.py directly") - 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 graph_products().items(): declared = set(string_list(config, "publish_targets", product)) diff --git a/tools/release/release-publish.mjs b/tools/release/release-publish.mjs index 00c3f4ed..f609f594 100755 --- a/tools/release/release-publish.mjs +++ b/tools/release/release-publish.mjs @@ -49,8 +49,8 @@ function usage() { Runs protected release publish and publish dry-run operations through the Bun release command surface. The public no-product publish dry-run and product dry-runs are handled in Bun, including the legacy --wasm shortcut for the WASIX -Rust SDK dry-run. Protected publish steps that have not yet moved to Bun still -delegate to release.py while the remaining implementation is ported. +Rust SDK dry-run. Protected publish steps and no-product publish validation are +handled in Bun. `); } @@ -793,6 +793,33 @@ function productPublishDryRunPlan(args) { }; } +async function runProductDryRunPlan(productDryRunPlan) { + run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check.mjs"]); + run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check-registries.mjs", ...productDryRunPlan.passthrough]); + for (const product of productDryRunPlan.products) { + await runBunProductDryRun(product, { allowDirty: productDryRunPlan.allowDirty }); + } +} + +async function publishNoProduct(args) { + const productsJson = flagValue(args, "--products-json"); + const productDryRunPlan = productPublishDryRunPlan(args); + if (productsJson !== null) { + run(TOOL, ["tools/release/check_publish_environment.mjs", "--products-json", productsJson]); + } + if (productDryRunPlan !== null) { + await runProductDryRunPlan(productDryRunPlan); + console.log("publish environment and dry-run checks passed; package-native publish steps run in the Release workflow"); + return; + } + run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check.mjs"]); + const passthrough = args.filter((arg) => arg !== "--allow-dirty"); + if (passthrough.length > 0) { + run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check-registries.mjs", ...passthrough]); + } + console.log("No release products selected; publish environment and package publish steps skipped."); +} + if (isNoProductPublishDryRun(command, argv.slice(1))) { const passthrough = noProductPublishDryRunPassthrough(argv.slice(1)); run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check.mjs"]); @@ -804,11 +831,7 @@ if (isNoProductPublishDryRun(command, argv.slice(1))) { const productDryRunPlan = command === "publish-dry-run" ? productPublishDryRunPlan(argv.slice(1)) : null; if (productDryRunPlan !== null) { - run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check.mjs"]); - run(TOOL, ["tools/dev/bun.sh", "tools/release/release-check-registries.mjs", ...productDryRunPlan.passthrough]); - for (const product of productDryRunPlan.products) { - await runBunProductDryRun(product, { allowDirty: productDryRunPlan.allowDirty }); - } + await runProductDryRunPlan(productDryRunPlan); process.exit(0); } @@ -931,14 +954,9 @@ if (command === "publish" && flagValue(argv.slice(1), "--step") === "maven-centr } } -const result = spawnSync("tools/release/release.py", argv, { - cwd: ROOT, - stdio: "inherit", -}); - -if (result.error !== undefined) { - console.error(`${TOOL}: ${result.error.message}`); - process.exit(1); +if (command === "publish" && publishProductStep === null && flagValue(argv.slice(1), "--product") === null && flagValue(argv.slice(1), "--step") === null) { + await publishNoProduct(argv.slice(1)); + process.exit(0); } -process.exit(result.status ?? 1); +fail(`unsupported publish arguments: ${argv.slice(1).join(" ") || ""}`); From f25e399771f263517b058e683fefc73c6e457ad5 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Sun, 28 Jun 2026 11:15:41 +0000 Subject: [PATCH 301/308] refactor: retire python release publish entrypoint --- .../final-product-source-architecture.md | 6 +- docs/maintainers/repo-structure.md | 5 +- docs/maintainers/tooling.md | 14 +- tools/policy/check-tooling-stack.sh | 7 +- tools/policy/python-entrypoints.allowlist | 2 +- tools/release/check_artifact_targets.mjs | 12 +- tools/release/check_consumer_shape.py | 64 +- tools/release/check_release_metadata.py | 38 +- tools/release/release.py | 763 +----------------- 9 files changed, 98 insertions(+), 813 deletions(-) mode change 100755 => 100644 tools/release/release.py diff --git a/docs/architecture/final-product-source-architecture.md b/docs/architecture/final-product-source-architecture.md index 43c3ebcc..23b44fdd 100644 --- a/docs/architecture/final-product-source-architecture.md +++ b/docs/architecture/final-product-source-architecture.md @@ -14,8 +14,10 @@ Oliphaunt uses one source graph and one release identity system: does not model: owner, kind, publish targets, registry coordinates, release artifacts, and compatibility-version files. - Product-local `targets/*.toml` files own platform artifact metadata. -- `tools/release/release.py` owns protected publishing, checksums, - attestations, registry checks, and artifact verification. +- Bun entrypoints under `tools/release/*.mjs` own release checks, dry-runs, + publication routing, checksums, attestations, registry checks, and artifact + verification. `tools/release/release.py` is a legacy helper module behind + those entrypoints while the remaining Python validation helpers are retired. There is no separate release graph, release-input graph, CI jobs graph, or consumer lockfile. If a relationship affects source, task execution, or release diff --git a/docs/maintainers/repo-structure.md b/docs/maintainers/repo-structure.md index 0cb9917f..e3283313 100644 --- a/docs/maintainers/repo-structure.md +++ b/docs/maintainers/repo-structure.md @@ -222,8 +222,9 @@ gap must be represented as an explicit unsupported error and justified in workflow aliases; run product and repo work through Moon targets directly. - Release-please owns product versions, changelogs, release PRs, and product-scoped tags. Bun release entrypoints under `tools/release/*.mjs` own - the public check, dry-run, and publish command surface; `release.py` remains a - protected implementation detail only while publish dispatch is being ported. + the public and protected check, dry-run, and publish command surface. + `release.py` is a legacy helper module behind explicit Bun bridges while the + remaining Python validation and artifact helpers are retired. - Cargo publishing runs through `tools/dev/bun.sh tools/release/release-publish.mjs publish` and `cargo publish` from the protected Release workflow. Do not add a Rust-only release orchestrator beside diff --git a/docs/maintainers/tooling.md b/docs/maintainers/tooling.md index bf9df2bd..d4e2386e 100644 --- a/docs/maintainers/tooling.md +++ b/docs/maintainers/tooling.md @@ -15,11 +15,10 @@ predictable without hiding ecosystem-native behavior. - Product-local `targets/*.toml` files own platform artifact metadata. - Product-native build tools own product behavior: Cargo, SwiftPM/Xcode, Gradle, npm/JSR, Expo, React Native Codegen, and PostgreSQL build scripts. -- Bun release entrypoints under `tools/release/*.mjs` own the public release - check, dry-run, and publish command surface. `tools/release/release.py` - remains the protected implementation detail behind publish dispatch while - registry publishing, checksums, attestations, and GitHub release asset - verification are being ported. +- Bun release entrypoints under `tools/release/*.mjs` own the public and + protected release check, dry-run, and publish command surface. Remaining + Python files under `tools/release/` are legacy validator or artifact-helper + implementations invoked through those Bun entrypoints while they are retired. Do not add a second source graph, release graph, or root alias layer over Moon. Do not add a repo-wide tool because it is popular in one language ecosystem. @@ -175,8 +174,9 @@ What release-please does not own: - verifying already-published GitHub release assets. Those stay behind the Bun release entrypoints and product-native release tasks. -Until publish dispatch is fully ported, the Bun publish entrypoint may delegate -protected implementation work to `tools/release/release.py`. +The public publish path no longer delegates to `tools/release/release.py`; +remaining Python release helpers are called only through explicit Bun-owned +validation or artifact-helper bridges. Do not reintroduce release-plz, git-cliff product changelog ownership, a central release graph, or broad clean-registry reinstall gates as routine CI policy. diff --git a/tools/policy/check-tooling-stack.sh b/tools/policy/check-tooling-stack.sh index a6fa273f..ba9769d6 100755 --- a/tools/policy/check-tooling-stack.sh +++ b/tools/policy/check-tooling-stack.sh @@ -407,16 +407,21 @@ for retired_release_command in \ 'def command_check_registries(' \ 'def command_consumer_shape(' \ 'def command_verify_release(' \ + 'def command_publish(' \ + 'def command_publish_dry_run(' \ + 'def command_publish_product_step(' \ 'command == "check"' \ 'command == "check-registries"' \ 'command == "consumer-shape"' \ 'command == "verify-release"' \ + 'subparsers.add_parser("publish")' \ + 'subparsers.add_parser("publish-dry-run")' \ '"check-registries",' \ '"consumer-shape",' \ '"verify-release",' do if grep -Fq "$retired_release_command" tools/release/release.py; then - fail "release.py must not retain non-publish release check command surface: $retired_release_command" + fail "release.py must not retain public release command surface after it moved to Bun: $retired_release_command" fi done grep -Fq 'tools/release/check-release-metadata.mjs' tools/release/release-check.mjs || diff --git a/tools/policy/python-entrypoints.allowlist b/tools/policy/python-entrypoints.allowlist index 655f5bd1..91545b14 100644 --- a/tools/policy/python-entrypoints.allowlist +++ b/tools/policy/python-entrypoints.allowlist @@ -4,4 +4,4 @@ src/extensions/tools/check-extension-model.py extensions defer-extension-model-port generates and validates multi-language extension catalog, SDK metadata, docs, and evidence from one model tools/release/check_consumer_shape.py release-consumer-shape defer-release-graph-port implementation behind the Bun consumer-shape entrypoint validating cross-SDK package/runtime/install shape tools/release/check_release_metadata.py release-metadata defer-release-graph-port implementation behind the Bun release-metadata entrypoint validating release metadata and publish-step wiring -tools/release/release.py release-orchestrator defer-release-graph-port protected release implementation behind Bun check/verify/publish entrypoints during release-graph port +tools/release/release.py release-helpers defer-release-graph-port legacy release validation and artifact helper module retained while remaining Python helpers move behind Bun entrypoints diff --git a/tools/release/check_artifact_targets.mjs b/tools/release/check_artifact_targets.mjs index f861bf19..e0a88c9b 100644 --- a/tools/release/check_artifact_targets.mjs +++ b/tools/release/check_artifact_targets.mjs @@ -708,9 +708,9 @@ function validateCiReleaseArtifacts() { "release workflow must download Node direct optional npm package artifacts from CI", ); requireText( - "tools/release/release.py", - "node_direct_optional_npm_tarballs", - "Node direct protected npm publish must validate staged optional npm tarballs", + "tools/release/release-publish.mjs", + "nodeDirectOptionalNpmTarballs", + "Node direct protected npm publish must validate staged optional npm tarballs through Bun", ); requireText( "tools/release/release-product-dry-run.mjs", @@ -733,9 +733,9 @@ function validateCiReleaseArtifacts() { "Exact-extension product dry-runs must run Maven Local publication in Bun", ); requireText( - "tools/release/release.py", - 'run(["npm", "publish", str(tarball), "--access", "public", "--provenance"])', - "Node direct optional npm publish must publish CI-built tarballs directly", + "tools/release/release-publish.mjs", + "npmPublishTarball(packageName, tarball, version)", + "Node direct optional npm publish must publish CI-built tarballs directly through Bun", ); for (const projectId of sdkPackageProducts()) { const moonFile = projectId === "oliphaunt-wasix-rust" diff --git a/tools/release/check_consumer_shape.py b/tools/release/check_consumer_shape.py index de43708b..7fb6ea2d 100755 --- a/tools/release/check_consumer_shape.py +++ b/tools/release/check_consumer_shape.py @@ -878,6 +878,8 @@ def check_liboliphaunt(findings: list[Finding]) -> None: native_macos_packager = read_text("tools/release/package-liboliphaunt-macos-assets.sh") native_windows_packager = read_text("tools/release/package-liboliphaunt-windows-assets.ps1") release_cli = read_text("tools/release/release.py") + release_publish = read_text("tools/release/release-publish.mjs") + release_product_dry_run = read_text("tools/release/release-product-dry-run.mjs") local_registry_publisher = read_text("tools/release/local-registry-publish.mjs") oliphaunt_build_source = read_text("src/sdks/rust/crates/oliphaunt-build/src/lib.rs") native_runtime_package_split_failures = native_npm_tool_split_failures( @@ -1000,19 +1002,30 @@ def check_liboliphaunt(findings: list[Finding]) -> None: "tools/release/release.py", severity="P0", ) - for required in [ - "package-liboliphaunt-cargo-artifacts.mjs", - "publish_liboliphaunt_cargo_artifacts", - "liboliphaunt_cargo_artifact_crates", - "liboliphaunt_cargo_package_name", + for required, source, label in [ + ( + "package-liboliphaunt-cargo-artifacts.mjs", + release_product_dry_run, + "tools/release/release-product-dry-run.mjs", + ), + ( + "publishLiboliphauntNativeCargoArtifacts", + release_publish, + "tools/release/release-publish.mjs", + ), + ( + "liboliphauntNativeCargoArtifactPackages", + release_product_dry_run, + "tools/release/release-product-dry-run.mjs", + ), ]: require( findings, product, "liboliphaunt-rust-artifact-crates", - required in release_cli, + required in source, "liboliphaunt native Rust consumers must resolve release assets from Cargo artifact crates.", - f"tools/release/release.py missing {required}", + f"{label} missing {required}", severity="P0", ) require( @@ -1683,7 +1696,8 @@ def check_kotlin(findings: list[Finding]) -> None: 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_publish = read_text("tools/release/release-publish.mjs") + release_product_dry_run = read_text("tools/release/release-product-dry-run.mjs") release_workflow = read_text(".github/workflows/release.yml") for required in [ "include(\":oliphaunt-maven-artifacts\")", @@ -1701,24 +1715,40 @@ def check_kotlin(findings: list[Finding]) -> None: f"missing {required}", severity="P0", ) - for required in [ - "build_maven_artifact_manifest.mjs", - "publish_liboliphaunt_runtime_maven", - "publish_selected_extension_maven", - ":oliphaunt-maven-artifacts:publishAndReleaseToMavenCentral", + for required, source, label in [ + ( + "build_maven_artifact_manifest.mjs", + release_product_dry_run, + "tools/release/release-product-dry-run.mjs", + ), + ( + "publishLiboliphauntRuntimeMaven", + release_publish, + "tools/release/release-publish.mjs", + ), + ( + "publishSelectedExtensionMaven", + release_publish, + "tools/release/release-publish.mjs", + ), + ( + ":oliphaunt-maven-artifacts:publishAndReleaseToMavenCentral", + release_publish, + "tools/release/release-publish.mjs", + ), ]: require( findings, product, "android-maven-release-hooks", - required in release_cli, + required in source, "Release CLI must publish Android runtime and exact-extension artifacts to Maven Central.", - f"tools/release/release.py missing {required}", + f"{label} missing {required}", severity="P0", ) maven_artifact_release_helper = "" - if "def run_maven_artifact_publisher(" in release_cli: - maven_artifact_release_helper = release_cli.split("def run_maven_artifact_publisher(", 1)[1].split("\ndef ", 1)[0] + if "export function runMavenArtifactPublisher(" in release_product_dry_run: + maven_artifact_release_helper = release_product_dry_run.split("export function runMavenArtifactPublisher(", 1)[1].split("\nexport function ", 1)[0] require( findings, product, diff --git a/tools/release/check_release_metadata.py b/tools/release/check_release_metadata.py index f3524468..664a34fb 100755 --- a/tools/release/check_release_metadata.py +++ b/tools/release/check_release_metadata.py @@ -725,8 +725,11 @@ def validate_graph_files() -> None: examples_readme = read_text("examples/README.md") examples_local_registries = read_text("examples/tools/with-local-registries.sh") if ( - '"tools/release/release-check.mjs"' not in release_source - or '"tools/release/release-check-registries.mjs", *passthrough' not in release_source + "def command_publish(" in release_source + or "def command_publish_dry_run(" in release_source + or "def command_publish_product_step(" in release_source + or 'subparsers.add_parser("publish")' in release_source + or 'subparsers.add_parser("publish-dry-run")' in release_source or "def command_check(" in release_source or "def command_check_registries(" in release_source or "def command_consumer_shape(" in release_source @@ -762,7 +765,7 @@ def validate_graph_files() -> None: or 'command: "tools/release/release.py check"' in root_moon or 'command: "tools/release/check_release_metadata.py"' in root_moon ): - fail("active release check, registry-check, verify, and consumer-shape orchestration must live in Bun helpers; release.py must keep only the protected publish and publish-dry-run implementation") + fail("active release check, registry-check, verify, consumer-shape, publish, and publish-dry-run orchestration must live in Bun helpers; release.py must not expose a public release command parser") if ( "tools/dev/bun.sh tools/release/prepare-rust-release-source.mjs" not in rust_sdk_check or '"prepare-rust-release-source"' in release_source @@ -1154,22 +1157,27 @@ def validate_publish_target_coverage() -> None: saw_extension = True continue for step in step_coverage: - if ( - (product in {"oliphaunt-rust", "oliphaunt-wasix-rust"} and step == "crates-io") - or (product == "oliphaunt-js" and step == "npm-jsr") - or (product == "oliphaunt-swift" and step == "github-release") - or (product == "oliphaunt-kotlin" and step == "maven-central") - ): - if f'publishProductStep?.product === "{product}" && publishProductStep.step === "{step}"' not in release_publish: - fail(f"Bun publish implementation must dispatch publish step {product}:{step}") - elif f'product == "{product}" and step == "{step}"' not in release_source: - fail(f"release.py must dispatch publish step {product}:{step}") + if step == "github-release-assets": + if "GITHUB_RELEASE_ASSET_PUBLISHERS" not in release_publish or f'"{product}"' not in release_publish: + fail(f"Bun publish implementation must dispatch GitHub release assets for {product}") + elif f'publishProductStep?.product === "{product}" && publishProductStep.step === "{step}"' not in release_publish: + fail(f"Bun publish implementation 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 step == "github-release-assets": + if ( + "EXTENSION_PRODUCTS.has(publishProductStep.product)" not in release_publish + or "publishExtensionGithubReleaseAssets" not in release_publish + or "publishSelectedExtensionGithubReleaseAssets" not in release_publish + ): + fail("Bun publish implementation must dispatch exact-extension GitHub release assets") + elif ( + 'publishProductStep?.step === "maven-central" && EXTENSION_PRODUCTS.has(publishProductStep.product)' not in release_publish + or "publishSelectedExtensionMaven" not in release_publish + ): + fail("Bun publish implementation must dispatch exact-extension Maven artifacts") if f"--step {step} --products-json" not in workflow: fail(f"Release workflow must invoke aggregate extension publish step {step}") diff --git a/tools/release/release.py b/tools/release/release.py old mode 100755 new mode 100644 index a3990db5..42948426 --- a/tools/release/release.py +++ b/tools/release/release.py @@ -1,9 +1,7 @@ -#!/usr/bin/env python3 -"""Single public release CLI for Oliphaunt product releases.""" +"""Legacy release validation helpers retained behind Bun release entrypoints.""" from __future__ import annotations -import argparse import hashlib import json import os @@ -12,7 +10,6 @@ import subprocess import sys import tarfile -import time import zipfile from dataclasses import dataclass from functools import lru_cache @@ -554,11 +551,6 @@ def output(args: list[str], *, cwd: Path = ROOT) -> str: return subprocess.check_output(args, cwd=cwd, text=True).strip() -def succeeds(args: list[str], *, cwd: Path = ROOT) -> bool: - result = subprocess.run(args, cwd=cwd, text=True, capture_output=True, check=False) - return result.returncode == 0 - - def registry_check_args(*args: str) -> list[str]: return [*REGISTRY_PUBLICATION_CHECK, *args] @@ -578,20 +570,6 @@ def cratesio_product_crates(product: str) -> list[str]: 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.""" @@ -623,26 +601,6 @@ def pnpm_pack_for_npm_publish(package_dir: Path) -> Path: return tarball -def sdk_artifact_dir(product: str) -> Path: - return ROOT / "target" / "sdk-artifacts" / product - - -def require_staged_sdk_artifact(product: str, description: str, suffixes: tuple[str, ...]) -> list[Path]: - directory = sdk_artifact_dir(product) - matches = sorted( - path - for path in directory.glob("*") - if path.is_file() and path.name != "artifacts.txt" and path.suffix in suffixes - ) - if not matches: - fail( - f"{product} requires staged {description} artifact(s) under " - f"{directory.relative_to(ROOT)}; download the CI workflow SDK package artifacts " - "before release validation or publishing" - ) - return matches - - def sha256_file(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as handle: @@ -651,112 +609,6 @@ def sha256_file(path: Path) -> str: return digest.hexdigest() -def staged_cargo_crates(product: str) -> list[Path]: - matches = require_staged_sdk_artifact(product, "Cargo package", (".crate",)) - names = [path.name for path in matches] - if len(names) != len(set(names)): - fail(f"{product} staged Cargo artifacts contain duplicate crate filenames: {names}") - return matches - - -def verify_staged_cargo_crate_identity( - product: str, - package: str, - version: str, - *, - allow_dirty: bool, -) -> None: - expected_name = f"{package}-{version}.crate" - matches = [path for path in staged_cargo_crates(product) if path.name == expected_name] - if len(matches) != 1: - staged_names = sorted(path.name for path in staged_cargo_crates(product)) - fail( - f"{product} staged Cargo artifacts must contain exactly one {expected_name}; " - f"staged={staged_names}" - ) - staged = matches[0] - print(f"validated staged Cargo crate identity: {product} -> {staged.relative_to(ROOT)}") - - -def verify_staged_cargo_product_crates(product: str, version: str, *, allow_dirty: bool) -> None: - 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)) - expected_names = sorted(f"{crate}-{version}.crate" for crate in crates) - if staged_names != expected_names: - fail(f"{product} staged Cargo artifacts mismatch: expected={expected_names}, staged={staged_names}") - - -def staged_npm_package_tarball(product: str) -> Path | None: - matches = require_staged_sdk_artifact(product, "npm package", (".tgz",)) - if not matches: - return None - if len(matches) != 1: - fail(f"{product} staged npm package artifacts must contain exactly one .tgz, got {len(matches)}") - validate_staged_npm_package_tarball(product, matches[0]) - return matches[0] - - -def json_contains_workspace_protocol(value: object) -> bool: - if isinstance(value, str): - return value.startswith("workspace:") - if isinstance(value, list): - return any(json_contains_workspace_protocol(item) for item in value) - if isinstance(value, dict): - return any(json_contains_workspace_protocol(item) for item in value.values()) - return False - - -def validate_staged_npm_package_tarball(product: str, tarball: Path) -> None: - package_dir = ROOT / package_path(product) - package_json = package_dir / "package.json" - if not package_json.is_file(): - fail(f"{product} has no package.json at {package_json.relative_to(ROOT)}") - source_package = json.loads(package_json.read_text(encoding="utf-8")) - expected_name = source_package.get("name") - expected_version = current_product_version(product) - if not isinstance(expected_name, str) or not expected_name: - fail(f"{package_json.relative_to(ROOT)} must declare a package name") - expected_filename = f"{safe_npm_package_filename_prefix(expected_name)}-{expected_version}.tgz" - if tarball.name != expected_filename: - fail(f"{product} staged npm tarball must be named {expected_filename}, got {tarball.name}") - - try: - with tarfile.open(tarball, "r:gz") as archive: - names = set(archive.getnames()) - if "package/package.json" not in names: - fail(f"{tarball.relative_to(ROOT)} is missing package/package.json") - package_member = archive.extractfile("package/package.json") - if package_member is None: - fail(f"{tarball.relative_to(ROOT)} package/package.json could not be read") - with package_member: - packed_package = json.loads(package_member.read().decode("utf-8")) - if packed_package.get("name") != expected_name: - fail( - f"{tarball.relative_to(ROOT)} package name must be {expected_name}, " - f"got {packed_package.get('name')!r}" - ) - if packed_package.get("version") != expected_version: - fail( - f"{tarball.relative_to(ROOT)} package version must be {expected_version}, " - f"got {packed_package.get('version')!r}" - ) - if json_contains_workspace_protocol(packed_package): - fail(f"{tarball.relative_to(ROOT)} must not contain workspace: dependency specifiers") - if not any(name.startswith("package/lib/") for name in names): - fail(f"{tarball.relative_to(ROOT)} must contain built package/lib output") - except (tarfile.TarError, json.JSONDecodeError, UnicodeDecodeError) as error: - fail(f"{tarball.relative_to(ROOT)} is not a valid staged npm package tarball: {error}") - - -def npm_publish_pnpm_packed_package(package_dir: Path, *, product: str | None = None) -> None: - tarball = staged_npm_package_tarball(product) if product is not None else None - if tarball is None: - tarball = pnpm_pack_for_npm_publish(package_dir) - run(["npm", "publish", str(tarball), "--access", "public", "--provenance"]) - - def xtask(args: list[str], *, quiet: bool = False) -> str: command = ["cargo", "run"] if quiet: @@ -768,10 +620,6 @@ def xtask(args: list[str], *, quiet: bool = False) -> str: return "" -def cargo_publish_args(allow_dirty: bool) -> list[str]: - return ["--allow-dirty"] if allow_dirty else [] - - def passthrough_value(args: list[str], name: str) -> str | None: index = 0 while index < len(args): @@ -818,10 +666,6 @@ def is_extension_product(product: str) -> bool: return product.startswith(EXTENSION_PRODUCT_PREFIX) -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]]: coverage: dict[str, set[str]] = {} for row in publish_step_target_coverage_rows(product): @@ -879,17 +723,6 @@ def upload_github_release_assets(product: str, *, tag: str | None = None, assets run(command) -def npm_package_is_published(package_name: str, version: str) -> bool: - result = subprocess.run( - ["npm", "view", f"{package_name}@{version}", "version"], - cwd=ROOT, - text=True, - capture_output=True, - check=False, - ) - return result.returncode == 0 and result.stdout.strip() == version - - def validate_no_consumer_install_scripts(package: dict, label: str) -> None: scripts = package.get("scripts", {}) if not isinstance(scripts, dict): @@ -899,101 +732,6 @@ def validate_no_consumer_install_scripts(package: dict, label: str) -> None: fail(f"{label} must not declare consumer install lifecycle scripts: {', '.join(forbidden)}") -def git_commit(ref: str) -> str | None: - result = subprocess.run( - ["git", "rev-list", "-n", "1", ref], - cwd=ROOT, - text=True, - capture_output=True, - check=False, - ) - if result.returncode != 0: - return None - value = result.stdout.strip() - return value or None - - -def product_tag_points_at(product: str, head_ref: str) -> bool: - tag_commit = git_commit(product_tag(product)) - head_commit = git_commit(head_ref) - return tag_commit is not None and head_commit is not None and tag_commit == head_commit - - -def product_registry_is_published(product: str) -> bool: - return succeeds( - registry_check_args( - "--product", - product, - "--require-published", - ) - ) - - -def published_rerun(product: str, head_ref: str) -> bool: - return product_tag_points_at(product, head_ref) and product_registry_is_published(product) - - -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 cratesio_crate_version_exists(crate, version): - return - if attempt < retries: - print(f"waiting for crates.io to index {crate} {version}...") - time.sleep(retry_delay) - 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 cratesio_crate_version_exists(package, version): - print(f"{package} {version} is already published on crates.io; skipping cargo publish.") - return - run( - [ - "cargo", - "publish", - "-p", - package, - "--locked", - *cargo_publish_args(allow_dirty), - ] - ) - wait_for_cratesio_package(package, version) - - -def cargo_publish_manifest(package: str, version: str, manifest_path: Path, *, allow_dirty: bool = False) -> None: - if cratesio_crate_version_exists(package, version): - print(f"{package} {version} is already published on crates.io; skipping cargo publish.") - return - run( - [ - "cargo", - "publish", - "--manifest-path", - str(manifest_path), - "--target-dir", - str(ROOT / "target" / "release" / "cargo-publish"), - *cargo_publish_args(allow_dirty), - ] - ) - wait_for_cratesio_package(package, version) - - -def cargo_registry_packages(product: str) -> list[str]: - return sorted(registry_package_names(product, "crates")) - - def wasix_release_asset_dir() -> Path: return ROOT / "target/oliphaunt-wasix/release-assets" @@ -1056,11 +794,6 @@ def validate_wasix_release_assets() -> None: print(f"validated liboliphaunt-wasix staged release assets under {asset_dir.relative_to(ROOT)}") -def run_wasix_runtime_release_dry_run(allow_dirty: bool) -> None: - validate_wasix_release_assets() - liboliphaunt_wasix_cargo_artifact_crates(current_product_version("liboliphaunt-wasix")) - - def tar_zstd_members(archive: Path) -> list[str]: result = subprocess.run( ["tar", "--zstd", "-tf", str(archive)], @@ -1309,11 +1042,6 @@ def ensure_liboliphaunt_release_assets() -> None: ) -def run_liboliphaunt_dry_run() -> None: - ensure_liboliphaunt_release_assets() - liboliphaunt_cargo_artifact_crates(current_product_version("liboliphaunt-native")) - - def staged_runtime_input_dirs(env_name: str) -> list[Path]: raw = os.environ.get(env_name) or os.environ.get("OLIPHAUNT_RELEASE_ASSET_INPUT_DIRS") or "" dirs = [Path(item).expanduser() for item in raw.split(":") if item] @@ -1632,12 +1360,6 @@ def extension_asset_paths(product: str) -> list[str]: return [str(path.relative_to(ROOT)) for path in assets] -def run_extension_artifact_dry_run(product: str) -> None: - for asset in extension_asset_paths(product): - print(f"{product} release asset: {asset}") - run_extension_maven_artifact_dry_run(product) - - def build_maven_artifact_manifest( name: str, *, @@ -1679,157 +1401,6 @@ def run_maven_artifact_publisher(manifest: Path, task: str, cache_slug: str) -> ) -def run_runtime_maven_artifact_dry_run() -> None: - manifest = build_maven_artifact_manifest("liboliphaunt-native-runtime", runtime=True) - run_maven_artifact_publisher( - manifest, - ":oliphaunt-maven-artifacts:publishToMavenLocal", - "liboliphaunt-native-maven-dry-run", - ) - - -def run_extension_maven_artifact_dry_run(product: str) -> None: - manifest = build_maven_artifact_manifest(product, extensions=True, extension_products=[product]) - run_maven_artifact_publisher( - manifest, - ":oliphaunt-maven-artifacts:publishToMavenLocal", - f"{product}-maven-dry-run", - ) - - -def validate_staged_sdk_package(product: str) -> None: - run(["tools/dev/bun.sh", "tools/release/check-staged-artifacts.mjs", "--require-sdk-product", product]) - - -def run_broker_dry_run() -> None: - version = current_product_version("oliphaunt-broker") - ensure_broker_release_assets() - broker_npm_tarballs(version) - broker_cargo_artifact_crates(version) - - -def run_react_native_sdk_dry_run() -> None: - validate_staged_sdk_package("oliphaunt-react-native") - require_staged_sdk_artifact("oliphaunt-react-native", "npm package", (".tgz",)) - - -def run_node_direct_dry_run() -> None: - run(["src/runtimes/node-direct/tools/check-package.sh", "package-shape"]) - ensure_node_direct_release_assets() - node_direct_optional_npm_tarballs(current_product_version("oliphaunt-node-direct")) - - -def run_product_publish_dry_runs(products: list[str], *, allow_dirty: bool, head_ref: str) -> None: - for product in products: - if product == "liboliphaunt-native": - run_liboliphaunt_dry_run() - liboliphaunt_npm_tarballs(current_product_version("liboliphaunt-native")) - run_runtime_maven_artifact_dry_run() - elif product == "liboliphaunt-wasix": - run_wasix_runtime_release_dry_run(allow_dirty) - elif product == "oliphaunt-broker": - run_broker_dry_run() - elif product == "oliphaunt-node-direct": - run_node_direct_dry_run() - elif product == "oliphaunt-react-native": - run_react_native_sdk_dry_run() - elif is_extension_product(product): - run_extension_artifact_dry_run(product) - else: - fail(f"no publish dry-run handler for {product}") - - -def publish_liboliphaunt_github_assets(head_ref: str) -> None: - verify_release_tag("liboliphaunt-native", head_ref) - ensure_liboliphaunt_release_assets() - assets = glob_release_assets( - ROOT / "target/liboliphaunt/release-assets", - (".tar.gz", ".tar.zst", ".tsv", ".zip", ".sha256"), - ) - upload_github_release_assets("liboliphaunt-native", assets=assets) - - -def publish_liboliphaunt_runtime_maven(head_ref: str) -> None: - verify_release_tag("liboliphaunt-native", head_ref) - ensure_liboliphaunt_release_assets() - manifest = build_maven_artifact_manifest("liboliphaunt-native-runtime", runtime=True) - version = current_product_version("liboliphaunt-native") - if succeeds( - [ - *REGISTRY_PUBLICATION_CHECK, - "--product", - "liboliphaunt-native", - "--registry-kind", - "maven", - "--require-published", - ] - ): - print(f"dev.oliphaunt.runtime artifacts {version} are already published on Maven Central; skipping publishAndReleaseToMavenCentral.") - else: - run_maven_artifact_publisher( - manifest, - ":oliphaunt-maven-artifacts:publishAndReleaseToMavenCentral", - "liboliphaunt-native-maven-release", - ) - run( - [ - *REGISTRY_PUBLICATION_CHECK, - "--product", - "liboliphaunt-native", - "--registry-kind", - "maven", - "--require-published", - "--retries", - "12", - "--retry-delay", - "10", - ] - ) - - -def publish_react_native_npm(head_ref: str) -> None: - verify_release_tag("oliphaunt-react-native", head_ref) - version = current_product_version("oliphaunt-react-native") - if npm_package_is_published("@oliphaunt/react-native", version): - print(f"@oliphaunt/react-native {version} is already published on npm; skipping npm publish.") - else: - npm_publish_pnpm_packed_package( - ROOT / "src/sdks/react-native", - product="oliphaunt-react-native", - ) - run( - [ - *REGISTRY_PUBLICATION_CHECK, - "--product", - "oliphaunt-react-native", - "--require-published", - "--retries", - "12", - "--retry-delay", - "10", - ] - ) - upload_github_release_assets("oliphaunt-react-native") - - -def publish_broker_release_assets(head_ref: str) -> None: - verify_release_tag("oliphaunt-broker", head_ref) - ensure_broker_release_assets() - assets = glob_release_assets( - ROOT / "target/oliphaunt-broker/release-assets", - (".tar.gz", ".zip", ".sha256"), - ) - upload_github_release_assets("oliphaunt-broker", assets=assets) - - -def publish_node_direct_release_assets(head_ref: str) -> None: - verify_release_tag("oliphaunt-node-direct", head_ref) - ensure_node_direct_release_assets() - asset_dir = ROOT / "target/oliphaunt-node-direct/release-assets" - assets = glob_release_assets(asset_dir, (".tar.gz", ".zip", ".sha256")) - upload_github_release_assets("oliphaunt-node-direct", assets=assets) - - def node_direct_optional_package_targets(version: str) -> list[tuple[str, Path, ArtifactTarget]]: package_dirs = npm_package_dirs_under(NODE_DIRECT_PACKAGE_ROOT) packages: list[tuple[str, Path, ArtifactTarget]] = [] @@ -2359,14 +1930,6 @@ def stage_broker_npm_payloads( return stages -def npm_publish_packages(package_tarballs: list[tuple[str, Path]], version: str) -> None: - for package_name, tarball in package_tarballs: - if npm_package_is_published(package_name, version): - print(f"{package_name} {version} is already published on npm; skipping npm publish.") - continue - run(["npm", "publish", str(tarball), "--access", "public", "--provenance"]) - - def node_direct_optional_npm_tarballs(version: str) -> list[tuple[str, Path]]: tarballs: list[tuple[str, Path]] = [] for package_name, _package_dir, _target in node_direct_optional_package_targets(version): @@ -2723,327 +2286,3 @@ def liboliphaunt_wasix_cargo_artifact_crates(version: str) -> list[tuple[str, Pa if unexpected: fail("unexpected liboliphaunt-wasix Cargo artifact crate(s): " + ", ".join(unexpected)) return packages - - -def publish_liboliphaunt_cargo_artifacts(head_ref: str) -> None: - verify_release_tag("liboliphaunt-native", head_ref) - version = current_product_version("liboliphaunt-native") - packages = liboliphaunt_cargo_artifact_crates(version) - for crate, _crate_path, manifest_path, role in packages: - if role == "part": - cargo_publish_manifest(crate, version, manifest_path) - 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], - version, - ) - run( - [ - *REGISTRY_PUBLICATION_CHECK, - "--product", - "liboliphaunt-native", - "--registry-kind", - "crates", - "--require-published", - "--retries", - "12", - "--retry-delay", - "10", - ] - ) - - -def publish_liboliphaunt_wasix_cargo_artifacts(head_ref: str) -> None: - verify_release_tag("liboliphaunt-wasix", head_ref) - version = current_product_version("liboliphaunt-wasix") - 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( - [ - *REGISTRY_PUBLICATION_CHECK, - "--product", - "liboliphaunt-wasix", - "--registry-kind", - "crates", - "--require-published", - "--retries", - "12", - "--retry-delay", - "10", - ] - ) - - -def publish_broker_cargo_artifacts(head_ref: str) -> None: - verify_release_tag("oliphaunt-broker", head_ref) - version = current_product_version("oliphaunt-broker") - for crate, _crate_path, manifest_path in broker_cargo_artifact_crates(version): - cargo_publish_manifest(crate, version, manifest_path) - run( - [ - *REGISTRY_PUBLICATION_CHECK, - "--product", - "oliphaunt-broker", - "--registry-kind", - "crates", - "--require-published", - "--retries", - "12", - "--retry-delay", - "10", - ] - ) - - -def publish_node_direct_npm_optional_packages(head_ref: str) -> None: - verify_release_tag("oliphaunt-node-direct", head_ref) - version = current_product_version("oliphaunt-node-direct") - ensure_node_direct_release_assets() - tarballs = node_direct_optional_npm_tarballs(version) - for package_name, tarball in tarballs: - if npm_package_is_published(package_name, version): - print(f"{package_name} {version} is already published on npm; skipping npm publish.") - continue - run(["npm", "publish", str(tarball), "--access", "public", "--provenance"]) - run( - [ - *REGISTRY_PUBLICATION_CHECK, - "--product", - "oliphaunt-node-direct", - "--require-published", - "--retries", - "12", - "--retry-delay", - "10", - ] - ) - - -def publish_liboliphaunt_npm_packages(head_ref: str) -> None: - verify_release_tag("liboliphaunt-native", head_ref) - version = current_product_version("liboliphaunt-native") - npm_publish_packages(liboliphaunt_npm_tarballs(version), version) - run( - [ - *REGISTRY_PUBLICATION_CHECK, - "--product", - "liboliphaunt-native", - "--registry-kind", - "npm", - "--require-published", - "--retries", - "12", - "--retry-delay", - "10", - ] - ) - - -def publish_broker_npm_packages(head_ref: str) -> None: - verify_release_tag("oliphaunt-broker", head_ref) - version = current_product_version("oliphaunt-broker") - npm_publish_packages(broker_npm_tarballs(version), version) - run( - [ - *REGISTRY_PUBLICATION_CHECK, - "--product", - "oliphaunt-broker", - "--registry-kind", - "npm", - "--require-published", - "--retries", - "12", - "--retry-delay", - "10", - ] - ) - - -def publish_wasm_release_assets() -> None: - validate_wasix_release_assets() - asset_dir = wasix_release_asset_dir() - assets = glob_release_assets(asset_dir, (".tar.zst", ".sha256")) - upload_github_release_assets("liboliphaunt-wasix", assets=assets) - - -def publish_extension_release_assets(product: str, head_ref: str) -> None: - verify_release_tag(product, head_ref) - upload_github_release_assets(product, assets=extension_asset_paths(product)) - - -def publish_selected_extension_release_assets(products: list[str], head_ref: str) -> None: - extensions = selected_extension_products(products) - if not extensions: - fail("no extension products selected") - for product in extensions: - verify_release_tag(product, head_ref) - upload_github_release_assets(product, assets=extension_asset_paths(product)) - - -def extension_maven_artifacts_published(products: list[str]) -> bool: - return succeeds( - [ - *REGISTRY_PUBLICATION_CHECK, - "--products-json", - json.dumps(products), - "--registry-kind", - "maven", - "--require-published", - ] - ) - - -def require_extension_maven_artifacts_published(products: list[str]) -> None: - run( - [ - *REGISTRY_PUBLICATION_CHECK, - "--products-json", - json.dumps(products), - "--registry-kind", - "maven", - "--require-published", - "--retries", - "12", - "--retry-delay", - "10", - ] - ) - - -def publish_selected_extension_maven(products: list[str], head_ref: str) -> None: - extensions = selected_extension_products(products) - if not extensions: - fail("no extension products selected") - for product in extensions: - verify_release_tag(product, head_ref) - ensure_extension_release_package(product) - manifest = build_maven_artifact_manifest( - "selected-extensions", - extensions=True, - extension_products=extensions, - ) - if extension_maven_artifacts_published(extensions): - print("selected Oliphaunt extension Android artifacts are already published on Maven Central; skipping publishAndReleaseToMavenCentral.") - else: - run_maven_artifact_publisher( - manifest, - ":oliphaunt-maven-artifacts:publishAndReleaseToMavenCentral", - "oliphaunt-extensions-maven-release", - ) - require_extension_maven_artifacts_published(extensions) - - -def command_publish_product_step(args: argparse.Namespace) -> None: - product = args.product - step = args.step - head_ref = args.head_ref - if product is None or step is None: - fail("publish product step requires --product and --step") - known = set(product_ids()) - if product not in known: - fail(f"unknown release product: {product}") - - 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) - elif product == "liboliphaunt-native" and step == "maven-central": - publish_liboliphaunt_runtime_maven(head_ref) - elif product == "liboliphaunt-native" and step == "crates-io": - publish_liboliphaunt_cargo_artifacts(head_ref) - elif product == "liboliphaunt-wasix" and step == "github-release-assets": - verify_release_tag("liboliphaunt-wasix", head_ref) - publish_wasm_release_assets() - elif product == "liboliphaunt-wasix" and step == "crates-io": - publish_liboliphaunt_wasix_cargo_artifacts(head_ref) - elif product == "oliphaunt-react-native" and step == "npm": - publish_react_native_npm(head_ref) - elif product == "oliphaunt-broker" and step == "github-release-assets": - publish_broker_release_assets(head_ref) - elif product == "oliphaunt-broker" and step == "crates-io": - publish_broker_cargo_artifacts(head_ref) - elif product == "oliphaunt-broker" and step == "npm": - publish_broker_npm_packages(head_ref) - elif product == "oliphaunt-node-direct" and step == "github-release-assets": - publish_node_direct_release_assets(head_ref) - elif product == "oliphaunt-node-direct" and step == "npm": - publish_node_direct_npm_optional_packages(head_ref) - elif is_extension_product(product) and step == "github-release-assets": - publish_extension_release_assets(product, head_ref) - elif is_extension_product(product) and step == "maven-central": - publish_selected_extension_maven([product], head_ref) - else: - fail(f"unsupported publish step {product}:{step}") - - -def command_publish_dry_run(args: argparse.Namespace, passthrough: list[str]) -> None: - run(["tools/dev/bun.sh", "tools/release/release-check.mjs"]) - products = selected_products_from_passthrough(passthrough) - if products: - run(["tools/dev/bun.sh", "tools/release/release-check-registries.mjs", *passthrough]) - run_product_publish_dry_runs( - products, - allow_dirty=args.allow_dirty, - head_ref=passthrough_value(passthrough, "--head-ref") or "HEAD", - ) - return - if passthrough: - run(["tools/dev/bun.sh", "tools/release/release-check-registries.mjs", *passthrough]) - - -def command_publish(args: argparse.Namespace, passthrough: list[str]) -> None: - products = selected_products_from_passthrough(passthrough) - if args.step == "github-release-assets" and not args.product and selected_extension_products(products): - publish_selected_extension_release_assets(products, args.head_ref) - return - if args.step == "maven-central" and not args.product and selected_extension_products(products): - publish_selected_extension_maven(products, args.head_ref) - return - if args.product or args.step: - command_publish_product_step(args) - return - products_args = passthrough - 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") - - -def main(argv: list[str]) -> int: - parser = argparse.ArgumentParser(description=__doc__) - subparsers = parser.add_subparsers(dest="command", required=True) - - dry_run = subparsers.add_parser("publish-dry-run") - dry_run.add_argument("--allow-dirty", action="store_true") - - publish = subparsers.add_parser("publish") - publish.add_argument("--allow-dirty", action="store_true") - publish.add_argument("--product") - publish.add_argument("--step") - publish.add_argument("--head-ref", default="HEAD") - - args, passthrough = parser.parse_known_args(argv) - command = args.command - - if command == "publish-dry-run": - command_publish_dry_run(args, passthrough) - elif command == "publish": - command_publish(args, passthrough) - else: - fail(f"unknown command {command}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) From 797d79cd65a0a61ace77398aa0b61747a7a3b333 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Mon, 29 Jun 2026 06:00:08 +0000 Subject: [PATCH 302/308] fix: harden native and wasix packaging checks --- prek.toml | 2 +- src/extensions/artifacts/native/moon.yml | 2 +- .../tools/extension-artifact-packager.mjs | 13 ++- src/extensions/artifacts/wasix/moon.yml | 2 +- ...2026-06-07-transitional-catalog-smoke.json | 2 +- .../generated/docs/extension-evidence.json | 80 ++++++++-------- src/extensions/model/moon.yml | 2 +- .../native/bin/mobile-postgis-extensions.sh | 27 +++--- .../assets/build/build_wasix_libiconv.sh | 36 ++++---- .../wasix/assets/build/docker/Dockerfile | 1 + .../assets/generated/asset-inputs.sha256 | 2 +- .../node-direct/tools/build-node-addon.sh | 3 +- tools/policy/check-tauri-example-rustfmt.sh | 14 +++ .../optimize_native_runtime_payload.mjs | 44 ++++++++- .../package-liboliphaunt-mobile-assets.sh | 4 +- .../release/strip_native_release_binaries.mjs | 91 ++++++++++++++++--- 16 files changed, 228 insertions(+), 97 deletions(-) create mode 100755 tools/policy/check-tauri-example-rustfmt.sh diff --git a/prek.toml b/prek.toml index 04f867fe..ea8c689a 100644 --- a/prek.toml +++ b/prek.toml @@ -34,5 +34,5 @@ hooks = [ repo = "local" hooks = [ { id = "cargo-fmt", name = "cargo fmt", language = "system", entry = "cargo fmt --check", pass_filenames = false, files = "\\.(rs|toml)$", stages = ["pre-commit"] }, - { id = "tauri-cargo-fmt", name = "Tauri cargo fmt", language = "system", entry = "cargo fmt --manifest-path src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/Cargo.toml --check", pass_filenames = false, files = "^src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/.*\\.(rs|toml)$", stages = ["pre-commit"] }, + { id = "tauri-rustfmt", name = "Tauri rustfmt", language = "system", entry = "tools/policy/check-tauri-example-rustfmt.sh", pass_filenames = false, files = "^src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri/.*\\.(rs|toml)$", stages = ["pre-commit"] }, ] diff --git a/src/extensions/artifacts/native/moon.yml b/src/extensions/artifacts/native/moon.yml index acf2eff0..0368ba2f 100644 --- a/src/extensions/artifacts/native/moon.yml +++ b/src/extensions/artifacts/native/moon.yml @@ -23,7 +23,7 @@ project: tasks: check: tags: ["quality", "static"] - command: "tools/dev/bun.sh src/extensions/tools/check-extension-model.mjs --check" + command: "bash tools/dev/bun.sh src/extensions/tools/check-extension-model.mjs --check" deps: - "extension-model:check" inputs: diff --git a/src/extensions/artifacts/native/tools/extension-artifact-packager.mjs b/src/extensions/artifacts/native/tools/extension-artifact-packager.mjs index ad03224d..21a67546 100755 --- a/src/extensions/artifacts/native/tools/extension-artifact-packager.mjs +++ b/src/extensions/artifacts/native/tools/extension-artifact-packager.mjs @@ -805,10 +805,15 @@ async function writeArtifactDirectory(artifactRoot, args) { await fs.writeFile(path.join(artifactRoot, 'manifest.properties'), manifest); } -function stripNativeReleaseBinaries(artifactRoot) { +function stripNativeReleaseBinaries(artifactRoot, nativeTarget) { + const stripArgs = ['tools/release/strip_native_release_binaries.mjs']; + if (nativeTarget) { + stripArgs.push('--target', nativeTarget); + } + stripArgs.push(artifactRoot); const result = spawnSync( path.join(root, 'tools/dev/bun.sh'), - ['tools/release/strip_native_release_binaries.mjs', artifactRoot], + stripArgs, { cwd: root, stdio: 'inherit' }, ); if (result.error !== undefined) { @@ -849,7 +854,7 @@ async function createArtifact(argv) { await fs.rm(output, { recursive: true, force: true }); } await writeArtifactDirectory(output, args); - stripNativeReleaseBinaries(output); + stripNativeReleaseBinaries(output, args.nativeTarget); console.log(`path=${output}`); console.log(`sqlName=${args.sqlName}`); console.log('format=directory'); @@ -864,7 +869,7 @@ async function createArtifact(argv) { await fs.mkdir(artifactRoot, { recursive: true }); try { await writeArtifactDirectory(artifactRoot, args); - stripNativeReleaseBinaries(artifactRoot); + stripNativeReleaseBinaries(artifactRoot, args.nativeTarget); if (args.format === 'tar') { await fs.writeFile(output, await createTar(artifactRoot)); } else { diff --git a/src/extensions/artifacts/wasix/moon.yml b/src/extensions/artifacts/wasix/moon.yml index b6a137cb..134c3970 100644 --- a/src/extensions/artifacts/wasix/moon.yml +++ b/src/extensions/artifacts/wasix/moon.yml @@ -23,7 +23,7 @@ project: tasks: check: tags: ["quality", "static"] - command: "tools/dev/bun.sh src/extensions/tools/check-extension-model.mjs --check" + command: "bash tools/dev/bun.sh src/extensions/tools/check-extension-model.mjs --check" deps: - "extension-model:check" inputs: 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 372af6c6..88c64348 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:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2", + "sourceDigest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50", "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 d04afb40..7a00e414 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:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -56,7 +56,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -92,7 +92,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -128,7 +128,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -164,7 +164,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -200,7 +200,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -236,7 +236,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -272,7 +272,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -308,7 +308,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -344,7 +344,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -380,7 +380,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -416,7 +416,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -452,7 +452,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -488,7 +488,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -524,7 +524,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -560,7 +560,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -596,7 +596,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -632,7 +632,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -668,7 +668,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -704,7 +704,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -740,7 +740,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -776,7 +776,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -812,7 +812,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -848,7 +848,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -884,7 +884,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -920,7 +920,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -956,7 +956,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -992,7 +992,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -1028,7 +1028,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -1064,7 +1064,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -1100,7 +1100,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -1136,7 +1136,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -1172,7 +1172,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -1208,7 +1208,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -1244,7 +1244,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -1280,7 +1280,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -1316,7 +1316,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -1352,7 +1352,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -1388,7 +1388,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2" + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" } ], "platform-targets": [ @@ -1420,7 +1420,7 @@ "path": "src/extensions/evidence/runs" } ], - "source-digest": "sha256:a548468b5cffefc0421a53752f2205a37c1e49d5aaec437a74c5b4577bcedbf2", + "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50", "source-digest-inputs": [ "src/postgres/versions/18/source.toml", "src/extensions/catalog/extensions.promoted.toml", diff --git a/src/extensions/model/moon.yml b/src/extensions/model/moon.yml index ac57e62a..8b4b3fa8 100644 --- a/src/extensions/model/moon.yml +++ b/src/extensions/model/moon.yml @@ -14,7 +14,7 @@ project: tasks: check: tags: ["quality", "static"] - command: "tools/dev/bun.sh src/extensions/tools/check-extension-model.mjs --check" + command: "bash tools/dev/bun.sh src/extensions/tools/check-extension-model.mjs --check" inputs: - "/src/extensions/**/*" - "/src/shared/extension-runtime-contract/**/*" diff --git a/src/runtimes/liboliphaunt/native/bin/mobile-postgis-extensions.sh b/src/runtimes/liboliphaunt/native/bin/mobile-postgis-extensions.sh index e6d744dd..b229be93 100644 --- a/src/runtimes/liboliphaunt/native/bin/mobile-postgis-extensions.sh +++ b/src/runtimes/liboliphaunt/native/bin/mobile-postgis-extensions.sh @@ -239,6 +239,7 @@ build_postgis_libiconv_dependency() { esac local dependency_dir="$mobile_static_dependency_root/libiconv" local build_root="$work_root/libiconv-$oliphaunt_mobile_target-build" + local source_dir="$repo_root/target/oliphaunt-sources/checkouts/libiconv" local source_tar="$work_root/source/libiconv-1.19.tar.gz" local archive="$dependency_dir/lib/libiconv.a" local charset_archive="$dependency_dir/lib/libcharset.a" @@ -247,19 +248,23 @@ build_postgis_libiconv_dependency() { oliphaunt_postgis_dependency_archive libcharset "$charset_archive" return 0 fi - mkdir -p "$(dirname "$source_tar")" - if [ ! -f "$source_tar" ]; then - curl -L --fail --silent --show-error \ - --retry 8 --retry-all-errors --retry-delay 5 --connect-timeout 20 \ - https://ftp.gnu.org/gnu/libiconv/libiconv-1.19.tar.gz \ - -o "$source_tar" - fi - printf '%s %s\n' \ - "88dd96a8c0464eca144fc791ae60cd31cd8ee78321e67397e25fc095c4a19aa6" \ - "$source_tar" | shasum -a 256 -c - >> "$make_log" 2>&1 rm -rf "$build_root" "$dependency_dir" mkdir -p "$build_root" "$dependency_dir" - tar -xzf "$source_tar" -C "$build_root" --strip-components=1 + if [ -f "$source_dir/configure" ]; then + rsync -a --delete --exclude .git "$source_dir/" "$build_root/" + else + mkdir -p "$(dirname "$source_tar")" + if [ ! -f "$source_tar" ]; then + curl -L --fail --silent --show-error \ + --retry 8 --retry-all-errors --retry-delay 5 --connect-timeout 20 \ + https://ftp.gnu.org/gnu/libiconv/libiconv-1.19.tar.gz \ + -o "$source_tar" + fi + printf '%s %s\n' \ + "88dd96a8c0464eca144fc791ae60cd31cd8ee78321e67397e25fc095c4a19aa6" \ + "$source_tar" | shasum -a 256 -c - >> "$make_log" 2>&1 + tar -xzf "$source_tar" -C "$build_root" --strip-components=1 + fi ( cd "$build_root" CC="$clang_path" AR="$llvm_ar" RANLIB="$llvm_ranlib" \ diff --git a/src/runtimes/liboliphaunt/wasix/assets/build/build_wasix_libiconv.sh b/src/runtimes/liboliphaunt/wasix/assets/build/build_wasix_libiconv.sh index 82cdce61..c5638b79 100755 --- a/src/runtimes/liboliphaunt/wasix/assets/build/build_wasix_libiconv.sh +++ b/src/runtimes/liboliphaunt/wasix/assets/build/build_wasix_libiconv.sh @@ -9,6 +9,7 @@ GENERATED_ROOT="$(oliphaunt_wasix_generated_root "$REPO_ROOT")" LIBICONV_VERSION="${LIBICONV_VERSION:-1.19}" LIBICONV_URL="${LIBICONV_URL:-https://ftp.gnu.org/gnu/libiconv/libiconv-$LIBICONV_VERSION.tar.gz}" LIBICONV_SHA256="${LIBICONV_SHA256:-88dd96a8c0464eca144fc791ae60cd31cd8ee78321e67397e25fc095c4a19aa6}" +LIBICONV_SOURCE_DIR="${LIBICONV_SOURCE_DIR:-$REPO_ROOT/target/oliphaunt-sources/checkouts/libiconv}" LIBICONV_ARCHIVE="${LIBICONV_ARCHIVE:-$GENERATED_ROOT/source-cache/libiconv-$LIBICONV_VERSION.tar.gz}" LIBICONV_BUILD_DIR="${LIBICONV_BUILD_DIR:-$GENERATED_ROOT/work/libiconv-wasix-build}" LIBICONV_PREFIX="${LIBICONV_PREFIX:-$GENERATED_ROOT/work/libiconv-wasix}" @@ -39,24 +40,27 @@ if [ -f "$LIBICONV_PREFIX/.oliphaunt-wasix-libiconv-build" ] && fi { - mkdir -p "$(dirname "$LIBICONV_ARCHIVE")" - if [ ! -f "$LIBICONV_ARCHIVE" ] || - [ "$(sha256sum "$LIBICONV_ARCHIVE" | awk '{print $1}')" != "$LIBICONV_SHA256" ]; then - tmp_archive="$LIBICONV_ARCHIVE.tmp" - rm -f "$tmp_archive" - curl -fsSL --retry 8 --retry-all-errors --retry-delay 5 --connect-timeout 20 \ - "$LIBICONV_URL" -o "$tmp_archive" - actual_sha="$(sha256sum "$tmp_archive" | awk '{print $1}')" - if [ "$actual_sha" != "$LIBICONV_SHA256" ]; then - echo "libiconv archive sha256 mismatch: expected $LIBICONV_SHA256 got $actual_sha" >&2 - exit 1 - fi - mv "$tmp_archive" "$LIBICONV_ARCHIVE" - fi - rm -rf "$LIBICONV_BUILD_DIR" "$LIBICONV_PREFIX" mkdir -p "$LIBICONV_BUILD_DIR" "$(dirname "$LIBICONV_PREFIX")" - tar -xzf "$LIBICONV_ARCHIVE" -C "$LIBICONV_BUILD_DIR" --strip-components=1 + if [ -f "$LIBICONV_SOURCE_DIR/configure" ]; then + oliphaunt_wasix_copy_source_clean "$LIBICONV_SOURCE_DIR" "$LIBICONV_BUILD_DIR" + else + mkdir -p "$(dirname "$LIBICONV_ARCHIVE")" + if [ ! -f "$LIBICONV_ARCHIVE" ] || + [ "$(sha256sum "$LIBICONV_ARCHIVE" | awk '{print $1}')" != "$LIBICONV_SHA256" ]; then + tmp_archive="$LIBICONV_ARCHIVE.tmp" + rm -f "$tmp_archive" + curl -fsSL --retry 8 --retry-all-errors --retry-delay 5 --connect-timeout 20 \ + "$LIBICONV_URL" -o "$tmp_archive" + actual_sha="$(sha256sum "$tmp_archive" | awk '{print $1}')" + if [ "$actual_sha" != "$LIBICONV_SHA256" ]; then + echo "libiconv archive sha256 mismatch: expected $LIBICONV_SHA256 got $actual_sha" >&2 + exit 1 + fi + mv "$tmp_archive" "$LIBICONV_ARCHIVE" + fi + tar -xzf "$LIBICONV_ARCHIVE" -C "$LIBICONV_BUILD_DIR" --strip-components=1 + fi cd "$LIBICONV_BUILD_DIR" ./configure \ --build="$(build-aux/config.guess)" \ diff --git a/src/runtimes/liboliphaunt/wasix/assets/build/docker/Dockerfile b/src/runtimes/liboliphaunt/wasix/assets/build/docker/Dockerfile index 10744615..e94ff4b4 100644 --- a/src/runtimes/liboliphaunt/wasix/assets/build/docker/Dockerfile +++ b/src/runtimes/liboliphaunt/wasix/assets/build/docker/Dockerfile @@ -30,6 +30,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ sqlite3 \ tar \ tcl \ + unzip \ wget \ xz-utils \ zstd diff --git a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 index 720513b7..536bfd88 100644 --- a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 +++ b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 @@ -1 +1 @@ -8dc087fc0c529f19e1151a544d2257e13328df10fc9e7b9a1d3bc2a2c7cc41a9 +320c3099e0f907761229e14a1b0c489ba08a42b81b53ed12ce9916f58008c5b2 diff --git a/src/runtimes/node-direct/tools/build-node-addon.sh b/src/runtimes/node-direct/tools/build-node-addon.sh index 8daa3893..1fc1daf7 100755 --- a/src/runtimes/node-direct/tools/build-node-addon.sh +++ b/src/runtimes/node-direct/tools/build-node-addon.sh @@ -261,11 +261,12 @@ if (!entry || typeof entry.filename !== 'string' || !entry.filename.endsWith('.t process.stdout.write(path.isAbsolute(entry.filename) ? entry.filename : path.join(process.env.PACK_DIR, entry.filename)); JS )" +tarball="$(to_shell_path "$tarball")" [ -f "$tarball" ] || { echo "npm pack did not create $tarball" >&2 exit 1 } -if ! tar -tzf "$tarball" | grep -Fxq "package/prebuilds/oliphaunt_node.node"; then +if ! tar --force-local -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 diff --git a/tools/policy/check-tauri-example-rustfmt.sh b/tools/policy/check-tauri-example-rustfmt.sh new file mode 100755 index 00000000..6586bb7b --- /dev/null +++ b/tools/policy/check-tauri-example-rustfmt.sh @@ -0,0 +1,14 @@ +#!/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" + +tauri_dir="src/bindings/wasix-rust/examples/tauri-sqlx-vanilla/src-tauri" +mapfile -t rust_files < <(git ls-files -- "$tauri_dir" | awk '/\.rs$/ { print }' | sort) +[ "${#rust_files[@]}" -gt 0 ] || exit 0 + +rustfmt --edition 2021 --check "${rust_files[@]}" diff --git a/tools/release/optimize_native_runtime_payload.mjs b/tools/release/optimize_native_runtime_payload.mjs index 33d3185a..eb74a2b6 100644 --- a/tools/release/optimize_native_runtime_payload.mjs +++ b/tools/release/optimize_native_runtime_payload.mjs @@ -228,6 +228,20 @@ function isDevRuntimeFile(relativePath, { windows }) { return windows && WINDOWS_DEV_RUNTIME_SUFFIXES.some((suffix) => name.endsWith(suffix)); } +function pruneTopLevelModuleDevFiles(root, { windows }) { + const moduleDir = join(root, "lib", "modules"); + if (!isDirectory(moduleDir)) { + return; + } + for (const path of walk(moduleDir)) { + const relativePath = posixRelative(moduleDir, path); + if (isDevRuntimeFile(relativePath, { windows })) { + removePath(path); + } + } + pruneEmptyDirs(moduleDir); +} + export function pruneRuntimePayload(root, target = null, { toolSet = "packaged" } = {}) { const runtimeDir = runtimeDirFor(root); if (!runtimeDir) { @@ -277,6 +291,7 @@ export function pruneRuntimePayload(root, target = null, { toolSet = "packaged" } pruneEmptyDirs(runtimeDir); + pruneTopLevelModuleDevFiles(root, { windows }); } function which(command) { @@ -317,8 +332,13 @@ function stripSupportedForTarget(target) { return true; } -function stripPayload(root) { - const result = spawnSync(process.execPath, ["tools/release/strip_native_release_binaries.mjs", root], { +function stripPayload(root, target) { + const command = ["tools/release/strip_native_release_binaries.mjs"]; + if (target) { + command.push("--target", target); + } + command.push(root); + const result = spawnSync(process.execPath, command, { cwd: ROOT, stdio: "inherit", env: process.env, @@ -383,6 +403,21 @@ function validateNativeFiles(root) { return errors; } +function validateTopLevelModuleDevFiles(root, { windows }) { + const errors = []; + const moduleDir = join(root, "lib", "modules"); + if (!isDirectory(moduleDir)) { + return errors; + } + for (const path of walk(moduleDir)) { + const relativePath = posixRelative(moduleDir, path); + if (isDevRuntimeFile(relativePath, { windows })) { + errors.push(`${rel(path)} is a development-only native module file`); + } + } + return errors; +} + function validateRuntimeTree(root, target, requireRuntime, { toolSet = "packaged" } = {}) { const errors = []; const runtimeDir = runtimeDirFor(root); @@ -461,8 +496,11 @@ function validateRuntimeTree(root, target, requireRuntime, { toolSet = "packaged } export function validatePayload(root, target = null, { requireRuntime = true, toolSet = "packaged" } = {}) { + const runtimeDir = runtimeDirFor(root); + const windows = isWindowsTarget(target, runtimeDir); const errors = [ ...validateRuntimeTree(root, target, requireRuntime, { toolSet }), + ...validateTopLevelModuleDevFiles(root, { windows }), ...validateNativeFiles(root), ]; if (errors.length > 0) { @@ -481,7 +519,7 @@ export function optimizePayload( pruneRuntimePayload(root, target, { toolSet }); const shouldStrip = strip === true || (strip === "auto" && stripSupportedForTarget(target)); if (shouldStrip) { - stripPayload(root); + stripPayload(root, target); } validatePayload(root, target, { requireRuntime, toolSet }); } diff --git a/tools/release/package-liboliphaunt-mobile-assets.sh b/tools/release/package-liboliphaunt-mobile-assets.sh index 82481e53..655abf67 100755 --- a/tools/release/package-liboliphaunt-mobile-assets.sh +++ b/tools/release/package-liboliphaunt-mobile-assets.sh @@ -76,7 +76,7 @@ package_android() { rsync -a --delete "$headers_dir/" "$stage/include/" cp "$lib" "$stage/jni/$abi/" echo "==> Stripping staged liboliphaunt Android $abi release binaries" - tools/dev/bun.sh tools/release/strip_native_release_binaries.mjs "$stage" + tools/dev/bun.sh tools/release/strip_native_release_binaries.mjs --target "$target_id" "$stage" archive_staged_dir "$stage" } @@ -114,7 +114,7 @@ package_ios() { mkdir -p "$stage_ios" rsync -a --delete "$ios_xcframework" "$stage_ios/" echo "==> Stripping staged liboliphaunt iOS release binaries" - tools/dev/bun.sh tools/release/strip_native_release_binaries.mjs "$stage_ios" + tools/dev/bun.sh tools/release/strip_native_release_binaries.mjs --target "$target_id" "$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 index 543bdd8c..f2c46e7a 100644 --- a/tools/release/strip_native_release_binaries.mjs +++ b/tools/release/strip_native_release_binaries.mjs @@ -137,7 +137,52 @@ function darwinStripTool() { return findTool("strip"); } -function stripToolFor(native) { +function androidStripTool() { + const override = envTool("OLIPHAUNT_ANDROID_STRIP", "OLIPHAUNT_ELF_STRIP", "OLIPHAUNT_STRIP"); + if (override) { + return override; + } + const ndk = process.env.ANDROID_NDK_HOME ?? process.env.ANDROID_NDK_ROOT; + if (!ndk) { + return undefined; + } + const hosts = { + linux: ["linux-x86_64"], + darwin: ["darwin-arm64", "darwin-x86_64"], + win32: ["windows-x86_64"], + }[process.platform] ?? []; + for (const host of hosts) { + const candidate = path.join( + ndk, + "toolchains", + "llvm", + "prebuilt", + host, + "bin", + process.platform === "win32" ? "llvm-strip.exe" : "llvm-strip", + ); + if (isExecutable(candidate)) { + return candidate; + } + } + return undefined; +} + +function stripToolFor(native, target) { + if (native.archive && path.extname(native.path).toLowerCase() === ".lib") { + console.error(`skippedMsvcImportLibrary=${native.path}`); + return undefined; + } + if (target?.startsWith("android-") && native.kind === "elf") { + const tool = androidStripTool(); + if (!tool) { + fail(`missing Android llvm-strip for ${native.path}; set ANDROID_NDK_HOME or OLIPHAUNT_ANDROID_STRIP`); + } + return { + tool, + flags: native.archive ? ["--strip-debug"] : ["--strip-unneeded"], + }; + } if (native.kind === "macho") { const tool = darwinStripTool(); if (!tool) { @@ -160,14 +205,6 @@ function stripToolFor(native) { } 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}`); @@ -178,9 +215,9 @@ function stripToolFor(native) { }; } -async function stripNative(native) { +async function stripNative(native, target) { const before = (await stat(native.path)).size; - const command = stripToolFor(native); + const command = stripToolFor(native, target); if (command === undefined) { return false; } @@ -198,9 +235,35 @@ async function stripNative(native) { return (await stat(native.path)).size !== before; } -const roots = Bun.argv.slice(2); +function parseArgs(argv) { + const args = { + target: undefined, + roots: [], + }; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === "--target") { + args.target = argv[++index]; + if (!args.target) { + fail("--target requires a value"); + } + continue; + } + if (arg === "--help" || arg === "-h") { + console.log("usage: strip_native_release_binaries.mjs [--target ] [path...]"); + process.exit(0); + } + if (arg.startsWith("-")) { + fail(`unknown option: ${arg}`); + } + args.roots.push(arg); + } + return args; +} + +const { target, roots } = parseArgs(Bun.argv.slice(2)); if (roots.length === 0) { - fail("usage: strip_native_release_binaries.mjs [path...]"); + fail("usage: strip_native_release_binaries.mjs [--target ] [path...]"); } const nativeFiles = []; @@ -213,7 +276,7 @@ for await (const file of iterFiles(roots)) { let changed = 0; for (const native of nativeFiles) { - if (await stripNative(native)) { + if (await stripNative(native, target)) { changed += 1; } } From aa4f14e7d1ea453f33661a2816c2c10e3d404ca9 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Mon, 29 Jun 2026 07:58:54 +0000 Subject: [PATCH 303/308] fix: stabilize remaining ci packaging checks --- ...2026-06-07-transitional-catalog-smoke.json | 2 +- .../generated/docs/extension-evidence.json | 80 +++++++++---------- src/extensions/tools/check-extension-model.py | 9 ++- .../wasix/assets/build/docker/Dockerfile | 6 +- .../assets/generated/asset-inputs.sha256 | 2 +- .../node-direct/tools/build-node-addon.sh | 22 ++++- tools/dev/bootstrap-tools.sh | 8 +- 7 files changed, 82 insertions(+), 47 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 88c64348..79706286 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:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50", + "sourceDigest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d", "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 7a00e414..74b7b379 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:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -56,7 +56,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -92,7 +92,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -128,7 +128,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -164,7 +164,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -200,7 +200,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -236,7 +236,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -272,7 +272,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -308,7 +308,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -344,7 +344,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -380,7 +380,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -416,7 +416,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -452,7 +452,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -488,7 +488,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -524,7 +524,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -560,7 +560,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -596,7 +596,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -632,7 +632,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -668,7 +668,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -704,7 +704,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -740,7 +740,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -776,7 +776,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -812,7 +812,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -848,7 +848,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -884,7 +884,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -920,7 +920,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -956,7 +956,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -992,7 +992,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -1028,7 +1028,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -1064,7 +1064,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -1100,7 +1100,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -1136,7 +1136,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -1172,7 +1172,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -1208,7 +1208,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -1244,7 +1244,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -1280,7 +1280,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -1316,7 +1316,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -1352,7 +1352,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -1388,7 +1388,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50" + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" } ], "platform-targets": [ @@ -1420,7 +1420,7 @@ "path": "src/extensions/evidence/runs" } ], - "source-digest": "sha256:b5d898a7d47052acab5f686206c7dd8581674821bbe49569d6b1a9731188dd50", + "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d", "source-digest-inputs": [ "src/postgres/versions/18/source.toml", "src/extensions/catalog/extensions.promoted.toml", diff --git a/src/extensions/tools/check-extension-model.py b/src/extensions/tools/check-extension-model.py index 0236aca3..2b3b57c2 100755 --- a/src/extensions/tools/check-extension-model.py +++ b/src/extensions/tools/check-extension-model.py @@ -4,6 +4,7 @@ import argparse import hashlib import json +import os import re import shutil import subprocess @@ -160,11 +161,17 @@ def format_typescript_source(source: str, path: Path) -> str: fail(f"failed to format generated TypeScript extension metadata with Biome {BIOME_VERSION}: {error}") +def bun_command(*args: str) -> list[str]: + if os.name == "nt": + return ["bash", "tools/dev/bun.sh", *args] + return ["tools/dev/bun.sh", *args] + + @lru_cache(maxsize=None) def release_graph_rows(command: str) -> tuple[dict, ...]: try: output = subprocess.check_output( - ["tools/dev/bun.sh", "tools/release/release_graph_query.mjs", command], + bun_command("tools/release/release_graph_query.mjs", command), cwd=ROOT, text=True, stderr=subprocess.PIPE, diff --git a/src/runtimes/liboliphaunt/wasix/assets/build/docker/Dockerfile b/src/runtimes/liboliphaunt/wasix/assets/build/docker/Dockerfile index e94ff4b4..02dda94a 100644 --- a/src/runtimes/liboliphaunt/wasix/assets/build/docker/Dockerfile +++ b/src/runtimes/liboliphaunt/wasix/assets/build/docker/Dockerfile @@ -38,12 +38,16 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ ENV HOME=/opt/wasixcc-home ENV WASIX_HOME=/opt/wasixcc-home/.wasixcc ENV PATH=/opt/wasixcc-home/.wasixcc/bin:$PATH +ARG WASIXCC_INSTALLER_URL=https://raw.githubusercontent.com/wasix-org/wasixcc/v0.4.3/installer/public/install.sh +ARG WASIXCC_SYSROOT_TAG=v2026-03-02.1 RUN bash -euxo pipefail <<'EOF' mkdir -p /opt/wasixcc-home for attempt in 1 2 3 4 5 6 7 8 9 10 11 12; do rm -rf /opt/wasixcc-home/.wasixcc /tmp/wasixcc-install.sh - if curl -fsSL --retry 5 --retry-all-errors --retry-delay 2 https://wasix.cc -o /tmp/wasixcc-install.sh \ + if curl -fsSL --retry 5 --retry-all-errors --retry-delay 2 "$WASIXCC_INSTALLER_URL" -o /tmp/wasixcc-install.sh \ + && sed -i "s/^WASIXCC_SYSROOT_TAG=.*/WASIXCC_SYSROOT_TAG=\"${WASIXCC_SYSROOT_TAG}\"/" /tmp/wasixcc-install.sh \ + && grep -F "WASIXCC_SYSROOT_TAG=\"${WASIXCC_SYSROOT_TAG}\"" /tmp/wasixcc-install.sh \ && HOME=/opt/wasixcc-home sh /tmp/wasixcc-install.sh \ && wasixcc --version; then exit 0 diff --git a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 index 536bfd88..5087ecf7 100644 --- a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 +++ b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 @@ -1 +1 @@ -320c3099e0f907761229e14a1b0c489ba08a42b81b53ed12ce9916f58008c5b2 +da31e540b581976951b68bd5646e8b5c4a38d174bbd565c292294756e31e8719 diff --git a/src/runtimes/node-direct/tools/build-node-addon.sh b/src/runtimes/node-direct/tools/build-node-addon.sh index 1fc1daf7..26f7b3cc 100755 --- a/src/runtimes/node-direct/tools/build-node-addon.sh +++ b/src/runtimes/node-direct/tools/build-node-addon.sh @@ -49,6 +49,24 @@ to_shell_path() { fi } +tar_list_gzip() { + if [ "$platform" = "windows" ]; then + tar --force-local -tzf "$1" + else + tar -tzf "$1" + fi +} + +tar_extract_gzip() { + archive="$1" + destination="$2" + if [ "$platform" = "windows" ]; then + tar --force-local -C "$destination" --strip-components=1 -xzf "$archive" + else + tar -C "$destination" --strip-components=1 -xzf "$archive" + fi +} + version="$(node -e "console.log(require('./src/runtimes/node-direct/package.json').version)")" node_exec="$(to_shell_path "$(node -p "process.execPath")")" node_bin_dir="$(dirname "$node_exec")" @@ -94,7 +112,7 @@ if [ -z "$node_include" ]; then node_headers_url="https://nodejs.org/dist/v$node_version/node-v$node_version-headers.tar.gz" curl --fail --location --retry 8 --retry-all-errors --retry-delay 5 --connect-timeout 20 \ --output "$node_headers_archive" "$node_headers_url" - tar --force-local -C "$node_headers_dir" --strip-components=1 -xzf "$node_headers_archive" + tar_extract_gzip "$node_headers_archive" "$node_headers_dir" fi fi @@ -266,7 +284,7 @@ tarball="$(to_shell_path "$tarball")" echo "npm pack did not create $tarball" >&2 exit 1 } -if ! tar --force-local -tzf "$tarball" | grep -Fxq "package/prebuilds/oliphaunt_node.node"; then +if ! tar_list_gzip "$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 diff --git a/tools/dev/bootstrap-tools.sh b/tools/dev/bootstrap-tools.sh index 74eea3e6..275acc04 100755 --- a/tools/dev/bootstrap-tools.sh +++ b/tools/dev/bootstrap-tools.sh @@ -129,7 +129,13 @@ install_cargo_binstall() { tmp="$(mktemp -d)" archive="$tmp/$asset" url="https://github.com/cargo-bins/cargo-binstall/releases/download/v${CARGO_BINSTALL_VERSION}/${asset}" - curl -L --fail --retry 3 --output "$archive" "$url" + if ! curl -L --fail --retry 8 --retry-all-errors --retry-delay 5 --connect-timeout 20 --output "$archive" "$url"; then + echo "cargo-binstall download failed; falling back to cargo install cargo-binstall@$CARGO_BINSTALL_VERSION" >&2 + rm -rf "$tmp" + cargo install cargo-binstall --version "$CARGO_BINSTALL_VERSION" --locked --force + installed_pinned_tool_version "$local_binary" "$CARGO_BINSTALL_VERSION" >/dev/null + return + fi case "$extract" in zip) command -v unzip >/dev/null 2>&1 || { From cd1b92f1d9c645f0c3d5d31a5c2632a36a6703b2 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Mon, 29 Jun 2026 11:40:23 +0000 Subject: [PATCH 304/308] fix: avoid nested moon query in release graph --- tools/release/release-graph.mjs | 148 +++++++++++++++++++++++++------- 1 file changed, 116 insertions(+), 32 deletions(-) diff --git a/tools/release/release-graph.mjs b/tools/release/release-graph.mjs index 5d5d0ee0..c103240a 100644 --- a/tools/release/release-graph.mjs +++ b/tools/release/release-graph.mjs @@ -81,6 +81,13 @@ export function commandJson(args, prefix) { return value; } +function gitLines(args) { + return execFileSync("git", args, { cwd: ROOT, encoding: "utf8" }) + .split(/\r?\n/u) + .map((line) => line.trim()) + .filter(Boolean); +} + export function gitOutput(args) { return execFileSync("git", args, { cwd: ROOT, encoding: "utf8" }).trim(); } @@ -140,43 +147,120 @@ function releasePleasePackagesByComponent(prefix) { 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"); +function addDependency(dependencyScopes, projectId, scope) { + if (!projectId || scope === undefined) { + return; } - const parsed = new Map(); - for (const project of projects) { - if (project === null || Array.isArray(project) || typeof project !== "object" || typeof project.id !== "string") { + const existing = dependencyScopes[projectId]; + if (existing === "production" && scope !== "production") { + return; + } + dependencyScopes[projectId] = scope; +} + +function parseTaskDependencyProject(target) { + if (typeof target !== "string" || target.length === 0 || target.startsWith("^")) { + return undefined; + } + const separator = target.indexOf(":"); + return separator > 0 ? target.slice(0, separator) : undefined; +} + +function readMoonProjectConfig(file, prefix) { + const pathParts = file.split("/"); + const source = pathParts.length === 1 ? "." : pathParts.slice(0, -1).join("/"); + let config; + try { + config = Bun.YAML.parse(readFileSync(path.join(ROOT, file), "utf8")); + } catch (error) { + fail(prefix, `${file} is invalid Moon project YAML: ${error.message}`); + } + if (config === null || Array.isArray(config) || typeof config !== "object") { + fail(prefix, `${file} must contain a Moon project object`); + } + const id = config.id; + if (typeof id !== "string" || id.length === 0) { + fail(prefix, `${file} must declare a non-empty Moon project id`); + } + + const dependencyScopes = {}; + const rawDeps = config.dependsOn ?? []; + if (!Array.isArray(rawDeps)) { + fail(prefix, `${file}.dependsOn must be a list when present`); + } + for (const dependency of rawDeps) { + if (typeof dependency === "string") { + addDependency(dependencyScopes, dependency, "production"); + } else if ( + dependency !== null && + typeof dependency === "object" && + !Array.isArray(dependency) && + typeof dependency.id === "string" + ) { + addDependency(dependencyScopes, dependency.id, String(dependency.scope || "production")); + } else { + fail(prefix, `${file}.dependsOn entries must be project ids or dependency objects`); + } + } + + const tasks = config.tasks && typeof config.tasks === "object" && !Array.isArray(config.tasks) ? config.tasks : {}; + for (const [taskId, task] of Object.entries(tasks)) { + if (task === null || Array.isArray(task) || typeof task !== "object" || task.deps === undefined) { 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"); - } + if (!Array.isArray(task.deps)) { + fail(prefix, `${file}.tasks.${taskId}.deps must be a list when present`); + } + for (const dependency of task.deps) { + const target = typeof dependency === "string" + ? dependency + : dependency !== null && typeof dependency === "object" && !Array.isArray(dependency) + ? dependency.target + : undefined; + const projectId = parseTaskDependencyProject(target); + if (projectId !== undefined && projectId !== id) { + addDependency(dependencyScopes, projectId, "build"); } } - parsed.set(project.id, { - id: project.id, - source: project.source || config.source || "", - layer: typeof config.layer === "string" ? config.layer : undefined, - 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 : {}, - }); + } + + const project = + config.project && typeof config.project === "object" && !Array.isArray(config.project) ? { ...config.project } : {}; + if (project.release !== undefined) { + const metadata = + project.metadata && typeof project.metadata === "object" && !Array.isArray(project.metadata) + ? project.metadata + : {}; + project.metadata = { ...metadata, release: project.release }; + delete project.release; + } else if (project.metadata === undefined && Object.keys(project).length > 0) { + project.metadata = {}; + } + return { + id, + source, + layer: typeof config.layer === "string" ? config.layer : undefined, + dependsOn: Object.keys(dependencyScopes).sort(compareText), + dependencyScopes: Object.fromEntries( + Object.entries(dependencyScopes).sort(([left], [right]) => compareText(left, right)), + ), + tags: Array.isArray(config.tags) ? [...config.tags].sort(compareText) : [], + project, + }; +} + +export function moonProjectsById(prefix = "release-graph") { + const files = gitLines(["ls-files", ":(glob)**/moon.yml"]); + if (files.length === 0) { + fail(prefix, "repository does not contain any tracked moon.yml project files"); + } + const parsed = new Map(); + for (const file of files.sort(compareText)) { + const project = readMoonProjectConfig(file, prefix); + if (parsed.has(project.id)) { + fail(prefix, `duplicate Moon project id ${project.id}`); + } + parsed.set(project.id, project); } return parsed; } From a93db4672623d5f6279fb316f1f84517c64faa6f Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Mon, 29 Jun 2026 11:51:14 +0000 Subject: [PATCH 305/308] fix: stabilize release graph project discovery --- src/extensions/tools/check-extension-model.py | 4 +++- tools/release/release-graph.mjs | 15 ++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/extensions/tools/check-extension-model.py b/src/extensions/tools/check-extension-model.py index 2b3b57c2..07b06fd1 100755 --- a/src/extensions/tools/check-extension-model.py +++ b/src/extensions/tools/check-extension-model.py @@ -177,7 +177,9 @@ def release_graph_rows(command: str) -> tuple[dict, ...]: stderr=subprocess.PIPE, ) except (FileNotFoundError, subprocess.CalledProcessError) as error: - detail = getattr(error, "stderr", "") or str(error) + stderr = getattr(error, "stderr", "") or "" + stdout = getattr(error, "output", "") or "" + detail = "\n".join(part for part in [stderr.strip(), stdout.strip()] if part) or str(error) fail(f"failed to query release graph {command}: {detail.strip()}") try: rows = json.loads(output) diff --git a/tools/release/release-graph.mjs b/tools/release/release-graph.mjs index c103240a..ccbd06b3 100644 --- a/tools/release/release-graph.mjs +++ b/tools/release/release-graph.mjs @@ -82,10 +82,15 @@ export function commandJson(args, prefix) { } function gitLines(args) { - return execFileSync("git", args, { cwd: ROOT, encoding: "utf8" }) - .split(/\r?\n/u) - .map((line) => line.trim()) - .filter(Boolean); + try { + return execFileSync("git", args, { cwd: ROOT, encoding: "utf8" }) + .split(/\r?\n/u) + .map((line) => line.trim()) + .filter(Boolean); + } catch (error) { + const detail = error.stderr || error.stdout || error.message; + fail("release-graph", `git ${args.join(" ")} failed: ${String(detail).trim()}`); + } } export function gitOutput(args) { @@ -250,7 +255,7 @@ function readMoonProjectConfig(file, prefix) { } export function moonProjectsById(prefix = "release-graph") { - const files = gitLines(["ls-files", ":(glob)**/moon.yml"]); + const files = gitLines(["ls-files", "*moon.yml"]); if (files.length === 0) { fail(prefix, "repository does not contain any tracked moon.yml project files"); } From 586a474eaa9b55baf7ce1f096808ff8d7a7379d3 Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Mon, 29 Jun 2026 12:05:07 +0000 Subject: [PATCH 306/308] fix: resolve pinned bun on windows checks --- src/extensions/tools/check-extension-model.py | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/src/extensions/tools/check-extension-model.py b/src/extensions/tools/check-extension-model.py index 07b06fd1..406ed33f 100755 --- a/src/extensions/tools/check-extension-model.py +++ b/src/extensions/tools/check-extension-model.py @@ -161,9 +161,66 @@ def format_typescript_source(source: str, path: Path) -> str: fail(f"failed to format generated TypeScript extension metadata with Biome {BIOME_VERSION}: {error}") +@lru_cache(maxsize=1) +def pinned_bun_version() -> str: + for raw_line in (ROOT / ".prototools").read_text(encoding="utf-8").splitlines(): + key, separator, value = raw_line.partition("=") + if separator and key.strip() == "bun": + return value.strip().strip('"') + fail(".prototools must pin a bun version") + + +def pinned_bun_executable() -> str | None: + for name in ["bun.exe", "bun"]: + candidate = shutil.which(name) + if candidate is None: + continue + try: + version = subprocess.check_output( + [candidate, "--version"], + cwd=ROOT, + stderr=subprocess.DEVNULL, + text=True, + ).strip() + except (FileNotFoundError, subprocess.CalledProcessError): + continue + if version == pinned_bun_version(): + return candidate + return None + + +def git_bash_executable() -> str: + candidates: list[Path] = [] + for root in [os.environ.get("ProgramFiles"), os.environ.get("ProgramFiles(x86)")]: + if root: + candidates.extend([Path(root) / "Git/bin/bash.exe", Path(root) / "Git/usr/bin/bash.exe"]) + for name in ["git.exe", "git"]: + git = shutil.which(name) + if git is None: + continue + for parent in Path(git).parents: + if parent.name.lower() == "git": + candidates.extend([parent / "bin/bash.exe", parent / "usr/bin/bash.exe"]) + break + for name in ["bash.exe", "bash"]: + bash = shutil.which(name) + if bash is None: + continue + candidate = Path(bash) + if "system32" not in {part.lower() for part in candidate.parts}: + candidates.append(candidate) + for candidate in candidates: + if candidate.is_file(): + return str(candidate) + fail("failed to find Git for Windows bash.exe; install Git Bash or put it on PATH") + + def bun_command(*args: str) -> list[str]: if os.name == "nt": - return ["bash", "tools/dev/bun.sh", *args] + bun = pinned_bun_executable() + if bun is not None: + return [bun, *args] + return [git_bash_executable(), "tools/dev/bun.sh", *args] return ["tools/dev/bun.sh", *args] From e35e0128591dd5b3519d07c2b42cf141828309aa Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Mon, 29 Jun 2026 12:45:23 +0000 Subject: [PATCH 307/308] fix: run native stripper through active bun --- .../artifacts/native/tools/extension-artifact-packager.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extensions/artifacts/native/tools/extension-artifact-packager.mjs b/src/extensions/artifacts/native/tools/extension-artifact-packager.mjs index 21a67546..40d6f873 100755 --- a/src/extensions/artifacts/native/tools/extension-artifact-packager.mjs +++ b/src/extensions/artifacts/native/tools/extension-artifact-packager.mjs @@ -812,7 +812,7 @@ function stripNativeReleaseBinaries(artifactRoot, nativeTarget) { } stripArgs.push(artifactRoot); const result = spawnSync( - path.join(root, 'tools/dev/bun.sh'), + process.execPath, stripArgs, { cwd: root, stdio: 'inherit' }, ); From ea1ba7bae3af9862275fb675f0f54f9cd714672b Mon Sep 17 00:00:00 2001 From: Sid Jain Date: Mon, 29 Jun 2026 13:00:36 +0000 Subject: [PATCH 308/308] fix: fetch libiconv from gnu mirror --- ...2026-06-07-transitional-catalog-smoke.json | 2 +- .../postgis/dependencies/libiconv/source.toml | 2 +- .../generated/docs/extension-evidence.json | 80 +++++++++---------- .../native/bin/mobile-postgis-extensions.sh | 2 +- .../assets/build/build_wasix_libiconv.sh | 2 +- .../assets/generated/asset-inputs.sha256 | 2 +- 6 files changed, 45 insertions(+), 45 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 79706286..39fea3c0 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:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d", + "sourceDigest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab", "sourceDigestInputs": [ "src/postgres/versions/18/source.toml", "src/extensions/catalog/extensions.promoted.toml", diff --git a/src/extensions/external/postgis/dependencies/libiconv/source.toml b/src/extensions/external/postgis/dependencies/libiconv/source.toml index 2dc016a5..6e1a3fb0 100644 --- a/src/extensions/external/postgis/dependencies/libiconv/source.toml +++ b/src/extensions/external/postgis/dependencies/libiconv/source.toml @@ -1,6 +1,6 @@ name = "libiconv" kind = "archive" -url = "https://ftp.gnu.org/gnu/libiconv/libiconv-1.19.tar.gz" +url = "https://ftpmirror.gnu.org/libiconv/libiconv-1.19.tar.gz" branch = "1.19" commit = "88dd96a8c0464eca144fc791ae60cd31cd8ee78321e67397e25fc095c4a19aa6" sha256 = "88dd96a8c0464eca144fc791ae60cd31cd8ee78321e67397e25fc095c4a19aa6" diff --git a/src/extensions/generated/docs/extension-evidence.json b/src/extensions/generated/docs/extension-evidence.json index 74b7b379..6d6bb16c 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:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -56,7 +56,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -92,7 +92,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -128,7 +128,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -164,7 +164,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -200,7 +200,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -236,7 +236,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -272,7 +272,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -308,7 +308,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -344,7 +344,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -380,7 +380,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -416,7 +416,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -452,7 +452,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -488,7 +488,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -524,7 +524,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -560,7 +560,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -596,7 +596,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -632,7 +632,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -668,7 +668,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -704,7 +704,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -740,7 +740,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -776,7 +776,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -812,7 +812,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -848,7 +848,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -884,7 +884,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -920,7 +920,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -956,7 +956,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -992,7 +992,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -1028,7 +1028,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -1064,7 +1064,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -1100,7 +1100,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -1136,7 +1136,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -1172,7 +1172,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -1208,7 +1208,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -1244,7 +1244,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -1280,7 +1280,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -1316,7 +1316,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -1352,7 +1352,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -1388,7 +1388,7 @@ "restart": "passed", "server": "passed" }, - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d" + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab" } ], "platform-targets": [ @@ -1420,7 +1420,7 @@ "path": "src/extensions/evidence/runs" } ], - "source-digest": "sha256:22fce6e9390277a8c0177b24c98388b78c89b4988a7e7a4847441c69782c791d", + "source-digest": "sha256:2902866ff6bf789f11f4a608ce07a36772e21d8a6773f777324aad6936c2b5ab", "source-digest-inputs": [ "src/postgres/versions/18/source.toml", "src/extensions/catalog/extensions.promoted.toml", diff --git a/src/runtimes/liboliphaunt/native/bin/mobile-postgis-extensions.sh b/src/runtimes/liboliphaunt/native/bin/mobile-postgis-extensions.sh index b229be93..c748a330 100644 --- a/src/runtimes/liboliphaunt/native/bin/mobile-postgis-extensions.sh +++ b/src/runtimes/liboliphaunt/native/bin/mobile-postgis-extensions.sh @@ -257,7 +257,7 @@ build_postgis_libiconv_dependency() { if [ ! -f "$source_tar" ]; then curl -L --fail --silent --show-error \ --retry 8 --retry-all-errors --retry-delay 5 --connect-timeout 20 \ - https://ftp.gnu.org/gnu/libiconv/libiconv-1.19.tar.gz \ + https://ftpmirror.gnu.org/libiconv/libiconv-1.19.tar.gz \ -o "$source_tar" fi printf '%s %s\n' \ diff --git a/src/runtimes/liboliphaunt/wasix/assets/build/build_wasix_libiconv.sh b/src/runtimes/liboliphaunt/wasix/assets/build/build_wasix_libiconv.sh index c5638b79..5d778527 100755 --- a/src/runtimes/liboliphaunt/wasix/assets/build/build_wasix_libiconv.sh +++ b/src/runtimes/liboliphaunt/wasix/assets/build/build_wasix_libiconv.sh @@ -7,7 +7,7 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(oliphaunt_wasix_repo_root "$ROOT")" GENERATED_ROOT="$(oliphaunt_wasix_generated_root "$REPO_ROOT")" LIBICONV_VERSION="${LIBICONV_VERSION:-1.19}" -LIBICONV_URL="${LIBICONV_URL:-https://ftp.gnu.org/gnu/libiconv/libiconv-$LIBICONV_VERSION.tar.gz}" +LIBICONV_URL="${LIBICONV_URL:-https://ftpmirror.gnu.org/libiconv/libiconv-$LIBICONV_VERSION.tar.gz}" LIBICONV_SHA256="${LIBICONV_SHA256:-88dd96a8c0464eca144fc791ae60cd31cd8ee78321e67397e25fc095c4a19aa6}" LIBICONV_SOURCE_DIR="${LIBICONV_SOURCE_DIR:-$REPO_ROOT/target/oliphaunt-sources/checkouts/libiconv}" LIBICONV_ARCHIVE="${LIBICONV_ARCHIVE:-$GENERATED_ROOT/source-cache/libiconv-$LIBICONV_VERSION.tar.gz}" diff --git a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 index 5087ecf7..1d15b4f9 100644 --- a/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 +++ b/src/runtimes/liboliphaunt/wasix/assets/generated/asset-inputs.sha256 @@ -1 +1 @@ -da31e540b581976951b68bd5646e8b5c4a38d174bbd565c292294756e31e8719 +c2fc077126511f0a966cd731aefd26f84027710546872bb35200338fe8c6031f